consumerm模块完成度90%,完善消费者和商家端数据库表,商品、聊天、订单数据对接好了supabase,和商家端对接了聊天功能,安卓端编译通过了css样式,剩余几个页面在处理函数规范问题

This commit is contained in:
cyh666666
2026-02-24 17:17:49 +08:00
parent e2f1dfb097
commit e606c597ca
174 changed files with 37917 additions and 4444 deletions

View File

@@ -1,20 +1,16 @@
// Supabase 配置 // Supabase 配置
// 内网环境 - 本地部署的 Supabase // 内网环境 - 本地部署的 Supabase
// IP: 192.168.1.62 // IP: 192.168.1.62
// IP: 192.168.1.62
// Kong HTTP Port: 8000 // 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_URL: string = 'http://192.168.1.61:18000'
export const SUPA_KEY: string = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlLTEiLCJpYXQiOjE3Njk2NzY0OTgsImV4cCI6MTkyNzM1NjQ5OH0.ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890' 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:// // 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.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://localhost:18000/realtime/v1/websocket'
//export const WS_URL: string = 'ws://192.168.1.63: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' export const TABORPAGE: string = '/pages/mall/consumer/index'
// 测试模式:放开任意跳转(禁用启动页/登录/401 的强制重定向) // 测试模式:放开任意跳转(禁用启动页/登录/401 的强制重定向)
export const IS_TEST_MODE: boolean = true export const IS_TEST_MODE: boolean = true

View File

@@ -331,7 +331,7 @@ export default {
.map-title { .map-title {
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 700;
color: #111; color: #111;
} }

View File

@@ -394,7 +394,7 @@ export default {
.menu-item.active .menu-text { .menu-item.active .menu-text {
color: #3b82f6; color: #3b82f6;
font-weight: 600; font-weight: 700;
} }
.menu-icon { .menu-icon {

View File

@@ -79,11 +79,11 @@ export class AkSupaQueryBuilder {
like(field : string, value : any) : AkSupaQueryBuilder { return this._addCond(field, 'like', value); } like(field : string, value : any) : AkSupaQueryBuilder { return this._addCond(field, 'like', value); }
ilike(field : string, value : any) : AkSupaQueryBuilder { return this._addCond(field, 'ilike', 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); } 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); } contains(field : string, value : any) : AkSupaQueryBuilder { return this._addCond(field, 'cs', value); }
containedBy(field : string, value : any) : AkSupaQueryBuilder { return this._addCond(field, 'cd', value); } containedBy(field : string, value : any) : AkSupaQueryBuilder { return this._addCond(field, 'cd', value); }
not(field : string, opOrValue : any, value?: any) : AkSupaQueryBuilder { not(field : string, opOrValue : any, value: any | null = null) : AkSupaQueryBuilder {
if (value !== undefined) { if (value != null) {
// 三元形式field, operator, value // 三元形式field, operator, value
// 例如 not('badge', 'is', null) -> badge=not.is.null // 例如 not('badge', 'is', null) -> badge=not.is.null
const combinedOp = 'not.' + opOrValue; const combinedOp = 'not.' + opOrValue;
@@ -113,7 +113,7 @@ export class AkSupaQueryBuilder {
return this; 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) //console.log('add cond:', op, afield, value)
const field = encodeURIComponent(afield)!! const field = encodeURIComponent(afield)!!
// 将 null 转换为字符串 'null',避免构造对象时缺少 value 属性 // 将 null 转换为字符串 'null',避免构造对象时缺少 value 属性
@@ -266,7 +266,7 @@ export class AkSupaQueryBuilder {
return params.length > 0 ? params.join('&') : null; return params.length > 0 ? params.join('&') : null;
} }
select(columns ?: string, opt ?: UTSJSONObject) : AkSupaQueryBuilder { select(columns : string = "*", opt : UTSJSONObject | null = null) : AkSupaQueryBuilder {
this._action = 'select'; this._action = 'select';
if (columns != null) { if (columns != null) {
this._options.columns = columns; this._options.columns = columns;
@@ -692,7 +692,7 @@ export class AkSupa {
token_type: 'bearer', token_type: 'bearer',
expires_in: 0, expires_in: 0,
raw: user raw: user
} as any; } as AkSupaSignInResult;
} }
return true; return true;
} catch (e) { } catch (e) {
@@ -999,6 +999,22 @@ async delete(table : string, filter : string | null) : Promise<AkReqResponse<any
from(tableName : string) : AkSupaQueryBuilder { from(tableName : string) : AkSupaQueryBuilder {
return new AkSupaQueryBuilder(this, tableName); return new AkSupaQueryBuilder(this, tableName);
} }
/**
* 创建实时订阅通道 (兼容 Supabase Realtime 接口,目前使用轮询模拟)
* @param topic 通道名称,如 public:table
*/
channel(topic: string): AkSupaRealtimeChannel {
return new AkSupaRealtimeChannel(this, topic);
}
/**
* 移除通道
*/
removeChannel(channel: AkSupaRealtimeChannel): Promise<string> {
channel.unsubscribe();
return Promise.resolve('ok');
}
// AkSupa类内新增自动刷新session // AkSupa类内新增自动刷新session
async refreshSession() : Promise<boolean> { async refreshSession() : Promise<boolean> {
if (this.session == null || this.session?.refresh_token == null) return false; 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); 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; export default AkSupa;

119
doc_mall/consumer/ed.md Normal file
View File

@@ -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-easyinputuni-nav-baruni-data-selectuni-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<SupadbComponentPublicInstance | null>(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<type> .不要用简写[]
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 `<text>|<button>`. there is an error on `<view class="ak-charts-container chart-view">`.
教师端用户id: 7bf7378e-a027-473e-97ac-3460ed3f170a
学生端用户id: eed3824b-bba1-4309-8048-19d17367c084
https://ak3.oulog.com/storage/v1/object/public/
设备id : ak_devices.id :12345678-1234-5678-9abc-123456789012
以下是 UTS Android 兼容性开发要求的简明版,适合 agent/开发者快速理解和执行:
1. 表单优先用 form 组件。
2. 跟 template 交互的变量尽量用一维变量(不要嵌套对象)。
3. 不要用 forEach、map、safeget数组遍历用 for 循环,类型转换用 UTSJSONObject。
4. 数组类型建议写成 Array<Type>,不要用 Type[] 简写。
5. 不支持 undefined变量未赋值就是 null。
6. 变量声明只能用 let 或 const不能用 var。
7. 判断空要用 !== null不能用 !变量。
8. 只支持 type不建议用 interfaceinterface 在 kotlin/swift 下不兼容)。
9. for 循环的 i 必须写明类型let i: Int = 0。
10. 逻辑或用 ||,空值合并用 ??,不能混用。
11. if 判断只能是 boolean 类型。
12. 不支持索引签名Index Signature
13. 类型推断严格,必要时用 as Type 明确类型。
14. 不支持 Intersection Type。
15. picker 用 picker-view 或 uni.showActionSheet 替代,不支持原生 picker。
16. 样式只支持 display: flex不支持 gap、grid、calc()、伪类选择器等复杂 CSS。
17. scroll-view 用 direction="vertical"。
18. 不支持 table、grid、vh 单位、min-width: 100% 等。
19. 组件事件如 picker-view 用 UniPickerViewChangeEvent。
20. 时间选择用 uni_modules/lime-date-time-picker。
21. 类型转换建议用 utils/utis 下的 UTSJSONObject。
22. 在uts setup的android模式下调用的函数必须在调用之前定义
数据获取争取都用强类型方式查询或rpc查询用supa.from.executeAs<T>()方式返回的是result,resultdata一般可以 as Array<T>,按照这个方式处理应该简单直接。update,insert用execute()

View File

@@ -0,0 +1,139 @@
-- =====================================================================================
-- 4. 创建聊天会话表 (ml_chat_rooms)
-- 用于优化聊天列表查询性能,维护用户与商家的会话状态
-- 依赖表: public.ak_users, public.ml_chat_messages
-- =====================================================================================
-- 1. 创建表结构
CREATE TABLE IF NOT EXISTS public.ml_chat_rooms (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE, -- 消费者ID
merchant_id UUID NOT NULL, -- 商家ID/店铺ID
shop_name VARCHAR(100), -- 缓存店铺名称
shop_logo TEXT, -- 缓存店铺Logo
last_message TEXT, -- 最后一条消息内容预览
last_message_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), -- 最后一条消息时间
unread_count INTEGER DEFAULT 0, -- 未读消息数
is_top BOOLEAN DEFAULT FALSE, -- 是否置顶
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
-- 确保同一个用户和同一个商家只有一个会话
UNIQUE(user_id, merchant_id)
);
COMMENT ON TABLE public.ml_chat_rooms IS '聊天会话列表表';
-- 2. 开启 RLS
ALTER TABLE public.ml_chat_rooms ENABLE ROW LEVEL SECURITY;
-- 3. RLS 策略
-- 策略:用户只能查自己的会话
CREATE POLICY ml_chat_rooms_select_policy ON public.ml_chat_rooms
FOR SELECT USING (
auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
);
-- 策略:允许插入(通常由触发器或后端逻辑维护)
CREATE POLICY ml_chat_rooms_insert_policy ON public.ml_chat_rooms
FOR INSERT WITH CHECK (
auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
);
-- 策略:允许更新(如清除未读数)
CREATE POLICY ml_chat_rooms_update_policy ON public.ml_chat_rooms
FOR UPDATE USING (
auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
);
-- =====================================================================================
-- 4. 触发器函数:自动维护会话列表
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.handle_new_chat_message()
RETURNS TRIGGER AS $$
DECLARE
v_user_id UUID;
v_merchant_id UUID;
v_shop_name TEXT;
v_shop_logo TEXT;
v_is_from_user BOOLEAN;
v_count_inc INTEGER;
BEGIN
-- 确定谁是用户,谁是商家
-- 假设 is_from_user 为 true 时sender_id 是用户receiver_id 是商家
-- 假设 is_from_user 为 false 时sender_id 是商家receiver_id 是用户
v_is_from_user := NEW.is_from_user;
IF v_is_from_user THEN
v_user_id := NEW.sender_id;
v_merchant_id := NEW.receiver_id;
v_count_inc := 0; -- 用户自己发的,未读数不变
ELSE
v_user_id := NEW.receiver_id;
v_merchant_id := NEW.sender_id;
v_count_inc := 1; -- 商家发的,用户未读数 +1
END IF;
-- 尝试更新用户的会话
UPDATE public.ml_chat_rooms
SET
last_message = NEW.content,
last_message_at = NEW.created_at,
updated_at = NOW(),
unread_count = unread_count + v_count_inc
WHERE user_id = v_user_id AND merchant_id = v_merchant_id;
-- 如果没有更新到任何行(说明会话不存在),则插入新会话
IF NOT FOUND THEN
-- 尝试从 ml_shops 表获取店铺信息(如果有的话)
BEGIN
SELECT shop_name, shop_logo INTO v_shop_name, v_shop_logo
FROM public.ml_shops
WHERE merchant_id = v_merchant_id
LIMIT 1;
EXCEPTION WHEN OTHERS THEN
-- 忽略错误,使用默认值
v_shop_name := '未知店铺';
v_shop_logo := '';
END;
-- 如果没查到,给个默认名
IF v_shop_name IS NULL THEN
v_shop_name := '店铺 ' || substr(v_merchant_id::text, 1, 8);
END IF;
INSERT INTO public.ml_chat_rooms (
user_id,
merchant_id,
last_message,
last_message_at,
unread_count,
shop_name,
shop_logo
)
VALUES (
v_user_id,
v_merchant_id,
NEW.content,
NEW.created_at,
v_count_inc,
v_shop_name,
v_shop_logo
);
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- 5. 创建触发器
DROP TRIGGER IF EXISTS on_chat_message_inserted ON public.ml_chat_messages;
CREATE TRIGGER on_chat_message_inserted
AFTER INSERT ON public.ml_chat_messages
FOR EACH ROW
EXECUTE FUNCTION public.handle_new_chat_message();

View File

@@ -0,0 +1,78 @@
-- =====================================================================================
-- 8. 修复聊天消息表 (ml_chat_messages) 的 RLS 策略
-- 解决 403 Forbidden 问题 (无法发送消息) 和无法获取聊天记录的问题
-- =====================================================================================
-- 1. 确保表存在 (如果尚未创建)
CREATE TABLE IF NOT EXISTS public.ml_chat_messages (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
sender_id UUID NOT NULL, -- 发送方ID (关联 ak_users.id)
receiver_id UUID NOT NULL, -- 接收方ID (关联 ak_users.id)
content TEXT NOT NULL,
msg_type VARCHAR(20) DEFAULT 'text', -- text, image, etc
is_read BOOLEAN DEFAULT FALSE,
is_from_user BOOLEAN DEFAULT TRUE,
extra_data TEXT, -- 额外JSON数据
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- 2. 开启 RLS
ALTER TABLE public.ml_chat_messages ENABLE ROW LEVEL SECURITY;
-- 3. 清理旧策略 (避免冲突)
DROP POLICY IF EXISTS "Chat messages insert policy" ON public.ml_chat_messages;
DROP POLICY IF EXISTS "Chat messages select policy" ON public.ml_chat_messages;
DROP POLICY IF EXISTS "Users can insert their own messages" ON public.ml_chat_messages;
DROP POLICY IF EXISTS "Users can view their own messages" ON public.ml_chat_messages;
DROP POLICY IF EXISTS "chat_insert_policy" ON public.ml_chat_messages;
DROP POLICY IF EXISTS "chat_select_policy" ON public.ml_chat_messages;
-- 4. 创建新策略
-- 策略:允许用户插入消息 (只要 sender_id 是自己)
CREATE POLICY "chat_messages_insert_policy" ON public.ml_chat_messages
FOR INSERT WITH CHECK (
-- 检查当前登录用户 (auth.uid()) 对应的 ak_users.id 是否等于要插入的 sender_id
EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid()
AND id = sender_id
)
);
-- 策略:允许用户查询消息 (只要自己是 sender_id 或 receiver_id)
CREATE POLICY "chat_messages_select_policy" ON public.ml_chat_messages
FOR SELECT USING (
-- 检查当前登录用户是否是发送者或接收者
EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid()
AND (id = sender_id OR id = receiver_id)
)
);
-- 策略:允许用户更新消息 (仅限标记已读,且自己是接收者)
CREATE POLICY "chat_messages_update_policy" ON public.ml_chat_messages
FOR UPDATE USING (
EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid()
AND id = receiver_id
)
)
WITH CHECK (
EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid()
AND id = receiver_id
)
);
-- =====================================================================================
-- 补充:确保 ak_users 上有适当的索引以提高 RLS 性能
-- =====================================================================================
CREATE INDEX IF NOT EXISTS idx_ak_users_auth_id ON public.ak_users(auth_id);
CREATE INDEX IF NOT EXISTS idx_chat_messages_sender ON public.ml_chat_messages(sender_id);
CREATE INDEX IF NOT EXISTS idx_chat_messages_receiver ON public.ml_chat_messages(receiver_id);
CREATE INDEX IF NOT EXISTS idx_chat_messages_created_at ON public.ml_chat_messages(created_at DESC);

View File

@@ -0,0 +1,60 @@
-- =====================================================================================
-- 8. 修复聊天消息表 (ml_chat_messages) 的 RLS 策略
-- 解决 403 Forbidden 问题 (无法发送消息) 和无法获取聊天记录的问题
-- 说明supabaseService 发送的是 auth.uid(),所以 sender_id/receiver_id 存储的是 Auth ID
-- =====================================================================================
-- 1. 确保表存在 (如果尚未创建)
CREATE TABLE IF NOT EXISTS public.ml_chat_messages (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
sender_id UUID NOT NULL, -- 发送方 Auth ID
receiver_id UUID NOT NULL, -- 接收方 Auth ID (或店铺关联ID)
content TEXT NOT NULL,
msg_type VARCHAR(20) DEFAULT 'text', -- text, image, etc
is_read BOOLEAN DEFAULT FALSE,
is_from_user BOOLEAN DEFAULT TRUE,
extra_data TEXT, -- 额外JSON数据
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- 2. 开启 RLS
ALTER TABLE public.ml_chat_messages ENABLE ROW LEVEL SECURITY;
-- 3. 清理旧策略 (避免冲突)
DROP POLICY IF EXISTS "chat_messages_insert_policy" ON public.ml_chat_messages;
DROP POLICY IF EXISTS "chat_messages_select_policy" ON public.ml_chat_messages;
DROP POLICY IF EXISTS "chat_messages_update_policy" ON public.ml_chat_messages;
-- 4. 创建新策略
-- 策略:允许用户插入消息 (只要 sender_id 是自己)
CREATE POLICY "chat_messages_insert_policy" ON public.ml_chat_messages
FOR INSERT WITH CHECK (
auth.uid() = sender_id
);
-- 策略:允许用户查询消息 (只要自己是 sender_id 或 receiver_id)
-- 注意:如果 receiver_id 是店铺ID商家查询时可能需要额外逻辑 (此处仅保证作为 User 能看到自己的收发)
CREATE POLICY "chat_messages_select_policy" ON public.ml_chat_messages
FOR SELECT USING (
auth.uid() = sender_id
OR
auth.uid() = receiver_id
);
-- 策略:允许用户更新消息 (仅限接收者标记已读)
CREATE POLICY "chat_messages_update_policy" ON public.ml_chat_messages
FOR UPDATE USING (
auth.uid() = receiver_id
)
WITH CHECK (
auth.uid() = receiver_id
);
-- =====================================================================================
-- 5. 补充索引
-- =====================================================================================
CREATE INDEX IF NOT EXISTS idx_chat_messages_sender ON public.ml_chat_messages(sender_id);
CREATE INDEX IF NOT EXISTS idx_chat_messages_receiver ON public.ml_chat_messages(receiver_id);
CREATE INDEX IF NOT EXISTS idx_chat_messages_created_at ON public.ml_chat_messages(created_at DESC);

26
fix_all_font_weights.py Normal file
View File

@@ -0,0 +1,26 @@
import os
target_dir = r'e:\companyproject\mall'
count = 0
for root, dirs, files in os.walk(target_dir):
for file in files:
if file.endswith('.uvue'):
file_path = os.path.join(root, file)
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Replace font-weight: 600 with font-weight: 700
if 'font-weight: 600' in content:
print(f"Fixing {file_path}")
new_content = content.replace('font-weight: 600', 'font-weight: 700')
with open(file_path, 'w', encoding='utf-8') as f:
f.write(new_content)
count += 1
except Exception as e:
print(f"Error processing {file_path}: {e}")
print(f"Fixed {count} files.")

View File

@@ -0,0 +1,28 @@
import os
import re
target_dir = r'e:\companyproject\mall'
count = 0
for root, dirs, files in os.walk(target_dir):
for file in files:
if file.endswith('.uvue'):
file_path = os.path.join(root, file)
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Regex replace font-weight: \s*600 with font-weight: 700
new_content = re.sub(r'font-weight:\s*600', 'font-weight: 700', content)
if content != new_content:
print(f"Fixing {file_path}")
with open(file_path, 'w', encoding='utf-8') as f:
f.write(new_content)
count += 1
except Exception as e:
print(f"Error processing {file_path}: {e}")
print(f"Fixed {count} files.")

37
fix_index_uvue.py Normal file
View File

@@ -0,0 +1,37 @@
import re
file_path = 'e:/companyproject/mall/pages/mall/consumer/index.uvue'
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Fix display: grid -> flex wrap
content = re.sub(r'display:\s*grid;', 'display: flex; flex-wrap: wrap;', content)
content = re.sub(r'grid-template-columns:[^;]+;', '', content)
# Fix display: block/inline-block -> commented out
content = re.sub(r'display:\s*(block|inline-block);', '/* display: \\1; */', content)
# Fix column-count/gap -> remove
content = re.sub(r'column-count:[^;]+;', '', content)
content = re.sub(r'column-gap:[^;]+;', '', content)
# Fix align-items: baseline -> flex-end
content = re.sub(r'align-items:\s*baseline;', 'align-items: flex-end;', content)
# Fix nested selectors in CSS
# .card-icon text -> .card-icon-text
content = content.replace('.card-icon text', '.card-icon-text')
# .service-icon text -> .service-icon-text
content = content.replace('.service-icon text', '.service-icon-text')
# Fix HTML to match new class names
# <view class=\"card-icon\">\n\t\t\t\t\t\t\t<text>{{ category.icon }}</text>\n\t\t\t\t\t\t</view>
content = re.sub(r'(class="card-icon"[^>]*>\s*)<text>', r'\1<text class="card-icon-text">', content)
# <view class=\"service-icon\" ...>\n\t\t\t\t\t\t\t<text>...</text>
content = re.sub(r'(class="service-icon"[^>]*>\s*)<text>', r'\1<text class="service-icon-text">', content)
with open(file_path, 'w', encoding='utf-8') as f:
f.write(content)
print("Successfully fixed index.uvue")

View File

@@ -71,7 +71,7 @@ function onRouteClick(routeId: string): void {
.header-title { .header-title {
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 700;
color: #333; color: #333;
} }
} }

View File

@@ -44,7 +44,7 @@ const props = defineProps<{
.placeholder-title { .placeholder-title {
display: block; display: block;
font-size: 20px; font-size: 20px;
font-weight: 600; font-weight: 700;
color: #333; color: #333;
margin-bottom: 12px; margin-bottom: 12px;
} }

View File

@@ -78,7 +78,7 @@ const cardClass = computed(() => {
.card-title { .card-title {
font-size: 32rpx; font-size: 32rpx;
font-weight: 600; font-weight: 700;
color: #262626; color: #262626;
margin: 0; margin: 0;
} }

View File

@@ -1,17 +1,23 @@
// 简化的main.uts移除i18n依赖 // 简化的main.uts移除i18n依赖
import { createSSRApp } from 'vue' import { createSSRApp } from 'vue'
import App from './App.uvue' import App from './App.uvue'
import i18n from '@/uni_modules/i18n/index.uts'
export function createApp() { export function createApp() {
const app = createSSRApp(App) const app = createSSRApp(App)
// 注册 i18n 全局属性,使组件可以使用 $t 方法 // 注册 i18n 全局属性,使组件可以使用 $t 方法
app.config.globalProperties.$t = (key: string, values?: any, locale?: string): string => { app.config.globalProperties.$t = (key: string, values?: any, locale?: string): string => {
if (!i18n.global) { if (i18n.global == null) {
console.error('i18n is not initialized') console.error('i18n is not initialized')
return key return key
} }
return i18n.global.t(key, values, locale) || key const params = values as UTSJSONObject | null
const res = i18n.global.t(key, params, locale)
if (res.length > 0) {
return res
}
return key
} }
return { app } return { app }

View File

@@ -1,74 +1,93 @@
{ {
"name": "mall", "name": "mall",
"appid": "__UNI__YOUR_APP_ID__", "appid": "__UNI__YOUR_APP_ID__",
"description": "A multi-role e-commerce application.", "description": "A multi-role e-commerce application.",
"versionName": "1.0.0", "versionName": "1.0.0",
"versionCode": "100", "versionCode": "100",
"transformPx": false, "transformPx": false,
"app-plus": { "app-plus": {
"usingComponents": true, "usingComponents": true,
"nvueStyleCompiler": "uni-app", "nvueStyleCompiler": "uni-app",
"compilerVersion": 3, "compilerVersion": 3,
"splashscreen": { "splashscreen": {
"alwaysShowBeforeRender": true, "alwaysShowBeforeRender": true,
"waiting": true, "waiting": true,
"autoclose": true, "autoclose": true,
"delay": 0 "delay": 0
}, },
"modules": {}, "modules": {},
"distribute": { "distribute": {
"android": { "android": {
"permissions": [ "permissions": [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>", "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>", "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>", "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>", "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>", "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>", "<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>", "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>", "<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>", "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>", "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>", "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>", "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>", "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>", "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>", "<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>",
"<uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\"/>", "<uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\"/>",
"<uses-permission android:name=\"android.permission.INTERNET\"/>" "<uses-permission android:name=\"android.permission.INTERNET\"/>"
] ]
}, },
"ios": {}, "ios": {},
"sdkConfigs": {} "sdkConfigs": {}
} }
}, },
"quickapp": {}, "quickapp": {},
"mp-weixin": { "mp-weixin": {
"appid": "", "appid": "",
"setting": { "setting": {
"urlCheck": false "urlCheck": false
}, },
"usingComponents": true "usingComponents": true
}, },
"mp-alipay": { "mp-alipay": {
"usingComponents": true "usingComponents": true
}, },
"mp-baidu": { "mp-baidu": {
"usingComponents": true "usingComponents": true
}, },
"mp-toutiao": { "mp-toutiao": {
"usingComponents": true "usingComponents": true
}, },
"uniStatistics": { "uniStatistics": {
"enable": false "enable": false
}, },
"vueVersion": "3", "vueVersion": "3",
"uni-app-x": {}, "uni-app-x": {},
"h5": { "h5": {
"title": "mall", "title": "mall",
"router": { "router": {
"mode": "hash", "mode": "hash",
"base": "./" "base": "./"
} }
} },
"app-android": {
"distribute": {
"modules": {
"uni-payment": {
"alipay": {},
"wxpay": {}
},
"uni-push": {
"hms": {},
"oppo": {},
"vivo": {},
"xiaomi": {},
"meizu": {},
"honor": {},
"fcm": {}
}
}
}
}
} }

666
pages - 副本.json Normal file
View File

@@ -0,0 +1,666 @@
{
"pages": [
{
"path": "pages/user/login",
"style": {
"navigationBarTitleText": "用户登录",
"navigationStyle": "custom"
}
},
{
"path": "pages/mall/admin/homePage/index",
"style": {
"navigationBarTitleText": "管理后台",
"navigationStyle": "custom"
}
},
{
"path": "pages/user/boot",
"style": {
"navigationBarTitleText": ""
}
},
{
"path": "pages/user/register",
"style": {
"navigationBarTitleText": "注册"
}
},
{
"path": "pages/user/forgot-password",
"style": {
"navigationBarTitleText": "忘记密码"
}
},
{
"path": "pages/user/terms",
"style": {
"navigationBarTitleText": "用户协议与隐私政策"
}
},
{
"path": "pages/user/center",
"style": {
"navigationBarTitleText": "用户中心"
}
},
{
"path": "pages/user/profile",
"style": {
"navigationBarTitleText": "个人资料"
}
},
{
"path": "pages/mall/consumer/index",
"style": {
"navigationBarTitleText": "首页",
"navigationStyle": "custom",
"enablePullDownRefresh": false
}
},
{
"path": "pages/mall/consumer/category",
"style": {
"navigationBarTitleText": "分类"
}
},
{
"path": "pages/mall/consumer/messages",
"style": {
"navigationBarTitleText": "消息",
"enablePullDownRefresh": true
}
},
{
"path": "pages/mall/consumer/cart",
"style": {
"navigationBarTitleText": "购物车"
}
},
{
"path": "pages/mall/consumer/profile",
"style": {
"navigationBarTitleText": "我的"
}
}
],
"subPackages": [
{
"root": "pages/mall/consumer",
"pages": [
{
"path": "settings",
"style": {
"navigationBarTitleText": "设置"
}
},
{
"path": "wallet",
"style": {
"navigationBarTitleText": "我的钱包"
}
},
{
"path": "withdraw",
"style": {
"navigationBarTitleText": "余额提现"
}
},
{
"path": "search",
"style": {
"navigationBarTitleText": "搜索",
"navigationStyle": "custom"
}
},
{
"path": "product-detail",
"style": {
"navigationBarTitleText": "商品详情"
}
},
{
"path": "shop-detail",
"style": {
"navigationBarTitleText": "店铺详情"
}
},
{
"path": "coupons",
"style": {
"navigationBarTitleText": "我的优惠券"
}
},
{
"path": "favorites",
"style": {
"navigationBarTitleText": "我的收藏"
}
},
{
"path": "footprint",
"style": {
"navigationBarTitleText": "我的足迹"
}
},
{
"path": "address-list",
"style": {
"navigationBarTitleText": "收货地址"
}
},
{
"path": "address-edit",
"style": {
"navigationBarTitleText": "编辑地址"
}
},
{
"path": "checkout",
"style": {
"navigationBarTitleText": "确认订单"
}
},
{
"path": "payment",
"style": {
"navigationBarTitleText": "收银台"
}
},
{
"path": "payment-success",
"style": {
"navigationBarTitleText": "支付成功",
"navigationStyle": "custom"
}
},
{
"path": "orders",
"style": {
"navigationBarTitleText": "我的订单",
"enablePullDownRefresh": true
}
},
{
"path": "order-detail",
"style": {
"navigationBarTitleText": "订单详情"
}
},
{
"path": "logistics",
"style": {
"navigationBarTitleText": "物流详情"
}
},
{
"path": "review",
"style": {
"navigationBarTitleText": "评价晒单"
}
},
{
"path": "refund",
"style": {
"navigationBarTitleText": "退款/售后"
}
},
{
"path": "apply-refund",
"style": {
"navigationBarTitleText": "申请售后"
}
},
{
"path": "refund-review",
"style": {
"navigationBarTitleText": "服务评价"
}
},
{
"path": "chat",
"style": {
"navigationBarTitleText": "客服聊天",
"navigationStyle": "custom"
}
},
{
"path": "subscription/plan-list",
"style": {
"navigationBarTitleText": "软件订阅"
}
},
{
"path": "subscription/plan-detail",
"style": {
"navigationBarTitleText": "订阅详情"
}
},
{
"path": "subscription/subscribe-checkout",
"style": {
"navigationBarTitleText": "确认订阅"
}
},
{
"path": "subscription/my-subscriptions",
"style": {
"navigationBarTitleText": "我的订阅"
}
},
{
"path": "subscription/followed-shops",
"style": {
"navigationBarTitleText": "关注店铺"
}
},
{
"path": "points/index",
"style": {
"navigationBarTitleText": "积分管理"
}
},
{
"path": "red-packets/index",
"style": {
"navigationBarTitleText": "我的红包"
}
},
{
"path": "bank-cards/index",
"style": {
"navigationBarTitleText": "银行卡管理"
}
},
{
"path": "bank-cards/add",
"style": {
"navigationBarTitleText": "添加银行卡"
}
}
]
},
{
"root": "pages/mall/delivery",
"pages": [
{
"path": "index",
"style": {
"navigationBarTitleText": "配送中心",
"navigationStyle": "custom"
}
},
{
"path": "order-detail",
"style": {
"navigationBarTitleText": "订单详情页",
"navigationStyle": "custom"
}
},
{
"path": "profile",
"style": {
"navigationBarTitleText": "配送个人中心",
"navigationStyle": "custom"
}
},
{
"path": "order-history",
"style": {
"navigationBarTitleText": "历史记录",
"navigationStyle": "custom"
}
},
{
"path": "earnings",
"style": {
"navigationBarTitleText": "收入明细",
"navigationStyle": "custom"
}
},
{
"path": "tasks",
"style": {
"navigationBarTitleText": "全部任务",
"navigationStyle": "custom"
}
},
{
"path": "task-detail",
"style": {
"navigationBarTitleText": "任务详情",
"navigationStyle": "custom"
}
},
{
"path": "profile-edit",
"style": {
"navigationBarTitleText": "编辑个人资料",
"navigationStyle": "custom"
}
},
{
"path": "ratings",
"style": {
"navigationBarTitleText": "评价",
"navigationStyle": "custom"
}
},
{
"path": "vehicle",
"style": {
"navigationBarTitleText": "车辆管理",
"navigationStyle": "custom"
}
},
{
"path": "vehicle-add",
"style": {
"navigationBarTitleText": "添加车辆",
"navigationStyle": "custom"
}
},
{
"path": "vehicle-edit",
"style": {
"navigationBarTitleText": "编辑车辆",
"navigationStyle": "custom"
}
},
{
"path": "help-center",
"style": {
"navigationBarTitleText": "帮助中心",
"navigationStyle": "custom"
}
},
{
"path": "about",
"style": {
"navigationBarTitleText": "关于我们",
"navigationStyle": "custom"
}
},
{
"path": "feedback",
"style": {
"navigationBarTitleText": "意见反馈",
"navigationStyle": "custom"
}
},
{
"path": "test",
"style": {
"navigationBarTitleText": "test",
"navigationStyle": "custom"
}
},
{
"path": "settings",
"style": {
"navigationBarTitleText": "设置",
"navigationStyle": "custom"
}
}
]
},
{
"root": "pages/mall/analytics",
"pages": [
{
"path": "index",
"style": {
"navigationBarTitleText": "数据分析",
"navigationStyle": "custom"
}
},
{
"path": "profile",
"style": {
"navigationBarTitleText": "数据分析个人中心"
}
},
{
"path": "sales-report",
"style": {
"navigationBarTitleText": "销售报表"
}
},
{
"path": "user-analysis",
"style": {
"navigationBarTitleText": "用户分析"
}
},
{
"path": "product-insights",
"style": {
"navigationBarTitleText": "商品洞察"
}
},
{
"path": "delivery-analysis",
"style": {
"navigationBarTitleText": "配送效率分析"
}
},
{
"path": "coupon-analysis",
"style": {
"navigationBarTitleText": "优惠券效果分析"
}
},
{
"path": "market-trends",
"style": {
"navigationBarTitleText": "市场趋势"
}
},
{
"path": "custom-report",
"style": {
"navigationBarTitleText": "自定义报表"
}
},
{
"path": "report-detail",
"style": {
"navigationBarTitleText": "报表详情",
"enablePullDownRefresh": false
}
},
{
"path": "data-detail",
"style": {
"navigationBarTitleText": "数据分析详情",
"enablePullDownRefresh": false
}
},
{
"path": "insight-detail",
"style": {
"navigationBarTitleText": "数据洞察详情",
"enablePullDownRefresh": false
}
}
]
},
{
"root": "pages/mall/admin",
"pages": [
{
"path": "user-management",
"style": {
"navigationBarTitleText": "用户管理",
"navigationStyle": "custom"
}
},
{
"path": "product-management",
"style": {
"navigationBarTitleText": "商品管理",
"navigationStyle": "custom"
}
},
{
"path": "order-management",
"style": {
"navigationBarTitleText": "订单管理",
"navigationStyle": "custom"
}
},
{
"path": "finance/record",
"style": {
"navigationBarTitleText": "财务管理",
"navigationStyle": "custom"
}
},
{
"path": "user-statistics",
"style": {
"navigationBarTitleText": "用户统计",
"navigationStyle": "custom"
}
},
{
"path": "system-settings",
"style": {
"navigationBarTitleText": "系统设置",
"navigationStyle": "custom"
}
},
{
"path": "subscription/plan-management",
"style": {
"navigationBarTitleText": "订阅方案管理"
}
},
{
"path": "subscription/user-subscriptions",
"style": {
"navigationBarTitleText": "用户订阅管理"
}
},
{
"path": "marketing/coupon/list",
"style": {
"navigationBarTitleText": "优惠券列表"
}
},
{
"path": "marketing/coupon/receive",
"style": {
"navigationBarTitleText": "用户领取记录"
}
},
{
"path": "marketing/signin/rule",
"style": {
"navigationBarTitleText": "签到规则"
}
},
{
"path": "marketing/signin/record",
"style": {
"navigationBarTitleText": "签到记录"
}
}
]
},
{
"root": "pages/mall/merchant",
"pages": [
{
"path": "index",
"style": {
"navigationBarTitleText": "商家中心",
"navigationStyle": "custom"
}
},
{
"path": "product-detail",
"style": {
"navigationBarTitleText": "商品管理详情",
"enablePullDownRefresh": false
}
},
{
"path": "profile",
"style": {
"navigationBarTitleText": "个人资料"
}
}
]
},
{
"root": "pages/mall/service",
"pages": [
{
"path": "index",
"style": {
"navigationBarTitleText": "客服工作台",
"navigationStyle": "custom"
}
},
{
"path": "profile",
"style": {
"navigationBarTitleText": "客服个人中心"
}
},
{
"path": "ticket-detail",
"style": {
"navigationBarTitleText": "工单详情",
"enablePullDownRefresh": false
}
}
]
}
],
"tabBar": {
"color": "#999999",
"selectedColor": "#ff5000",
"backgroundColor": "#ffffff",
"borderStyle": "black",
"list": [
{
"pagePath": "pages/mall/consumer/index",
"text": "首页",
"iconPath": "static/tabbar/home.png",
"selectedIconPath": "static/tabbar/home-active.png"
},
{
"pagePath": "pages/mall/consumer/category",
"text": "分类",
"iconPath": "static/tabbar/category.png",
"selectedIconPath": "static/tabbar/category-active.png"
},
{
"pagePath": "pages/mall/consumer/messages",
"text": "消息",
"iconPath": "static/tabbar/messages.png",
"selectedIconPath": "static/tabbar/messages-active.png"
},
{
"pagePath": "pages/mall/consumer/cart",
"text": "购物车",
"iconPath": "static/tabbar/cart.png",
"selectedIconPath": "static/tabbar/cart-active.png"
},
{
"pagePath": "pages/mall/consumer/profile",
"text": "我的",
"iconPath": "static/tabbar/profile.png",
"selectedIconPath": "static/tabbar/profile-active.png"
}
]
},
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "mall",
"navigationBarBackgroundColor": "#FFFFFF",
"backgroundColor": "#F8F8F8"
}
}

View File

@@ -7,13 +7,13 @@
"navigationStyle": "custom" "navigationStyle": "custom"
} }
}, },
{ // {
"path": "pages/mall/admin/homePage/index", // "path": "pages/mall/admin/homePage/index",
"style": { // "style": {
"navigationBarTitleText": "管理后台", // "navigationBarTitleText": "管理后台",
"navigationStyle": "custom" // "navigationStyle": "custom"
} // }
}, // },
{ {
"path": "pages/user/boot", "path": "pages/user/boot",
"style": { "style": {
@@ -50,6 +50,24 @@
"navigationBarTitleText": "个人资料" "navigationBarTitleText": "个人资料"
} }
}, },
{
"path": "pages/user/change-password",
"style": {
"navigationBarTitleText": "修改密码"
}
},
{
"path": "pages/user/bind-phone",
"style": {
"navigationBarTitleText": "绑定手机"
}
},
{
"path": "pages/user/bind-email",
"style": {
"navigationBarTitleText": "绑定邮箱"
}
},
{ {
"path": "pages/mall/consumer/index", "path": "pages/mall/consumer/index",
"style": { "style": {
@@ -61,7 +79,8 @@
{ {
"path": "pages/mall/consumer/category", "path": "pages/mall/consumer/category",
"style": { "style": {
"navigationBarTitleText": "分类" "navigationBarTitleText": "分类",
"navigationStyle": "custom"
} }
}, },
{ {
@@ -279,345 +298,345 @@
} }
} }
] ]
},
{
"root": "pages/mall/delivery",
"pages": [
{
"path": "index",
"style": {
"navigationBarTitleText": "配送中心",
"navigationStyle": "custom"
}
},
{
"path": "order-detail",
"style": {
"navigationBarTitleText": "订单详情页",
"navigationStyle": "custom"
}
},
{
"path": "profile",
"style": {
"navigationBarTitleText": "配送个人中心",
"navigationStyle": "custom"
}
},
{
"path": "order-history",
"style": {
"navigationBarTitleText": "历史记录",
"navigationStyle": "custom"
}
},
{
"path": "earnings",
"style": {
"navigationBarTitleText": "收入明细",
"navigationStyle": "custom"
}
},
{
"path": "tasks",
"style": {
"navigationBarTitleText": "全部任务",
"navigationStyle": "custom"
}
},
{
"path": "task-detail",
"style": {
"navigationBarTitleText": "任务详情",
"navigationStyle": "custom"
}
},
{
"path": "profile-edit",
"style": {
"navigationBarTitleText": "编辑个人资料",
"navigationStyle": "custom"
}
},
{
"path": "ratings",
"style": {
"navigationBarTitleText": "评价",
"navigationStyle": "custom"
}
},
{
"path": "vehicle",
"style": {
"navigationBarTitleText": "车辆管理",
"navigationStyle": "custom"
}
},
{
"path": "vehicle-add",
"style": {
"navigationBarTitleText": "添加车辆",
"navigationStyle": "custom"
}
},
{
"path": "vehicle-edit",
"style": {
"navigationBarTitleText": "编辑车辆",
"navigationStyle": "custom"
}
},
{
"path": "help-center",
"style": {
"navigationBarTitleText": "帮助中心",
"navigationStyle": "custom"
}
},
{
"path": "about",
"style": {
"navigationBarTitleText": "关于我们",
"navigationStyle": "custom"
}
},
{
"path": "feedback",
"style": {
"navigationBarTitleText": "意见反馈",
"navigationStyle": "custom"
}
},
{
"path": "test",
"style": {
"navigationBarTitleText": "test",
"navigationStyle": "custom"
}
},
{
"path": "settings",
"style": {
"navigationBarTitleText": "设置",
"navigationStyle": "custom"
}
}
]
},
{
"root": "pages/mall/analytics",
"pages": [
{
"path": "index",
"style": {
"navigationBarTitleText": "数据分析",
"navigationStyle": "custom"
}
},
{
"path": "profile",
"style": {
"navigationBarTitleText": "数据分析个人中心"
}
},
{
"path": "sales-report",
"style": {
"navigationBarTitleText": "销售报表"
}
},
{
"path": "user-analysis",
"style": {
"navigationBarTitleText": "用户分析"
}
},
{
"path": "product-insights",
"style": {
"navigationBarTitleText": "商品洞察"
}
},
{
"path": "delivery-analysis",
"style": {
"navigationBarTitleText": "配送效率分析"
}
},
{
"path": "coupon-analysis",
"style": {
"navigationBarTitleText": "优惠券效果分析"
}
},
{
"path": "market-trends",
"style": {
"navigationBarTitleText": "市场趋势"
}
},
{
"path": "custom-report",
"style": {
"navigationBarTitleText": "自定义报表"
}
},
{
"path": "report-detail",
"style": {
"navigationBarTitleText": "报表详情",
"enablePullDownRefresh": false
}
},
{
"path": "data-detail",
"style": {
"navigationBarTitleText": "数据分析详情",
"enablePullDownRefresh": false
}
},
{
"path": "insight-detail",
"style": {
"navigationBarTitleText": "数据洞察详情",
"enablePullDownRefresh": false
}
}
]
},
{
"root": "pages/mall/admin",
"pages": [
{
"path": "user-management",
"style": {
"navigationBarTitleText": "用户管理",
"navigationStyle": "custom"
}
},
{
"path": "product-management",
"style": {
"navigationBarTitleText": "商品管理",
"navigationStyle": "custom"
}
},
{
"path": "order-management",
"style": {
"navigationBarTitleText": "订单管理",
"navigationStyle": "custom"
}
},
{
"path": "finance/record",
"style": {
"navigationBarTitleText": "财务管理",
"navigationStyle": "custom"
}
},
{
"path": "user-statistics",
"style": {
"navigationBarTitleText": "用户统计",
"navigationStyle": "custom"
}
},
{
"path": "system-settings",
"style": {
"navigationBarTitleText": "系统设置",
"navigationStyle": "custom"
}
},
{
"path": "subscription/plan-management",
"style": {
"navigationBarTitleText": "订阅方案管理"
}
},
{
"path": "subscription/user-subscriptions",
"style": {
"navigationBarTitleText": "用户订阅管理"
}
},
{
"path": "marketing/coupon/list",
"style": {
"navigationBarTitleText": "优惠券列表"
}
},
{
"path": "marketing/coupon/receive",
"style": {
"navigationBarTitleText": "用户领取记录"
}
},
{
"path": "marketing/signin/rule",
"style": {
"navigationBarTitleText": "签到规则"
}
},
{
"path": "marketing/signin/record",
"style": {
"navigationBarTitleText": "签到记录"
}
}
]
},
{
"root": "pages/mall/merchant",
"pages": [
{
"path": "index",
"style": {
"navigationBarTitleText": "商家中心",
"navigationStyle": "custom"
}
},
{
"path": "product-detail",
"style": {
"navigationBarTitleText": "商品管理详情",
"enablePullDownRefresh": false
}
},
{
"path": "profile",
"style": {
"navigationBarTitleText": "个人资料"
}
}
]
},
{
"root": "pages/mall/service",
"pages": [
{
"path": "index",
"style": {
"navigationBarTitleText": "客服工作台",
"navigationStyle": "custom"
}
},
{
"path": "profile",
"style": {
"navigationBarTitleText": "客服个人中心"
}
},
{
"path": "ticket-detail",
"style": {
"navigationBarTitleText": "工单详情",
"enablePullDownRefresh": false
}
}
]
} }
// {
// "root": "pages/mall/delivery",
// "pages": [
// {
// "path": "index",
// "style": {
// "navigationBarTitleText": "配送中心",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "order-detail",
// "style": {
// "navigationBarTitleText": "订单详情页",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "profile",
// "style": {
// "navigationBarTitleText": "配送个人中心",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "order-history",
// "style": {
// "navigationBarTitleText": "历史记录",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "earnings",
// "style": {
// "navigationBarTitleText": "收入明细",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "tasks",
// "style": {
// "navigationBarTitleText": "全部任务",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "task-detail",
// "style": {
// "navigationBarTitleText": "任务详情",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "profile-edit",
// "style": {
// "navigationBarTitleText": "编辑个人资料",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "ratings",
// "style": {
// "navigationBarTitleText": "评价",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "vehicle",
// "style": {
// "navigationBarTitleText": "车辆管理",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "vehicle-add",
// "style": {
// "navigationBarTitleText": "添加车辆",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "vehicle-edit",
// "style": {
// "navigationBarTitleText": "编辑车辆",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "help-center",
// "style": {
// "navigationBarTitleText": "帮助中心",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "about",
// "style": {
// "navigationBarTitleText": "关于我们",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "feedback",
// "style": {
// "navigationBarTitleText": "意见反馈",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "test",
// "style": {
// "navigationBarTitleText": "test",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "settings",
// "style": {
// "navigationBarTitleText": "设置",
// "navigationStyle": "custom"
// }
// }
// ]
// },
// {
// "root": "pages/mall/analytics",
// "pages": [
// {
// "path": "index",
// "style": {
// "navigationBarTitleText": "数据分析",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "profile",
// "style": {
// "navigationBarTitleText": "数据分析个人中心"
// }
// },
// {
// "path": "sales-report",
// "style": {
// "navigationBarTitleText": "销售报表"
// }
// },
// {
// "path": "user-analysis",
// "style": {
// "navigationBarTitleText": "用户分析"
// }
// },
// {
// "path": "product-insights",
// "style": {
// "navigationBarTitleText": "商品洞察"
// }
// },
// {
// "path": "delivery-analysis",
// "style": {
// "navigationBarTitleText": "配送效率分析"
// }
// },
// {
// "path": "coupon-analysis",
// "style": {
// "navigationBarTitleText": "优惠券效果分析"
// }
// },
// {
// "path": "market-trends",
// "style": {
// "navigationBarTitleText": "市场趋势"
// }
// },
// {
// "path": "custom-report",
// "style": {
// "navigationBarTitleText": "自定义报表"
// }
// },
// {
// "path": "report-detail",
// "style": {
// "navigationBarTitleText": "报表详情",
// "enablePullDownRefresh": false
// }
// },
// {
// "path": "data-detail",
// "style": {
// "navigationBarTitleText": "数据分析详情",
// "enablePullDownRefresh": false
// }
// },
// {
// "path": "insight-detail",
// "style": {
// "navigationBarTitleText": "数据洞察详情",
// "enablePullDownRefresh": false
// }
// }
// ]
// },
// {
// "root": "pages/mall/admin",
// "pages": [
// {
// "path": "user-management",
// "style": {
// "navigationBarTitleText": "用户管理",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "product-management",
// "style": {
// "navigationBarTitleText": "商品管理",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "order-management",
// "style": {
// "navigationBarTitleText": "订单管理",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "finance/record",
// "style": {
// "navigationBarTitleText": "财务管理",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "user-statistics",
// "style": {
// "navigationBarTitleText": "用户统计",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "system-settings",
// "style": {
// "navigationBarTitleText": "系统设置",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "subscription/plan-management",
// "style": {
// "navigationBarTitleText": "订阅方案管理"
// }
// },
// {
// "path": "subscription/user-subscriptions",
// "style": {
// "navigationBarTitleText": "用户订阅管理"
// }
// },
// {
// "path": "marketing/coupon/list",
// "style": {
// "navigationBarTitleText": "优惠券列表"
// }
// },
// {
// "path": "marketing/coupon/receive",
// "style": {
// "navigationBarTitleText": "用户领取记录"
// }
// },
// {
// "path": "marketing/signin/rule",
// "style": {
// "navigationBarTitleText": "签到规则"
// }
// },
// {
// "path": "marketing/signin/record",
// "style": {
// "navigationBarTitleText": "签到记录"
// }
// }
// ]
// },
// {
// "root": "pages/mall/merchant",
// "pages": [
// {
// "path": "index",
// "style": {
// "navigationBarTitleText": "商家中心",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "product-detail",
// "style": {
// "navigationBarTitleText": "商品管理详情",
// "enablePullDownRefresh": false
// }
// },
// {
// "path": "profile",
// "style": {
// "navigationBarTitleText": "个人资料"
// }
// }
// ]
// },
// {
// "root": "pages/mall/service",
// "pages": [
// {
// "path": "index",
// "style": {
// "navigationBarTitleText": "客服工作台",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "profile",
// "style": {
// "navigationBarTitleText": "客服个人中心"
// }
// },
// {
// "path": "ticket-detail",
// "style": {
// "navigationBarTitleText": "工单详情",
// "enablePullDownRefresh": false
// }
// }
// ]
// }
], ],
"tabBar": { "tabBar": {
"color": "#999999", "color": "#999999",

View File

@@ -1,5 +1,6 @@
<template> <template>
<view class="address-edit-page"> <view class="page-container">
<scroll-view class="address-edit-page" direction="vertical">
<view class="form-group"> <view class="form-group">
<view class="form-item"> <view class="form-item">
<text class="label">收货人</text> <text class="label">收货人</text>
@@ -44,16 +45,17 @@
</view> </view>
<view class="footer-btn"> <view class="footer-btn">
<button class="save-btn" @click="saveAddress">保存</button> <button class="save-btn" @click="saveAddress">保存</button>
<button v-if="isEdit" class="delete-btn" @click="deleteAddress">删除收货地址</button> <button v-if="isEdit" class="delete-btn" @click="deleteAddress">删除收货地址</button>
</view> </view>
</scroll-view>
</view> </view>
</template> </template>
<script setup lang="uts"> <script setup lang="uts">
import { ref, reactive, computed } from 'vue' import { ref, reactive, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app' import { onLoad } from '@dcloudio/uni-app'
import { supabaseService } from '@/utils/supabaseService.uts' import { supabaseService, AddAddressParams, UpdateAddressParams } from '@/utils/supabaseService.uts'
type Address = { type Address = {
id: string id: string
@@ -73,51 +75,45 @@ const regionString = ref('')
const tags = ['家', '公司', '学校'] const tags = ['家', '公司', '学校']
const smartInput = ref('') const smartInput = ref('')
type AddressForm = {
name: string
phone: string
detail: string
isDefault: boolean
label: string
}
const formData = reactive({ const formData = reactive({
name: '', name: '',
phone: '', phone: '',
detail: '', detail: '',
isDefault: false, isDefault: false,
label: '' label: ''
} as { } as AddressForm)
name: string
phone: string
detail: string
isDefault: boolean
label: string
})
onLoad((options) => {
if (options['id']) {
isEdit.value = true
addressId.value = options['id'] as string
loadAddress(addressId.value)
}
})
const loadAddress = async (id: string) => { const loadAddress = async (id: string) => {
try { try {
// 从Supabase加载地址详情 // 从Supabase加载地址详情
const address = await supabaseService.getAddressById(id) const address = await supabaseService.getAddressById(id)
if (address) { if (address != null) {
formData.name = address.recipient_name formData.name = address.recipient_name
formData.phone = address.phone formData.phone = address.phone
formData.detail = address.detail_address formData.detail = address.detail_address
formData.isDefault = address.is_default formData.isDefault = address.is_default
formData.label = address.label || '' formData.label = address.label ?? ''
regionString.value = `${address.province} ${address.city} ${address.district}`.trim() regionString.value = `${address.province} ${address.city} ${address.district}`.trim()
} else { } else {
// 如果Supabase没有找到尝试从本地存储加载 // 如果Supabase没有找到尝试从本地存储加载
const storedAddresses = uni.getStorageSync('addresses') const storedAddresses = uni.getStorageSync('addresses')
if (storedAddresses) { if (storedAddresses != null) {
const addresses = JSON.parse(storedAddresses as string) as Address[] const addresses = JSON.parse(storedAddresses as string) as Address[]
const localAddress = addresses.find(item => item.id === id) const localAddress = addresses.find(item => item.id === id)
if (localAddress) { if (localAddress != null) {
formData.name = localAddress.name formData.name = localAddress.name
formData.phone = localAddress.phone formData.phone = localAddress.phone
formData.detail = localAddress.detail formData.detail = localAddress.detail
formData.isDefault = localAddress.isDefault formData.isDefault = localAddress.isDefault
formData.label = localAddress.label || '' formData.label = localAddress.label ?? ''
regionString.value = `${localAddress.province} ${localAddress.city} ${localAddress.district}`.trim() regionString.value = `${localAddress.province} ${localAddress.city} ${localAddress.district}`.trim()
} }
} }
@@ -126,16 +122,16 @@ const loadAddress = async (id: string) => {
console.error('加载地址详情失败:', error) console.error('加载地址详情失败:', error)
// 失败时从本地存储加载 // 失败时从本地存储加载
const storedAddresses = uni.getStorageSync('addresses') const storedAddresses = uni.getStorageSync('addresses')
if (storedAddresses) { if (storedAddresses != null) {
try { try {
const addresses = JSON.parse(storedAddresses as string) as Address[] const addresses = JSON.parse(storedAddresses as string) as Address[]
const address = addresses.find(item => item.id === id) const address = addresses.find(item => item.id === id)
if (address) { if (address != null) {
formData.name = address.name formData.name = address.name
formData.phone = address.phone formData.phone = address.phone
formData.detail = address.detail formData.detail = address.detail
formData.isDefault = address.isDefault formData.isDefault = address.isDefault
formData.label = address.label || '' formData.label = address.label ?? ''
regionString.value = `${address.province} ${address.city} ${address.district}`.trim() regionString.value = `${address.province} ${address.city} ${address.district}`.trim()
} }
} catch (e) { } catch (e) {
@@ -145,6 +141,14 @@ const loadAddress = async (id: string) => {
} }
} }
onLoad((options) => {
if (options['id'] != null) {
isEdit.value = true
addressId.value = options['id'] as string
loadAddress(addressId.value)
}
})
const selectTag = (tag: string) => { const selectTag = (tag: string) => {
if (formData.label === tag) { if (formData.label === tag) {
formData.label = '' formData.label = ''
@@ -158,28 +162,28 @@ const onSwitchChange = (e: UniSwitchChangeEvent) => {
} }
const saveAddress = async () => { const saveAddress = async () => {
if (!formData.name) { if (formData.name == '') {
uni.showToast({ title: '请填写收货人', icon: 'none' }) uni.showToast({ title: '请填写收货人', icon: 'none' })
return return
} }
if (!formData.phone) { if (formData.phone == '') {
uni.showToast({ title: '请填写手机号码', icon: 'none' }) uni.showToast({ title: '请填写手机号码', icon: 'none' })
return return
} }
if (!regionString.value) { if (regionString.value == '') {
uni.showToast({ title: '请填写所在地区', icon: 'none' }) uni.showToast({ title: '请填写所在地区', icon: 'none' })
return return
} }
if (!formData.detail) { if (formData.detail == '') {
uni.showToast({ title: '请填写详细地址', icon: 'none' }) uni.showToast({ title: '请填写详细地址', icon: 'none' })
return return
} }
// 简单解析地区(这里简化处理,实际应使用选择器) // 简单解析地区(这里简化处理,实际应使用选择器)
const regions = regionString.value.split(' ') const regions = regionString.value.split(' ')
const province = regions[0] || '' const province = regions[0] ?? ''
const city = regions[1] || '' const city = regions[1] ?? ''
const district = regions.slice(2).join(' ') || '' const district = regions.slice(2).join(' ')
// 构建地址对象 // 构建地址对象
const addressData = { const addressData = {
@@ -191,14 +195,25 @@ const saveAddress = async () => {
detail_address: formData.detail, detail_address: formData.detail,
postal_code: '', // 如果需要可以添加邮政编码字段 postal_code: '', // 如果需要可以添加邮政编码字段
is_default: formData.isDefault, is_default: formData.isDefault,
label: formData.label || '' label: formData.label
} } as AddAddressParams
let success = false let success = false
if (isEdit.value) { if (isEdit.value) {
// 更新地址 // 更新地址
success = await supabaseService.updateAddress(addressId.value, addressData) const updateData = {
recipient_name: formData.name,
phone: formData.phone,
province: province,
city: city,
district: district,
detail_address: formData.detail,
postal_code: '',
is_default: formData.isDefault,
label: formData.label
} as UpdateAddressParams
success = await supabaseService.updateAddress(addressId.value, updateData)
} else { } else {
// 添加新地址 // 添加新地址
success = await supabaseService.addAddress(addressData) success = await supabaseService.addAddress(addressData)
@@ -208,7 +223,7 @@ const saveAddress = async () => {
// 同时更新本地存储作为缓存 // 同时更新本地存储作为缓存
const storedAddresses = uni.getStorageSync('addresses') const storedAddresses = uni.getStorageSync('addresses')
let addresses: Address[] = [] let addresses: Address[] = []
if (storedAddresses) { if (storedAddresses != null) {
try { try {
addresses = JSON.parse(storedAddresses as string) as Address[] addresses = JSON.parse(storedAddresses as string) as Address[]
} catch (e) { } catch (e) {
@@ -274,72 +289,79 @@ const saveAddress = async () => {
const parseSmartInput = () => { const parseSmartInput = () => {
const input = smartInput.value.trim() const input = smartInput.value.trim()
if (!input) return if (input == '') return
// 提取手机号 // 提取手机号
const phoneRegex = /(1[3-9]\d{9})/ const phoneRegex = /(1[3-9]\d{9})/
const phoneMatch = input.match(phoneRegex) const phoneMatch = input.match(phoneRegex)
if (phoneMatch) { if (phoneMatch != null) {
formData.phone = phoneMatch[0] formData.phone = phoneMatch[0] ?? ''
} }
// 提取姓名取第一个2-4位中文 // 提取姓名取第一个2-4位中文
const nameRegex = /([\u4e00-\u9fa5]{2,4})/ const nameRegex = /([\u4e00-\u9fa5]{2,4})/
const nameMatch = input.match(nameRegex) const nameMatch = input.match(nameRegex)
if (nameMatch) { if (nameMatch != null) {
formData.name = nameMatch[0] formData.name = nameMatch[0] ?? ''
} }
// 去掉姓名和电话后剩余作为地址 // 去掉姓名和电话后剩余作为地址
let addrText = input let addrText = input
if (formData.name) addrText = addrText.replace(formData.name, '') if (formData.name != '') addrText = addrText.replace(formData.name, '')
if (formData.phone) addrText = addrText.replace(formData.phone, '') if (formData.phone != '') addrText = addrText.replace(formData.phone, '')
addrText = addrText.replace(/[,;\s]+/g, ' ').trim() addrText = addrText.replace(/[,;\s]+/g, ' ').trim()
// 解析省市区 // 解析省市区
const pattern1 = /^(.*?省)?(.*?市)?(.*?[区县])?(.*)$/ const pattern1 = /^(.*?省)?(.*?市)?(.*?[区县])?(.*)$/
const m = addrText.match(pattern1) const m = addrText.match(pattern1)
if (m) { if (m != null) {
const [, province, city, district, detail] = m const province = m[1] ?? ''
regionString.value = `${(province||'').trim()} ${(city||'').trim()} ${(district||'').trim()}`.trim() const city = m[2] ?? ''
formData.detail = (detail||'').trim() const district = m[3] ?? ''
const detail = m[4] ?? ''
regionString.value = `${province.trim()} ${city.trim()} ${district.trim()}`.trim()
formData.detail = detail.trim()
} else { } else {
formData.detail = addrText formData.detail = addrText
} }
} }
const deleteAddress = async () => { const deleteAddress = () => {
uni.showModal({ uni.showModal({
title: '提示', title: '提示',
content: '确定要删除该地址吗?', content: '确定要删除该地址吗?',
success: async (res) => { success: (res: UniShowModalResult) => {
if (res.confirm) { if (res.confirm) {
// 调用Supabase服务删除地址 // 调用Supabase服务删除地址
const success = await supabaseService.deleteAddress(addressId.value) supabaseService.deleteAddress(addressId.value).then((success) => {
if (success) {
if (success) { // 同时从本地存储中移除
// 同时从本地存储中移除 const storedAddresses = uni.getStorageSync('addresses')
const storedAddresses = uni.getStorageSync('addresses') if (storedAddresses != null) {
if (storedAddresses) { try {
let addresses = JSON.parse(storedAddresses as string) as Address[] let addresses = JSON.parse(storedAddresses as string) as Address[]
addresses = addresses.filter(item => item.id !== addressId.value) addresses = addresses.filter(item => item.id !== addressId.value)
uni.setStorageSync('addresses', JSON.stringify(addresses)) uni.setStorageSync('addresses', JSON.stringify(addresses))
} catch (e) {
console.error('解析本地地址数据失败', e)
}
}
uni.showToast({
title: '删除成功',
icon: 'success'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
} else {
console.error('删除地址失败')
uni.showToast({
title: '删除失败',
icon: 'none'
})
} }
})
uni.showToast({
title: '删除成功',
icon: 'success'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
} else {
console.error('删除地址失败')
uni.showToast({
title: '删除失败',
icon: 'none'
})
}
} }
} }
}) })
@@ -347,10 +369,14 @@ const deleteAddress = async () => {
</script> </script>
<style> <style>
.page-container {
flex: 1;
display: flex;
flex-direction: column;
}
.address-edit-page { .address-edit-page {
min-height: 100vh; flex: 1; /* Replace min-height: 100vh */
background-color: #f5f5f5;
padding-top: 15px;
} }
.form-group { .form-group {

View File

@@ -51,7 +51,7 @@ type Address = {
const addresses = ref<Address[]>([]) const addresses = ref<Address[]>([])
const selectionMode = ref<boolean>(false) const selectionMode = ref<boolean>(false)
let openerEventChannel: any = null let openerEventChannel: any | null = null
onShow(() => { onShow(() => {
loadAddresses() loadAddresses()

View File

@@ -34,8 +34,8 @@
</template> </template>
<script setup lang="uts"> <script setup lang="uts">
import { ref, onMounted } from 'vue' import { ref, onMounted, getCurrentInstance } from 'vue'
import { onShow } from '@dcloudio/uni-app' import { onShow, onLoad } from '@dcloudio/uni-app'
import { supabaseService, type UserAddress as SupabaseUserAddress } from '@/utils/supabaseService.uts' import { supabaseService, type UserAddress as SupabaseUserAddress } from '@/utils/supabaseService.uts'
type Address = { type Address = {
@@ -52,25 +52,6 @@ type Address = {
const addresses = ref<Address[]>([]) const addresses = ref<Address[]>([])
const selectionMode = ref<boolean>(false) const selectionMode = ref<boolean>(false)
let openerEventChannel: any = null
onShow(() => {
loadAddresses()
})
onMounted(() => {
try {
const ec = uni.getOpenerEventChannel()
openerEventChannel = ec
ec?.on('setSelectMode', (data: any) => {
if (data && typeof data.selectMode === 'boolean') {
selectionMode.value = data.selectMode
}
})
} catch (e) {
// ignore
}
})
const loadAddresses = async () => { const loadAddresses = async () => {
try { try {
@@ -78,17 +59,19 @@ const loadAddresses = async () => {
const supabaseAddresses = await supabaseService.getAddresses() const supabaseAddresses = await supabaseService.getAddresses()
// 转换数据格式以匹配前端界面 // 转换数据格式以匹配前端界面
const transformedAddresses = supabaseAddresses.map((item: SupabaseUserAddress) => ({ const transformedAddresses = supabaseAddresses.map((item: SupabaseUserAddress): Address => {
id: item.id, return {
name: item.recipient_name, id: item.id,
phone: item.phone, name: item.recipient_name,
province: item.province, phone: item.phone,
city: item.city, province: item.province,
district: item.district, city: item.city,
detail: item.detail_address, district: item.district,
isDefault: item.is_default, detail: item.detail_address,
label: '' // Supabase表没有label字段可以后续考虑添加或使用其他字段 isDefault: item.is_default,
})) label: '' // Supabase表没有label字段
} as Address
})
addresses.value = transformedAddresses addresses.value = transformedAddresses
@@ -98,7 +81,7 @@ const loadAddresses = async () => {
console.error('加载地址数据失败:', error) console.error('加载地址数据失败:', error)
// 如果API调用失败尝试从本地存储加载 // 如果API调用失败尝试从本地存储加载
const storedAddresses = uni.getStorageSync('addresses') const storedAddresses = uni.getStorageSync('addresses')
if (storedAddresses) { if (storedAddresses != null) {
try { try {
addresses.value = JSON.parse(storedAddresses as string) as Address[] addresses.value = JSON.parse(storedAddresses as string) as Address[]
} catch (e) { } catch (e) {
@@ -111,6 +94,19 @@ const loadAddresses = async () => {
} }
} }
onLoad((options) => {
if (options['selectMode'] == 'true') {
selectionMode.value = true
}
})
onShow(() => {
loadAddresses()
})
// onMounted logic for EventChannel removed as it is not fully supported in UTS Android
// Using uni.$emit for global event communication instead
const getFullAddress = (item: Address): string => { const getFullAddress = (item: Address): string => {
return `${item.province}${item.city}${item.district} ${item.detail}` return `${item.province}${item.city}${item.district} ${item.detail}`
} }
@@ -122,34 +118,34 @@ const addAddress = () => {
} }
// 删除地址 // 删除地址
const deleteAddress = async (id: string) => { const deleteAddress = (id: string) => {
uni.showModal({ uni.showModal({
title: '提示', title: '提示',
content: '确定要删除该地址吗?', content: '确定要删除该地址吗?',
success: async (res) => { success: (res) => {
if (res.confirm) { if (res.confirm) {
// 调用Supabase服务删除地址 // 调用Supabase服务删除地址
const success = await supabaseService.deleteAddress(id) supabaseService.deleteAddress(id).then((success) => {
if (success) {
if (success) { // 从本地列表移除
// 从本地列表移除 const index = addresses.value.findIndex(addr => addr.id === id)
const index = addresses.value.findIndex(addr => addr.id === id) if (index !== -1) {
if (index !== -1) { addresses.value.splice(index, 1)
addresses.value.splice(index, 1) // 更新本地存储缓存
// 更新本地存储缓存 uni.setStorageSync('addresses', JSON.stringify(addresses.value))
uni.setStorageSync('addresses', JSON.stringify(addresses.value)) uni.showToast({
title: '删除成功',
icon: 'success'
})
}
} else {
console.error('删除地址失败')
uni.showToast({ uni.showToast({
title: '删除成功', title: '删除失败',
icon: 'success' icon: 'none'
}) })
} }
} else { })
console.error('删除地址失败')
uni.showToast({
title: '删除失败',
icon: 'none'
})
}
} }
} }
}) })
@@ -162,8 +158,8 @@ const editAddress = (id: string) => {
} }
const selectAddress = (item: Address) => { const selectAddress = (item: Address) => {
if (selectionMode.value && openerEventChannel) { if (selectionMode.value) {
openerEventChannel.emit('addressSelected', { uni.$emit('addressSelected', {
id: item.id, id: item.id,
recipient_name: item.name, recipient_name: item.name,
phone: item.phone, phone: item.phone,
@@ -188,7 +184,14 @@ const selectAddress = (item: Address) => {
flex-direction: column; /* 竖向排列图标 */ flex-direction: column; /* 竖向排列图标 */
justify-content: center; justify-content: center;
align-items: center; align-items: center;
gap: 15px; }
.action-item {
margin-bottom: 15px;
}
.action-item:last-child {
margin-bottom: 0px;
} }
.footer-btn { .footer-btn {
@@ -198,11 +201,12 @@ const selectAddress = (item: Address) => {
right: 0; right: 0;
background-color: white; background-color: white;
padding: 10px 15px; padding: 10px 15px;
padding-bottom: calc(10px + env(safe-area-inset-bottom)); padding-bottom: 30px;
box-shadow: 0 -2px 10px rgba(0,0,0,0.05); box-shadow: 0 -2px 10px rgba(0,0,0,0.05);
display: flex; display: flex;
justify-content: center; /* 居中显示 */ justify-content: center;
align-items: center; align-items: center;
z-index: 100;
} }
.add-btn { .add-btn {
@@ -214,7 +218,6 @@ const selectAddress = (item: Address) => {
line-height: 44px; line-height: 44px;
border: none; border: none;
width: 100%; /* 默认占满 */ width: 100%; /* 默认占满 */
max-width: 100%;
} }
/* 响应式布局优化 */ /* 响应式布局优化 */
@@ -234,7 +237,7 @@ const selectAddress = (item: Address) => {
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
box-shadow: 0 -2px 10px rgba(0,0,0,0.05); box-shadow: 0 -2px 10px rgba(0,0,0,0.05);
border-radius: 12px 12px 0 0; /* 桌面端加点圆角更美观 */ border-radius: 12px 12px 0 0;
} }
.add-btn { .add-btn {

View File

@@ -131,7 +131,8 @@
</template> </template>
<script setup lang="uts"> <script setup lang="uts">
import { ref, onMounted } from 'vue' import { ref, onMounted, onUnmounted } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import supa from '@/components/supadb/aksupainstance.uts' import supa from '@/components/supadb/aksupainstance.uts'
type AddressType = { type AddressType = {
@@ -150,7 +151,6 @@ type AddressType = {
const addressList = ref<Array<AddressType>>([]) const addressList = ref<Array<AddressType>>([])
const fromSelect = ref<boolean>(false) const fromSelect = ref<boolean>(false)
const selectCallback = ref<any>(null)
const showNewAddressForm = ref<boolean>(false) const showNewAddressForm = ref<boolean>(false)
const newAddress = ref<any>({ const newAddress = ref<any>({
recipient_name: '', recipient_name: '',
@@ -163,16 +163,14 @@ const newAddress = ref<any>({
}) })
const smartAddressInput = ref<string>('') const smartAddressInput = ref<string>('')
onLoad((options: any) => {
const optObj = (options instanceof UTSJSONObject) ? (options as UTSJSONObject) : (JSON.parse(JSON.stringify(options ?? {})) as UTSJSONObject)
const flag = optObj.getString('fromSelect') ?? ''
fromSelect.value = (flag == '1' || flag == 'true')
})
// 生命周期 // 生命周期
onMounted(() => { onMounted(() => {
const eventChannel = uni.getEventChannel()
if (eventChannel) {
eventChannel.on('fromSelect', (data: any) => {
fromSelect.value = data.fromSelect || false
selectCallback.value = data.callback
})
}
loadAddresses() loadAddresses()
// 监听地址更新事件从checkout页面或其他页面 // 监听地址更新事件从checkout页面或其他页面
@@ -222,7 +220,11 @@ const loadAddresses = async () => {
// 获取当前用户ID // 获取当前用户ID
const getCurrentUserId = (): string | null => { const getCurrentUserId = (): string | null => {
const userStore = uni.getStorageSync('userInfo') const userStore = uni.getStorageSync('userInfo')
return userStore?.id || null if (userStore instanceof UTSJSONObject) {
return userStore.getString('id')
}
const obj = JSON.parse(JSON.stringify(userStore ?? {})) as UTSJSONObject
return obj.getString('id')
} }
// 获取完整地址 // 获取完整地址
@@ -232,9 +234,11 @@ const getFullAddress = (address: AddressType): string => {
// 选择地址 // 选择地址
const selectAddress = (address: AddressType) => { const selectAddress = (address: AddressType) => {
if (fromSelect.value && selectCallback.value) { if (fromSelect.value) {
// 返回选择的地址 try {
selectCallback.value(address) uni.setStorageSync('selectedAddress', JSON.stringify(address))
} catch (e) {}
uni.$emit('addressSelected', address)
uni.navigateBack() uni.navigateBack()
} }
} }

View File

@@ -4,11 +4,11 @@
<view class="section-title">退款类型</view> <view class="section-title">退款类型</view>
<radio-group @change="handleTypeChange" class="type-group"> <radio-group @change="handleTypeChange" class="type-group">
<label class="type-item"> <label class="type-item">
<radio value="1" :checked="refundType === 1" color="#ff4444" /> <radio value="1" :checked="refundType === 1" color="#ff4444" class="type-radio" />
<text>仅退款</text> <text>仅退款</text>
</label> </label>
<label class="type-item"> <label class="type-item">
<radio value="2" :checked="refundType === 2" color="#ff4444" /> <radio value="2" :checked="refundType === 2" color="#ff4444" class="type-radio" />
<text>退货退款</text> <text>退货退款</text>
</label> </label>
</radio-group> </radio-group>
@@ -56,7 +56,8 @@
</template> </template>
<script setup lang="uts"> <script setup lang="uts">
import { ref, onMounted } from 'vue' import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { supabaseService } from '@/utils/supabaseService.uts' import { supabaseService } from '@/utils/supabaseService.uts'
const orderId = ref('') const orderId = ref('')
@@ -79,24 +80,15 @@ const reasonList = [
'其他' '其他'
] ]
onMounted(() => {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const options = currentPage.options as any
if (options.orderId) {
orderId.value = options.orderId
loadOrderInfo()
}
})
const loadOrderInfo = async () => { const loadOrderInfo = async () => {
try { try {
const orderData = await supabaseService.getOrderDetail(orderId.value) const orderData = await supabaseService.getOrderDetail(orderId.value)
if (orderData != null) { if (orderData != null) {
const total = Number(orderData['total_amount'] ?? 0) // Cast to UTSJSONObject to access properties safely
const shipping = Number(orderData['shipping_fee'] ?? 0) const order = orderData as UTSJSONObject
const total = order.getNumber('total_amount') ?? 0
const shipping = order.getNumber('shipping_fee') ?? 0
maxAmount.value = total maxAmount.value = total
deliveryFee.value = shipping deliveryFee.value = shipping
@@ -111,17 +103,37 @@ const loadOrderInfo = async () => {
} }
} }
onLoad((options) => {
if (options['orderId'] != null) {
orderId.value = options['orderId'] as string
loadOrderInfo()
}
})
const handleTypeChange = (e: any) => { const handleTypeChange = (e: any) => {
refundType.value = parseInt(e.detail.value) // Use bracket notation to access detail property safely on 'any' type in UTS
// The structure is e -> detail -> value
// We need to cast e to UTSJSONObject first if we want to use bracket notation,
// OR we can use JSON.parse/stringify trick if simple casting fails,
// BUT the most standard way for UTS 'any' which is actually a Map/JSONObject at runtime:
const target = e as UTSJSONObject
const detail = target['detail'] as UTSJSONObject
const value = detail['value'] as string
refundType.value = parseInt(value)
} }
const handleReasonChange = (e: any) => { const handleReasonChange = (e: any) => {
const index = e.detail.value as number // Use bracket notation to access detail property safely on 'any' type in UTS
const target = e as UTSJSONObject
const detail = target['detail'] as UTSJSONObject
const value = detail['value'] as number
const index = value
refundReason.value = reasonList[index] refundReason.value = reasonList[index]
} }
const submitRefund = async () => { const submitRefund = async () => {
if (!refundReason.value) { if (refundReason.value == '') {
uni.showToast({ title: '请选择退款原因', icon: 'none' }) uni.showToast({ title: '请选择退款原因', icon: 'none' })
return return
} }
@@ -166,7 +178,7 @@ const submitRefund = async () => {
<style scoped> <style scoped>
.apply-refund-page { .apply-refund-page {
min-height: 100vh; flex: 1;
background-color: #f5f5f5; background-color: #f5f5f5;
padding: 15px; padding: 15px;
padding-bottom: 80px; padding-bottom: 80px;
@@ -189,16 +201,19 @@ const submitRefund = async () => {
.type-group { .type-group {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 15px;
} }
.type-item { .type-item {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; margin-bottom: 15px;
font-size: 14px; font-size: 14px;
} }
.type-radio {
margin-right: 10px;
}
.picker-content { .picker-content {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;

View File

@@ -25,7 +25,7 @@
</view> </view>
<view class="action-section"> <view class="action-section">
<button class="submit-btn" :disabled="loading" @click="submit">确认添加</button> <button class="submit-btn" :class="{ disabled: loading }" :disabled="loading" @click="submit">确认添加</button>
</view> </view>
</view> </view>
</template> </template>
@@ -34,6 +34,14 @@
import { ref, reactive } from 'vue' import { ref, reactive } from 'vue'
import { supabaseService } from '@/utils/supabaseService.uts' import { supabaseService } from '@/utils/supabaseService.uts'
type BankCardForm = {
holder_name: string
card_no: string
bank_name: string
phone: string
is_default: boolean
}
const loading = ref(false) const loading = ref(false)
const form = reactive({ const form = reactive({
holder_name: '', holder_name: '',
@@ -41,7 +49,7 @@ const form = reactive({
bank_name: '', bank_name: '',
phone: '', phone: '',
is_default: false is_default: false
}) } as BankCardForm)
const onSwitchChange = (e: UniSwitchChangeEvent) => { const onSwitchChange = (e: UniSwitchChangeEvent) => {
form.is_default = e.detail.value form.is_default = e.detail.value
@@ -60,7 +68,7 @@ const detectBank = (e: any) => {
} }
const submit = async () => { const submit = async () => {
if (!form.holder_name || !form.card_no || !form.bank_name) { if (form.holder_name == '' || form.card_no == '' || form.bank_name == '') {
uni.showToast({ title: '请完善卡片信息', icon: 'none' }) uni.showToast({ title: '请完善卡片信息', icon: 'none' })
return return
} }
@@ -71,6 +79,7 @@ const submit = async () => {
const cardData = new UTSJSONObject() const cardData = new UTSJSONObject()
cardData.set('holder_name', form.holder_name) cardData.set('holder_name', form.holder_name)
cardData.set('bank_name', form.bank_name) cardData.set('bank_name', form.bank_name)
cardData.set('card_no', form.card_no) // Also save full card no if needed, or just last4
// 截取后4位 // 截取后4位
const last4 = form.card_no.length > 4 ? form.card_no.slice(-4) : form.card_no const last4 = form.card_no.length > 4 ? form.card_no.slice(-4) : form.card_no
cardData.set('card_no_last4', last4) cardData.set('card_no_last4', last4)
@@ -100,8 +109,7 @@ const submit = async () => {
<style> <style>
.add-card-page { .add-card-page {
background-color: #f5f5f5; background-color: #f5f5f5;
min-height: 100vh; flex: 1;
padding-top: 20px;
} }
.form-container { .form-container {
@@ -146,7 +154,7 @@ const submit = async () => {
font-size: 16px; font-size: 16px;
} }
.submit-btn[disabled] { .submit-btn.disabled {
opacity: 0.6; opacity: 0.6;
} }
</style> </style>

View File

@@ -28,7 +28,6 @@
<script setup lang="uts"> <script setup lang="uts">
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { supabaseService } from '@/utils/supabaseService.uts' import { supabaseService } from '@/utils/supabaseService.uts'
type BankCard = { type BankCard = {
@@ -44,15 +43,15 @@ type BankCard = {
const cards = ref<BankCard[]>([]) const cards = ref<BankCard[]>([])
const loading = ref(true) const loading = ref(true)
onShow(() => {
loadData()
})
const loadData = async () => { const loadData = async () => {
loading.value = true loading.value = true
try { try {
const rawList = await supabaseService.getUserBankCards() const rawList = await supabaseService.getUserBankCards()
cards.value = rawList.map((item: any): BankCard => { const cardList: BankCard[] = []
// Use for loop instead of map to avoid complex closure typing issues
for (let i = 0; i < rawList.length; i++) {
const item = rawList[i]
let id = '' let id = ''
let bankName = '' let bankName = ''
let last4 = '' let last4 = ''
@@ -61,22 +60,23 @@ const loadData = async () => {
let isDef = false let isDef = false
if (item instanceof UTSJSONObject) { if (item instanceof UTSJSONObject) {
id = item.getString('id') || '' id = item.getString('id') ?? ''
bankName = item.getString('bank_name') || '' bankName = item.getString('bank_name') ?? ''
last4 = item.getString('card_no_last4') || '' last4 = item.getString('card_no_last4') ?? ''
type = item.getString('card_type') || 'debit' type = item.getString('card_type') ?? 'debit'
holder = item.getString('holder_name') || '' holder = item.getString('holder_name') ?? ''
isDef = item.getBoolean('is_default') || false isDef = item.getBoolean('is_default') ?? false
} else { } else {
id = (item['id'] as string) || '' const obj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
bankName = (item['bank_name'] as string) || '' id = obj.getString('id') ?? ''
last4 = (item['card_no_last4'] as string) || '' bankName = obj.getString('bank_name') ?? ''
type = (item['card_type'] as string) || 'debit' last4 = obj.getString('card_no_last4') ?? ''
holder = (item['holder_name'] as string) || '' type = obj.getString('card_type') ?? 'debit'
isDef = (item['is_default'] as boolean) || false holder = obj.getString('holder_name') ?? ''
isDef = obj.getBoolean('is_default') ?? false
} }
return { cardList.push({
id: id, id: id,
user_id: '', user_id: '',
bank_name: bankName, bank_name: bankName,
@@ -84,8 +84,10 @@ const loadData = async () => {
card_type: type, card_type: type,
holder_name: holder, holder_name: holder,
is_default: isDef is_default: isDef
} as BankCard } as BankCard)
}) }
cards.value = cardList
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} finally { } finally {
@@ -93,6 +95,10 @@ const loadData = async () => {
} }
} }
onShow(() => {
loadData()
})
const addCard = () => { const addCard = () => {
uni.navigateTo({ uni.navigateTo({
url: '/pages/mall/consumer/bank-cards/add' url: '/pages/mall/consumer/bank-cards/add'
@@ -103,15 +109,16 @@ const deleteCard = (card: BankCard) => {
uni.showModal({ uni.showModal({
title: '删除银行卡', title: '删除银行卡',
content: `确认删除尾号${card.card_no_last4}的${card.bank_name}卡片吗?`, content: `确认删除尾号${card.card_no_last4}的${card.bank_name}卡片吗?`,
success: async (res) => { success: (res) => {
if (res.confirm) { if (res.confirm) {
const success = await supabaseService.deleteBankCard(card.id) supabaseService.deleteBankCard(card.id).then((success) => {
if (success) { if (success) {
uni.showToast({ title: '已删除' }) uni.showToast({ title: '已删除' })
loadData() loadData()
} else { } else {
uni.showToast({ title: '删除失败', icon: 'none' }) uni.showToast({ title: '删除失败', icon: 'none' })
} }
})
} }
} }
}) })
@@ -131,10 +138,7 @@ const getCardClass = (bankName: string): string => {
.bank-cards-page { .bank-cards-page {
padding: 15px; padding: 15px;
background-color: #f5f5f5; background-color: #f5f5f5;
min-height: 100vh; flex: 1;
}
.card-item {
height: 140px; height: 140px;
border-radius: 12px; border-radius: 12px;
margin-bottom: 15px; margin-bottom: 15px;
@@ -212,7 +216,7 @@ const getCardClass = (bankName: string): string => {
.plus-icon { .plus-icon {
font-size: 24px; font-size: 24px;
margin-right: 5px; margin-right: 5px;
font-weight: 300; /* font-weight: 300; removed */
} }
.delete-btn { .delete-btn {
@@ -222,7 +226,7 @@ const getCardClass = (bankName: string): string => {
width: 24px; width: 24px;
height: 24px; height: 24px;
background-color: rgba(0,0,0,0.2); background-color: rgba(0,0,0,0.2);
border-radius: 50%; border-radius: 12px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;

View File

@@ -41,7 +41,7 @@
<text v-else class="unselected-icon"></text> <text v-else class="unselected-icon"></text>
</view> </view>
<text class="shop-icon" @click="navigateToShop(group.shopId, group.merchantId)">🏪</text> <text class="shop-icon" @click="navigateToShop(group.shopId, group.merchantId)">🏪</text>
<text class="shop-name" @click="navigateToShop(group.shopId, group.merchantId)">{{ group.shopName }}</text> <text class="shop-name" :lines="1" @click="navigateToShop(group.shopId, group.merchantId)">{{ group.shopName }}</text>
<text class="shop-arrow" @click="navigateToShop(group.shopId, group.merchantId)">></text> <text class="shop-arrow" @click="navigateToShop(group.shopId, group.merchantId)">></text>
</view> </view>
@@ -65,7 +65,7 @@
<view class="item-info"> <view class="item-info">
<view class="info-top"> <view class="info-top">
<text class="item-name">{{ item.name }}</text> <text class="item-name" :lines="1">{{ item.name }}</text>
<text class="item-spec">{{ item.spec }}</text> <text class="item-spec">{{ item.spec }}</text>
</view> </view>
@@ -125,7 +125,7 @@
:src="product.image" :src="product.image"
mode="aspectFill" mode="aspectFill"
/> />
<text class="recommend-name">{{ product.name }}</text> <text class="recommend-name" :lines="2">{{ product.name }}</text>
<view class="recommend-bottom"> <view class="recommend-bottom">
<text class="recommend-price">¥{{ product.price }}</text> <text class="recommend-price">¥{{ product.price }}</text>
<view class="recommend-add-btn" @click.stop="addToCart(product)"> <view class="recommend-add-btn" @click.stop="addToCart(product)">
@@ -170,55 +170,93 @@ import { ref, computed, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app' import { onShow } from '@dcloudio/uni-app'
import { supabaseService, type CartItem as SupabaseCartItem, type Product } from '@/utils/supabaseService.uts' import { supabaseService, type CartItem as SupabaseCartItem, type Product } from '@/utils/supabaseService.uts'
type LocalCartItem = {
id: string
shopId: string
shopName: string
name: string
price: number
image: string
spec: string
quantity: number
selected: boolean
productId: string
skuId: string
merchantId: string
}
type CartGroup = {
shopId: string
shopName: string
merchantId: string
items: LocalCartItem[]
}
type RecommendProduct = {
id: string
shopId: string
shopName: string
name: string
price: number
image: string
skuId: string
}
// 响应式数据 // 响应式数据
const cartItems = ref<any[]>([]) const cartItems = ref<LocalCartItem[]>([])
const recommendProducts = ref<any[]>([]) const recommendProducts = ref<RecommendProduct[]>([])
const loading = ref<boolean>(false) const loading = ref<boolean>(false)
const statusBarHeight = ref(0) const statusBarHeight = ref(0)
const isManageMode = ref(false) const isManageMode = ref(false)
const updatingItems = ref<Set<string>>(new Set()) // Track items being updated to prevent race conditions const updatingItems = ref<Set<string>>(new Set()) // Track items being updated to prevent race conditions
// 计算属性 // 计算属性
const cartGroups = computed(() => { const cartGroups = computed<CartGroup[]>(() => {
const groups = new Map<string, any>() const groups = new Map<string, CartGroup>()
cartItems.value.forEach(item => {
// Build a unique key for the shop cartItems.value.forEach((item: LocalCartItem) => {
const shopKey = item.shopId || 'unknown' const shopKey = item.shopId
if (!groups.has(shopKey)) { if (!groups.has(shopKey)) {
groups.set(shopKey, { groups.set(shopKey, {
shopId: item.shopId, shopId: item.shopId,
shopName: item.shopName || '商城优选', // Better default name shopName: item.shopName,
merchantId: item.merchantId, // Add merchantId for navigation merchantId: item.merchantId,
items: [] as any[] items: []
}) })
} }
const group = groups.get(shopKey) const group = groups.get(shopKey)
if (group) { if (group != null) {
group.items.push(item) group.items.push(item)
} }
}) })
return Array.from(groups.values())
const groupArray: CartGroup[] = []
groups.forEach((value: CartGroup) => {
groupArray.push(value)
})
return groupArray
}) })
const allSelected = computed(() => { const allSelected = computed(() => {
return cartItems.value.length > 0 && cartItems.value.every(item => item.selected) return cartItems.value.length > 0 && cartItems.value.every((item: LocalCartItem) => item.selected)
}) })
const selectedCount = computed(() => { const selectedCount = computed(() => {
return cartItems.value.filter(item => item.selected).reduce((sum, item) => sum + item.quantity, 0) return cartItems.value.filter((item: LocalCartItem) => item.selected).reduce((sum: number, item: LocalCartItem) => sum + item.quantity, 0)
}) })
const totalPrice = computed(() => { const totalPrice = computed(() => {
return cartItems.value return cartItems.value
.filter(item => item.selected) .filter((item: LocalCartItem) => item.selected)
.reduce((sum, item) => sum + item.price * item.quantity, 0) .reduce((sum: number, item: LocalCartItem) => sum + item.price * item.quantity, 0)
.toFixed(2) .toFixed(2)
}) })
// 检查店铺是否全选 // 检查店铺是否全选
const isShopSelected = (shopId: string) => { const isShopSelected = (shopId: string): boolean => {
const group = cartGroups.value.find(g => g.shopId === shopId) const shopItems = cartItems.value.filter((item: LocalCartItem): boolean => item.shopId === shopId)
return group ? group.items.every(item => item.selected) : false return shopItems.length > 0 && shopItems.every((item: LocalCartItem): boolean => item.selected)
} }
const toggleManageMode = () => { const toggleManageMode = () => {
@@ -228,7 +266,7 @@ const toggleManageMode = () => {
// 初始化页面数据 // 初始化页面数据
const initPage = () => { const initPage = () => {
const systemInfo = uni.getSystemInfoSync() const systemInfo = uni.getSystemInfoSync()
statusBarHeight.value = systemInfo.statusBarHeight || 0 statusBarHeight.value = systemInfo.statusBarHeight ?? 0
} }
// 生命周期 // 生命周期
@@ -236,10 +274,6 @@ onMounted(() => {
initPage() initPage()
}) })
onShow(() => {
loadCartData()
})
// 加载数据 // 加载数据
const loadCartData = async () => { const loadCartData = async () => {
loading.value = true loading.value = true
@@ -249,26 +283,28 @@ const loadCartData = async () => {
const supabaseCartItems = await supabaseService.getCartItems() const supabaseCartItems = await supabaseService.getCartItems()
// 转换数据格式以匹配前端界面 // 转换数据格式以匹配前端界面
const transformedItems = supabaseCartItems.map((item: SupabaseCartItem) => { const transformedItems = supabaseCartItems.map((item: SupabaseCartItem): LocalCartItem => {
// 调试日志:打印每条商品数据的关键字段 // 调试日志:打印每条商品数据的关键字段
console.log(`CartItem raw: id=${item.id}, shop_id=${item.shop_id}, shop_name=${item.shop_name}, name=${item.product_name}, price=${item.product_price}`); console.log(`CartItem raw: id=${item.id}, shop_id=${item.shop_id}, shop_name=${item.shop_name}, name=${item.product_name}, price=${item.product_price}`);
// 关键修复确保shopId有值如果后端返回null/undefined使用'default_shop'作为分组键
const shopId = (item.shop_id != null && item.shop_id !== '') ? item.shop_id : 'default_shop'
const shopName = (item.shop_name != null && item.shop_name !== '') ? item.shop_name : '商城优选'
return { return {
id: item.id, id: item.id,
// 关键修复确保shopId有值如果后端返回null/undefined使用'default_shop'作为分组键 shopId: shopId,
shopId: (item.shop_id != null && item.shop_id !== '') ? item.shop_id : 'default_shop', shopName: shopName,
// 关键修复确保shopName有值 name: item.product_name ?? '未知商品',
shopName: (item.shop_name != null && item.shop_name !== '') ? item.shop_name : '商城优选',
name: item.product_name || '未知商品',
price: item.product_price != null ? item.product_price : 0, price: item.product_price != null ? item.product_price : 0,
image: item.product_image || '/static/images/default-product.png', image: item.product_image ?? '/static/images/default-product.png',
spec: item.product_specification || '标准规格', spec: item.product_specification ?? '标准规格',
quantity: item.quantity || 1, quantity: item.quantity ?? 1,
selected: item.selected || false, selected: item.selected ?? false,
productId: item.product_id, productId: item.product_id ?? '',
skuId: item.sku_id, skuId: item.sku_id ?? '',
merchantId: item.merchant_id merchantId: item.merchant_id ?? ''
} } as LocalCartItem
}) })
console.log('Transformed items count:', transformedItems.length); console.log('Transformed items count:', transformedItems.length);
@@ -284,18 +320,17 @@ const loadCartData = async () => {
} }
if (recommends.length > 0) { if (recommends.length > 0) {
recommendProducts.value = recommends.map((p: Product) => { recommendProducts.value = recommends.map((p: Product): RecommendProduct => {
return { return {
id: p.id, id: p.id,
shopId: p.merchant_id || 'unknown', shopId: p.merchant_id ?? 'unknown',
shopName: p.shop_name || '商城推荐', shopName: p.shop_name ?? '商城推荐',
name: p.name, name: p.name,
price: p.base_price, price: p.base_price,
image: p.main_image_url || '/static/images/default-product.png', image: p.main_image_url ?? '/static/images/default-product.png',
specification: '', // 推荐列表不显示详细规格 skuId: ''
specDetails: {} }
} })
})
} else { } else {
recommendProducts.value = [] recommendProducts.value = []
} }
@@ -307,6 +342,10 @@ const loadCartData = async () => {
} }
} }
onShow(() => {
loadCartData()
})
// 商品操作 - 更新选中状态到Supabase // 商品操作 - 更新选中状态到Supabase
const toggleSelect = async (itemId: string) => { const toggleSelect = async (itemId: string) => {
// 乐观更新 // 乐观更新
@@ -329,18 +368,14 @@ const toggleSelect = async (itemId: string) => {
} }
const toggleShopSelect = async (shopId: string) => { const toggleShopSelect = async (shopId: string) => {
// 查找该组是否已存在,并判断目标状态 // 获取该店铺下所有商品的ID
const group = cartGroups.value.find((g: any) => g.shopId === shopId) const shopItems = cartItems.value.filter((item: LocalCartItem): boolean => item.shopId === shopId)
if (!group) return if (shopItems.length === 0) return
// 检查当前是否全选: 如果所有都选中,则目标是全不选(false);否则全选(true) const isAllShopSelected = shopItems.every((item: LocalCartItem): boolean => item.selected)
const isAllShopSelected = (group.items as any[]).every((item: any) => item.selected)
const newState = !isAllShopSelected const newState = !isAllShopSelected
// 获取该店铺下所有商品的ID const shopItemIds = shopItems.map((item: LocalCartItem): string => item.id)
const shopItemIds = cartItems.value
.filter(item => item.shopId === shopId)
.map(item => item.id)
// 乐观更新本地状态 // 乐观更新本地状态
const oldStates = new Map<string, boolean>() const oldStates = new Map<string, boolean>()
@@ -376,11 +411,11 @@ const toggleSelectAll = async () => {
const newSelectedState = !allSelected.value const newSelectedState = !allSelected.value
// 乐观更新 // 乐观更新
const oldItems = JSON.parse(JSON.stringify(cartItems.value)) const oldItems = JSON.parse(JSON.stringify(cartItems.value)) as LocalCartItem[]
const selectedItems = cartItems.value.map(item => ({ const selectedItems = cartItems.value.map((item): LocalCartItem => {
...item, item.selected = newSelectedState
selected: newSelectedState return item
})) })
cartItems.value = selectedItems cartItems.value = selectedItems
// 更新到Supabase // 更新到Supabase
@@ -391,7 +426,7 @@ const toggleSelectAll = async () => {
if (!success) { if (!success) {
console.error('批量更新选中状态失败') console.error('批量更新选中状态失败')
cartItems.value = oldItems cartItems.value = oldItems
uni.showToast({ uni.showToast({
title: '操作失败', title: '操作失败',
icon: 'none' icon: 'none'
@@ -450,24 +485,25 @@ const decreaseQuantity = async (itemId: string) => {
uni.showModal({ uni.showModal({
title: '提示', title: '提示',
content: '确定要从购物车移除该商品吗?', content: '确定要从购物车移除该商品吗?',
success: async (res) => { success: (res) => {
if (res.confirm) { if (res.confirm) {
// 从Supabase删除 // 从Supabase删除
const success = await supabaseService.deleteCartItem(itemId) supabaseService.deleteCartItem(itemId).then((success) => {
if (success) { if (success) {
cartItems.value.splice(index, 1) cartItems.value.splice(index, 1)
cartItems.value = [...cartItems.value] cartItems.value = [...cartItems.value]
uni.showToast({ uni.showToast({
title: '已移除', title: '已移除',
icon: 'none' icon: 'none'
}) })
} else { } else {
console.error('删除商品失败') console.error('删除商品失败')
uni.showToast({ uni.showToast({
title: '删除失败', title: '删除失败',
icon: 'none' icon: 'none'
}) })
} }
})
} }
} }
}) })
@@ -488,7 +524,7 @@ const deleteSelectedItems = async () => {
uni.showModal({ uni.showModal({
title: '提示', title: '提示',
content: `确定要删除选中的 ${selectedCount.value} 件商品吗?`, content: `确定要删除选中的 ${selectedCount.value} 件商品吗?`,
success: async (res) => { success: (res) => {
if (res.confirm) { if (res.confirm) {
// 获取选中的商品ID // 获取选中的商品ID
const selectedItemIds = cartItems.value const selectedItemIds = cartItems.value
@@ -496,27 +532,27 @@ const deleteSelectedItems = async () => {
.map(item => item.id) .map(item => item.id)
// 批量删除到Supabase // 批量删除到Supabase
const success = await supabaseService.batchDeleteCartItems(selectedItemIds) supabaseService.batchDeleteCartItems(selectedItemIds).then((success) => {
if (success) {
if (success) { // 从本地列表移除
// 从本地列表移除 cartItems.value = cartItems.value.filter(item => !item.selected)
cartItems.value = cartItems.value.filter(item => !item.selected)
// 如果购物车删空了,退出管理模式
// 如果购物车删空了,退出管理模式 if (cartItems.value.length === 0) {
if (cartItems.value.length === 0) { isManageMode.value = false
isManageMode.value = false }
uni.showToast({
title: '删除成功',
icon: 'success'
})
} else {
console.error('批量删除商品失败')
uni.showToast({
title: '删除失败',
icon: 'none'
})
} }
uni.showToast({ })
title: '删除成功',
icon: 'success'
})
} else {
console.error('批量删除商品失败')
uni.showToast({
title: '删除失败',
icon: 'none'
})
}
} }
} }
}) })
@@ -525,7 +561,11 @@ const deleteSelectedItems = async () => {
const addToCart = async (product: any) => { const addToCart = async (product: any) => {
try { try {
// 调用SupabaseService添加商品到购物车 // 调用SupabaseService添加商品到购物车
const success = await supabaseService.addToCart(product.id, 1, product.skuId) // 显式访问属性避免any类型导致的编译错误
const target = product as UTSJSONObject
const productId = target.getString('id') ?? ''
const skuId = target.getString('skuId') ?? ''
const success = await supabaseService.addToCart(productId, 1, skuId)
if (success) { if (success) {
uni.showToast({ uni.showToast({
title: '已添加到购物车', title: '已添加到购物车',
@@ -553,13 +593,16 @@ const addToCart = async (product: any) => {
// 导航函数 // 导航函数
const navigateToShop = (shopId: string, merchantId: any) => { const navigateToShop = (shopId: string, merchantId: any) => {
// Prevent navigation for invalid shops // Prevent navigation for invalid shops
if (!shopId || shopId === 'default_shop' || shopId === 'unknown') return if (shopId == '' || shopId === 'default_shop' || shopId === 'unknown') return
let url = `/pages/mall/consumer/shop-detail?id=${shopId}` let url = `/pages/mall/consumer/shop-detail?id=${shopId}`
if (merchantId) { if (merchantId != null) {
url += `&merchantId=${merchantId}` const mId = `${merchantId}`
} if (mId !== '' && mId !== 'null' && mId !== 'undefined' && mId !== 'false') {
uni.navigateTo({ url }) url += `&merchantId=${mId}`
}
}
uni.navigateTo({ url })
} }
const goShopping = () => { const goShopping = () => {
@@ -567,20 +610,50 @@ const goShopping = () => {
} }
const navigateToProduct = (product: any) => { const navigateToProduct = (product: any) => {
console.log('navigateToProduct', product)
// 使用 JSON 转换确保可以作为 JSONObject 处理,兼容 LocalCartItem 类型和普通对象
const productJson = JSON.parse(JSON.stringify(product)) as UTSJSONObject
// 使用productId如果存在作为跳转的商品ID否则使用id // 使用productId如果存在作为跳转的商品ID否则使用id
const productId = product.productId || product.id let productId = productJson.getString('productId')
if (productId == null || productId == '') {
productId = productJson.getString('id')
}
if (productId == null || productId == '') {
console.error('无法获取商品ID', product)
return
}
// 传递完整的参数,确保商品详情页能正确加载 // 传递完整的参数,确保商品详情页能正确加载
const params = new URLSearchParams() let paramsArr: string[] = []
params.append('id', productId) paramsArr.push('id=' + encodeURIComponent(productId))
params.append('productId', productId) paramsArr.push('productId=' + encodeURIComponent(productId))
params.append('price', product.price?.toString() || '0')
// 商品详情页期望的参数名是originalPrice const price = productJson.getNumber('price') ?? 0
params.append('originalPrice', (product.original_price || product.originalPrice || (product.price * 1.2).toFixed(2))?.toString()) paramsArr.push('price=' + price)
params.append('name', encodeURIComponent(product.name || ''))
params.append('image', encodeURIComponent(product.image || '/static/product1.jpg')) let originalPrice = productJson.getNumber('original_price')
if (originalPrice == null) {
originalPrice = productJson.getNumber('originalPrice')
}
if (originalPrice == null) {
originalPrice = parseFloat((price * 1.2).toFixed(2))
}
paramsArr.push('originalPrice=' + originalPrice)
const name = productJson.getString('name') ?? ''
paramsArr.push('name=' + encodeURIComponent(name))
const image = productJson.getString('image') ?? '/static/product1.jpg'
paramsArr.push('image=' + encodeURIComponent(image))
const url = `/pages/mall/consumer/product-detail?${paramsArr.join('&')}`
console.log('Navigate to:', url)
uni.navigateTo({ uni.navigateTo({
url: `/pages/mall/consumer/product-detail?${params.toString()}` url: url
}) })
} }
@@ -598,16 +671,16 @@ const goToCheckout = () => {
.filter(item => item.selected) .filter(item => item.selected)
.map(item => ({ .map(item => ({
id: item.id, id: item.id,
product_id: item.productId || item.id, product_id: item.productId ?? item.id,
sku_id: item.skuId || item.id, sku_id: item.skuId ?? item.id,
product_name: item.name, product_name: item.name,
shop_id: item.shopId, // 关键保留shopId用于分组 shop_id: item.shopId, // 关键保留shopId用于分组
shop_name: item.shopName, // 关键保留shopName shop_name: item.shopName, // 关键保留shopName
merchant_id: item.merchantId, merchant_id: item.merchantId,
product_image: item.image, product_image: item.image,
sku_specifications: item.spec, sku_specifications: item.spec,
price: Number(item.price), // 确保是数字 price: item.price, // 确保是数字
quantity: Number(item.quantity) // 确保是数字 quantity: item.quantity // 确保是数字
})) }))
// 关键修复:将结算数据写入 Storage确保 checkout 页面能稳定获取 // 关键修复:将结算数据写入 Storage确保 checkout 页面能稳定获取
@@ -623,13 +696,7 @@ const goToCheckout = () => {
// 跳转到结算页面并传递数据 // 跳转到结算页面并传递数据
uni.navigateTo({ uni.navigateTo({
url: '/pages/mall/consumer/checkout', url: '/pages/mall/consumer/checkout'
success: (res) => {
// 通过eventChannel传递数据 (作为备份)
res.eventChannel.emit('acceptData', {
selectedItems: selectedItems
})
}
}) })
} }
</script> </script>
@@ -637,7 +704,7 @@ const goToCheckout = () => {
<style> <style>
.cart-page { .cart-page {
width: 100%; width: 100%;
height: 100vh; height: 100%;
background-color: #f5f5f5; background-color: #f5f5f5;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -690,7 +757,7 @@ const goToCheckout = () => {
background: rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.2);
padding: 4px 12px; padding: 4px 12px;
border-radius: 20px; border-radius: 20px;
cursor: pointer; /* cursor: pointer; REMOVED */
transition: all 0.2s ease; transition: all 0.2s ease;
} }
@@ -707,7 +774,7 @@ const goToCheckout = () => {
.action-text { .action-text {
font-size: 12px; font-size: 12px;
color: white; color: white;
font-weight: 500; font-weight: bold;
} }
/* 导航栏占位符 */ /* 导航栏占位符 */
@@ -802,7 +869,7 @@ const goToCheckout = () => {
.shop-name { .shop-name {
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 700;
color: #333; color: #333;
margin-right: 4px; margin-right: 4px;
/* 自适应宽度,但不超过剩余空间 */ /* 自适应宽度,但不超过剩余空间 */
@@ -844,7 +911,7 @@ const goToCheckout = () => {
height: 18px; height: 18px;
background-color: #ff5000; background-color: #ff5000;
color: white; color: white;
border-radius: 50%; border-radius: 9px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -855,7 +922,7 @@ const goToCheckout = () => {
width: 18px; width: 18px;
height: 18px; height: 18px;
border: 1px solid #ddd; border: 1px solid #ddd;
border-radius: 50%; border-radius: 9px;
} }
.item-image { .item-image {
@@ -884,11 +951,12 @@ const goToCheckout = () => {
font-size: 14px; /* 稍微减小字体 */ font-size: 14px; /* 稍微减小字体 */
color: #333; color: #333;
margin-bottom: 2px; margin-bottom: 2px;
display: -webkit-box; /* display: -webkit-box; REMOVED */
-webkit-line-clamp: 1; /* 单行显示 */ /* -webkit-line-clamp: 1; REMOVED */
-webkit-box-orient: vertical; /* -webkit-box-orient: vertical; REMOVED */
overflow: hidden; overflow: hidden;
font-weight: 500; font-weight: bold;
text-overflow: ellipsis;
} }
.item-spec { .item-spec {
@@ -957,9 +1025,12 @@ const goToCheckout = () => {
} }
.recommend-list { .recommend-list {
display: grid; display: flex;
grid-template-columns: repeat(2, 1fr); /* 手机端默认双列 */ flex-direction: row;
gap: 12px; flex-wrap: wrap;
justify-content: space-between;
/* grid-template-columns: repeat(2, 1fr); REMOVED */
/* gap: 12px; REMOVED */
} }
.recommend-item { .recommend-item {
@@ -968,13 +1039,15 @@ const goToCheckout = () => {
background: #fff; background: #fff;
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
width: 48%; /* 替换 grid 1fr auto fit */
margin-bottom: 12px;
} }
.recommend-image { .recommend-image {
width: 100%; width: 100%;
aspect-ratio: 1; /* 保持正方形比例 */ height: 170px; /* 显式高度 */
height: auto; /* 自动高度 */ /* aspect-ratio: 1; REMOVED */
object-fit: cover; /* object-fit: cover; REMOVED */
border-radius: 8px; border-radius: 8px;
margin-bottom: 8px; margin-bottom: 8px;
background: #f5f5f5; background: #f5f5f5;
@@ -987,9 +1060,10 @@ const goToCheckout = () => {
line-height: 1.4; line-height: 1.4;
height: 36px; height: 36px;
overflow: hidden; overflow: hidden;
display: -webkit-box; /* display: -webkit-box; REMOVED */
-webkit-line-clamp: 2; /* -webkit-line-clamp: 2; REMOVED */
-webkit-box-orient: vertical; /* -webkit-box-orient: vertical; REMOVED */
text-overflow: ellipsis;
} }
.recommend-bottom { .recommend-bottom {
@@ -1010,7 +1084,7 @@ const goToCheckout = () => {
width: 24px; width: 24px;
height: 24px; height: 24px;
background-color: #ff5000; background-color: #ff5000;
border-radius: 50%; border-radius: 12px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -1028,12 +1102,17 @@ const goToCheckout = () => {
.cart-list, .cart-list,
.recommend-section { .recommend-section {
margin: 20px auto; margin: 20px auto;
max-width: 95%; width: 95%; /* max-width -> width */
} }
.recommend-list { .recommend-list {
grid-template-columns: repeat(4, 1fr); /* 平板显示4列 */ /* grid-template-columns: repeat(4, 1fr); REMOVED */
gap: 16px; /* gap: 16px; REMOVED */
/* Flex 布局参数调整在下方 update */
}
.recommend-item {
width: 23%;
margin-bottom: 16px;
} }
} }
@@ -1047,12 +1126,14 @@ const goToCheckout = () => {
.cart-list, .cart-list,
.recommend-section { .recommend-section {
margin: 20px auto; margin: 20px auto;
max-width: 1200px; /* 限制最大宽度 */ width: 96%; /* max-width -> width: percentage is safer */
max-width: 1200px;
} }
/* 店铺分组在桌面端显示为网格布局 */ /* 店铺分组在桌面端显示为网格布局 */
.shop-group { .shop-group {
display: block; display: flex;
flex-direction: column;
background: transparent; background: transparent;
box-shadow: none; box-shadow: none;
border-radius: 0; border-radius: 0;
@@ -1063,14 +1144,14 @@ const goToCheckout = () => {
background: white; background: white;
border-radius: 12px; border-radius: 12px;
margin-bottom: 12px; margin-bottom: 12px;
padding: 16px 24px; padding: 16px 80px 16px 24px; /* 同步增加右侧内边距 */
} }
/* 购物车商品列表转为列表布局 */ /* 购物车商品列表转为列表布局 */
.cart-item { .cart-item {
background: white; background: white;
border-radius: 0; border-radius: 0;
padding: 15px 30px; padding: 15px 80px 15px 30px; /* 进一步增加右侧内边距 */
height: 80px; /* 固定高度 */ height: 80px; /* 固定高度 */
border-bottom: 1px solid #eee; border-bottom: 1px solid #eee;
box-shadow: none; box-shadow: none;
@@ -1137,14 +1218,15 @@ const goToCheckout = () => {
flex-direction: row; flex-direction: row;
justify-content: flex-end; justify-content: flex-end;
align-items: center; align-items: center;
gap: 40px; /* gap: 40px; REMOVED */
height: 100%; height: 100%;
} }
.item-price { .item-price {
width: 100px; width: 100px;
text-align: right; text-align: right;
margin-bottom: 0; margin-bottom: 0;
margin-right: 40px; /* Replace gap */
} }
.quantity-control { .quantity-control {
@@ -1155,8 +1237,12 @@ const goToCheckout = () => {
/* 推荐商品优化 */ /* 推荐商品优化 */
.recommend-list { .recommend-list {
grid-template-columns: repeat(5, 1fr); /* 桌面端显示5列 */ /* grid-template-columns: repeat(5, 1fr); REMOVED */
gap: 20px; /* gap: 20px; REMOVED */
}
.recommend-item {
width: 18%; /* 5列 */
margin-bottom: 20px;
} }
.recommend-image { .recommend-image {
@@ -1166,7 +1252,7 @@ const goToCheckout = () => {
/* 底部结算栏优化 */ /* 底部结算栏优化 */
.cart-footer { .cart-footer {
padding: 0 40px; padding: 0 40px;
max-width: 100%; width: 100%; /* max-width -> width */
margin: 0 auto; margin: 0 auto;
box-shadow: 0 -2px 10px rgba(0,0,0,0.05); box-shadow: 0 -2px 10px rgba(0,0,0,0.05);
} }
@@ -1184,7 +1270,7 @@ const goToCheckout = () => {
@media screen and (min-width: 1400px) { @media screen and (min-width: 1400px) {
.cart-list, .cart-list,
.recommend-section { .recommend-section {
max-width: 1400px; width: 1400px;
} }
/* 大屏下购物车商品显示3列 - 移除,保持单列列表 */ /* 大屏下购物车商品显示3列 - 移除,保持单列列表 */
@@ -1193,11 +1279,14 @@ const goToCheckout = () => {
} */ } */
.recommend-list { .recommend-list {
grid-template-columns: repeat(6, 1fr); /* 大屏幕显示6列 */ /* grid-template-columns: repeat(6, 1fr); REMOVED */
}
.recommend-item {
width: 15%; /* 6 columns approx */
} }
.footer-content { .footer-content {
max-width: 1400px; width: 1400px;
} }
} }
@@ -1216,6 +1305,7 @@ const goToCheckout = () => {
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
width: 100%; width: 100%;
/* gap: 20px; REMOVED from .action-bar-content usually in desktop */
} }
.action-left { .action-left {
@@ -1324,12 +1414,12 @@ const goToCheckout = () => {
@media screen and (min-width: 768px) { @media screen and (min-width: 768px) {
.cart-action-bar { .cart-action-bar {
margin: 20px auto; margin: 20px auto;
max-width: 95%; width: 95%; /* max-width -> width */
padding: 20px; padding: 20px;
} }
.action-bar-content { .action-bar-content {
gap: 20px; /* gap: 20px; REMOVED */
} }
.total-price { .total-price {
@@ -1339,13 +1429,14 @@ const goToCheckout = () => {
.checkout-btn, .delete-btn { .checkout-btn, .delete-btn {
padding: 10px 30px; padding: 10px 30px;
font-size: 16px; font-size: 16px;
margin-left: 20px; /* Replace gap */
} }
} }
/* 桌面端优化 */ /* 桌面端优化 */
@media screen and (min-width: 1024px) { @media screen and (min-width: 1024px) {
.cart-action-bar { .cart-action-bar {
max-width: 1200px; width: 1200px; /* max-width -> width */
padding: 20px 30px; padding: 20px 30px;
} }
@@ -1378,7 +1469,7 @@ const goToCheckout = () => {
/* 大屏幕优化 */ /* 大屏幕优化 */
@media screen and (min-width: 1400px) { @media screen and (min-width: 1400px) {
.cart-action-bar { .cart-action-bar {
max-width: 1400px; width: 1400px; /* max-width -> width */
} }
} }
</style> </style>

View File

@@ -802,7 +802,7 @@ const goToCheckout = () => {
.shop-name { .shop-name {
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 700;
color: #333; color: #333;
margin-right: 4px; margin-right: 4px;
/* 自适应宽度,但不超过剩余空间 */ /* 自适应宽度,但不超过剩余空间 */

View File

@@ -25,14 +25,11 @@
</view> </view>
</view> </view>
<!-- 导航栏占位 -->
<view class="navbar-placeholder" :style="{ height: (statusBarHeight + 44) + 'px' }"></view>
<!-- 分类内容区 --> <!-- 分类内容区 -->
<view <view class="category-content">
class="category-content"
:style="{
marginTop: (statusBarHeight + headerHeight + 10) + 'px',
height: `calc(100vh - ${statusBarHeight + headerHeight + 10}px)`
}"
>
<!-- 左侧一级分类 --> <!-- 左侧一级分类 -->
<scroll-view scroll-y class="primary-category"> <scroll-view scroll-y class="primary-category">
<view <view
@@ -88,7 +85,7 @@
</view> </view>
<view class="product-meta"> <view class="product-meta">
<text class="manufacturer">{{ product.brand_name || product.shop_name || '自营' }}</text> <text class="manufacturer">{{ product.brand_name ?? product.shop_name ?? '自营' }}</text>
<view class="sales-info"> <view class="sales-info">
<text class="sales-count">已售{{ product.sale_count }}</text> <text class="sales-count">已售{{ product.sale_count }}</text>
</view> </view>
@@ -116,12 +113,20 @@
<script setup lang="uts"> <script setup lang="uts">
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import supabaseService from '@/utils/supabaseService.uts' import supabaseService from '@/utils/supabaseService.uts'
import type { Category, Product } from '@/utils/supabaseService.uts' import type { Product } from '@/utils/supabaseService.uts'
type LocalCategory = {
id: string
name: string
icon: string
description: string
color: string
}
// 响应式数据 // 响应式数据
const statusBarHeight = ref(0) const statusBarHeight = ref(0)
const headerHeight = ref(44) // 默认头部高度 const headerHeight = ref(44) // 默认头部高度
const primaryCategories = ref<Category[]>([]) const primaryCategories = ref<LocalCategory[]>([])
const productList = ref<Product[]>([]) const productList = ref<Product[]>([])
const activePrimary = ref<string>('') const activePrimary = ref<string>('')
const cartCount = ref(3) const cartCount = ref(3)
@@ -137,67 +142,10 @@ const currentCategoryDesc = ref('')
// 页面参数 // 页面参数
const pageParams = ref<any>({}) const pageParams = ref<any>({})
// 生命周期
onMounted(async() => {
await loadCategories()
// 等待分类加载完成后,再检查是否需要加载默认分类的商品
// 延迟一点时间,确保页面参数处理完成
setTimeout(async () => {
if (!hasLoadedFromParams.value && activePrimary.value) {
await loadProducts()
}
}, 300)
})
// 添加加载分类的方法
const loadCategories = async () => {
try {
const categoriesData = await supabaseService.getCategories()
console.log('加载分类数据成功,数量:', categoriesData.length)
// 映射数据并添加默认颜色,防止选中时背景透明导致文字看不清
const categories = categoriesData.map((cat: any) => ({
id: cat.id,
name: cat.name,
icon: cat.icon_url || '📦',
desc: cat.description || '',
description: cat.description || '', // 兼容不同字段名
color: cat.color || '#4CAF50' // 默认绿色如果有color字段则使用
})) as Category[]
if (categories.length > 0) {
primaryCategories.value = categories
// 如果没有通过参数设置分类,则设置默认选中第一个分类
if (!activePrimary.value && categories[0]) {
activePrimary.value = categories[0].id
console.log('设置默认分类为:', categories[0].name, 'ID:', categories[0].id)
currentCategoryName.value = categories[0].name
currentCategoryDesc.value = categories[0].description || ''
} else if (activePrimary.value) {
// 如果已经选中了分类可能来自Storage更新显示信息
const current = categories.find(c => c.id == activePrimary.value)
if (current) {
currentCategoryName.value = current.name
currentCategoryDesc.value = current.description || ''
// 如果此时没有商品列表(且没有正在加载),可能需要加载
if (productList.value.length === 0 && !loading.value) {
loadProducts()
}
}
}
} else {
console.warn('从Supabase获取的分类数据为空')
}
} catch (error) {
console.error('加载分类数据失败:', error)
}
}
// 加载商品数据 // 加载商品数据
const loadProducts = async () => { async function loadProducts(): Promise<void> {
if (loading.value) return if (loading.value) return
if (!activePrimary.value) { if (activePrimary.value == '') {
console.warn('activePrimary为空无法加载商品') console.warn('activePrimary为空无法加载商品')
return return
} }
@@ -222,10 +170,10 @@ const loadProducts = async () => {
hasMore.value = response.hasmore hasMore.value = response.hasmore
// 更新当前分类信息 // 更新当前分类信息
const category = primaryCategories.value.find(cat => cat.id === activePrimary.value) const category = primaryCategories.value.find((cat: LocalCategory): boolean => cat.id === activePrimary.value)
if (category) { if (category != null) {
currentCategoryName.value = category.name currentCategoryName.value = category.name
currentCategoryDesc.value = category.description || '' currentCategoryDesc.value = category.description
} }
console.log('商品列表加载完成,当前总数量:', productList.value.length) console.log('商品列表加载完成,当前总数量:', productList.value.length)
@@ -239,175 +187,82 @@ const loadProducts = async () => {
} }
} }
async function loadCategories(): Promise<void> {
try {
const categoriesData = await supabaseService.getCategories()
console.log('加载分类数据成功,数量:', categoriesData.length)
// 映射数据并添加默认颜色,防止选中时背景透明导致文字看不清
// 过滤掉医药健康相关分类
const categories: LocalCategory[] = []
const rawList = categoriesData as any[]
for (let i = 0; i < rawList.length; i++) {
const raw = rawList[i]
const catObj = (raw instanceof UTSJSONObject) ? (raw as UTSJSONObject) : (JSON.parse(JSON.stringify(raw)) as UTSJSONObject)
const name = catObj.getString('name') ?? ''
if (name.includes('医药') || name.includes('健康')) {
continue
}
const id = catObj.getString('id') ?? ''
const description = catObj.getString('description') ?? ''
const icon = catObj.getString('icon') ?? catObj.getString('icon_url') ?? '📦'
const color = catObj.getString('color') ?? '#4CAF50'
categories.push({
id,
name,
icon,
description,
color
})
}
if (categories.length > 0) {
primaryCategories.value = categories
// 如果没有通过参数设置分类,则设置默认选中一个分类
if (activePrimary.value == '') {
// 优先查找"厨具"相关的分类作为默认
const defaultCategory = categories.find((c: LocalCategory): boolean => c.name.includes('厨具')) ?? categories[0]
activePrimary.value = defaultCategory.id
console.log('设置默认分类为:', defaultCategory.name, 'ID:', defaultCategory.id)
currentCategoryName.value = defaultCategory.name
currentCategoryDesc.value = defaultCategory.description
} else {
// 如果已经选中了分类可能来自Storage更新显示信息
const current = categories.find((c: LocalCategory): boolean => c.id == activePrimary.value)
if (current != null) {
currentCategoryName.value = current.name
currentCategoryDesc.value = current.description
// 如果此时没有商品列表(且没有正在加载),可能需要加载
if (productList.value.length === 0 && !loading.value) {
loadProducts()
}
}
}
} else {
console.warn('从Supabase获取的分类数据为空')
}
} catch (error) {
console.error('加载分类数据失败:', error)
}
}
// 加载更多 // 加载更多
const loadMore = () => { function loadMore(): void {
if (hasMore.value && !loading.value) { if (hasMore.value && !loading.value) {
currentPage.value++ currentPage.value++
loadProducts() loadProducts()
} }
} }
// 页面加载时处理参数 - 这是处理分类切换的主要入口
onLoad((options: any) => {
console.log('=== category页面onLoad被调用 ===')
console.log('页面加载时间:', Date.now())
console.log('传入的options参数:', options)
console.log('当前活动分类:', activePrimary.value)
let categoryId = ''
let categoryName = ''
// 首先检查传入的options参数
if (options && options.categoryId) {
categoryId = options.categoryId
categoryName = options.name || ''
console.log('✅ onLoad中找到分类参数:', categoryId, categoryName)
}
// 如果options中没有尝试从getCurrentPages()获取
if (!categoryId) {
const pages = getCurrentPages()
if (pages.length > 0) {
const currentPage = pages[pages.length - 1]
const pageOptions = currentPage.options || {}
console.log('从getCurrentPages()获取参数:', pageOptions)
if (pageOptions.categoryId) {
categoryId = pageOptions.categoryId
categoryName = pageOptions.name || ''
console.log('✅ 从getCurrentPages()找到分类参数:', categoryId, categoryName)
}
}
}
// 如果有找到分类ID则选中对应的分类
if (categoryId) {
hasLoadedFromParams.value = true
console.log('✅ 准备选中分类:', categoryId)
console.log('分类名称:', categoryName || '未指定')
// 检查是否需要更新分类
if (activePrimary.value !== categoryId) {
console.log('当前分类:', activePrimary.value, '与目标分类:', categoryId, '不同,需要更新')
console.log('准备调用selectPrimaryCategory函数...')
selectPrimaryCategory(categoryId)
} else {
console.log('当前分类已经是目标分类,但可能用户想要刷新页面')
console.log('当前分类:', activePrimary.value, '目标分类:', categoryId)
// 即使分类相同,也重新加载数据,确保数据是最新的
// 添加一个小的延迟,确保页面完全显示后再更新数据
setTimeout(() => {
selectPrimaryCategory(categoryId)
}, 100)
}
} else {
console.log('⚠️ onLoad中未找到分类参数将使用从数据库加载的第一个分类')
// 不再使用硬编码的默认分类loadCategories 会设置第一个分类
}
console.log('=== category页面onLoad执行完成 ===')
})
// 页面显示时也检查参数,确保从其他页面返回时能正确显示
onShow(() => {
console.log('=== category页面onShow被调用 ===')
console.log('页面显示时间:', Date.now())
console.log('当前活动分类:', activePrimary.value)
// 1. 优先检查 Storage 中的参数 (由首页传入)
const storageCategoryId = uni.getStorageSync('selectedCategory')
if (storageCategoryId) {
console.log('✅ onShow中找到Storage分类参数:', storageCategoryId)
hasLoadedFromParams.value = true
// 清除Storage防止下次误读
uni.removeStorageSync('selectedCategory')
if (activePrimary.value !== storageCategoryId) {
selectPrimaryCategory(storageCategoryId)
}
// 如果分类还没加载完这里设置了ID等loadCategories完成后会自动匹配信息
return
}
// 在onShow中我们也需要检查是否有新的参数
// 因为当从主页再次点击分类跳转过来时可能不会触发onLoad
// 而是触发onShow
// 获取当前页面实例和参数
const pages = getCurrentPages()
if (pages.length > 0) {
const currentPage = pages[pages.length - 1]
const pageOptions = currentPage.options || {}
console.log('onShow中获取参数:', pageOptions)
// 检查是否有分类参数
if (pageOptions.categoryId) {
hasLoadedFromParams.value = true
const categoryId = pageOptions.categoryId
const categoryName = pageOptions.name || ''
console.log('✅ onShow中找到分类参数:', categoryId, categoryName)
console.log('URL中的时间戳参数:', pageOptions.timestamp)
console.log('URL中的随机参数:', pageOptions.random)
// 检查是否需要更新分类
if (activePrimary.value !== categoryId) {
console.log('当前分类:', activePrimary.value, '与目标分类:', categoryId, '不同,需要更新')
console.log('准备调用selectPrimaryCategory函数...')
selectPrimaryCategory(categoryId)
} else {
console.log('当前分类已经是目标分类,但可能用户想要刷新页面')
console.log('当前分类:', activePrimary.value, '目标分类:', categoryId)
// 即使分类相同,也重新加载数据,确保数据是最新的
// 添加一个小的延迟,确保页面完全显示后再更新数据
setTimeout(() => {
selectPrimaryCategory(categoryId)
}, 100)
}
} else {
console.log('⚠️ onShow中未找到分类参数')
console.log('尝试从URL中解析参数...')
// 尝试从当前页面的URL中解析参数
const currentUrl = currentPage.route || ''
console.log('当前页面路由:', currentUrl)
// 如果URL中有查询参数尝试解析
if (currentPage.$page && currentPage.$page.fullPath) {
const fullPath = currentPage.$page.fullPath
console.log('完整路径:', fullPath)
// 尝试解析查询参数
const queryIndex = fullPath.indexOf('?')
if (queryIndex > -1) {
const queryString = fullPath.substring(queryIndex + 1)
console.log('查询字符串:', queryString)
// 简单解析查询参数
const params = new URLSearchParams(queryString)
const urlCategoryId = params.get('categoryId')
if (urlCategoryId) {
hasLoadedFromParams.value = true
console.log('✅ 从URL解析到分类参数:', urlCategoryId)
selectPrimaryCategory(urlCategoryId)
}
}
}
}
}
console.log('=== category页面onShow执行完成 ===')
})
// 选择一级分类 // 选择一级分类
const selectPrimaryCategory = async (categoryId: string) => { async function selectPrimaryCategory(categoryId: string): Promise<void> {
console.log('=== selectPrimaryCategory函数开始执行 ===') console.log('=== selectPrimaryCategory函数开始执行 ===')
console.log('传入的categoryId:', categoryId) console.log('传入的categoryId:', categoryId)
console.log('当前时间:', Date.now()) console.log('当前时间:', Date.now())
// 验证categoryId是否有效 // 验证categoryId是否有效
if (!categoryId) { if (categoryId == '') {
console.error('categoryId为空尝试使用第一个分类') console.error('categoryId为空尝试使用第一个分类')
if (primaryCategories.value.length > 0) { if (primaryCategories.value.length > 0) {
categoryId = primaryCategories.value[0].id categoryId = primaryCategories.value[0].id
@@ -425,8 +280,8 @@ const selectPrimaryCategory = async (categoryId: string) => {
console.log('更新后的activePrimary:', activePrimary.value) console.log('更新后的activePrimary:', activePrimary.value)
// 更新当前分类信息 // 更新当前分类信息
const category = primaryCategories.value.find(cat => cat.id === categoryId) const category = primaryCategories.value.find((cat: LocalCategory): boolean => cat.id === categoryId)
if (category) { if (category != null) {
currentCategoryName.value = category.name currentCategoryName.value = category.name
currentCategoryDesc.value = category.description currentCategoryDesc.value = category.description
console.log('✅ 找到分类:', category.name, '描述:', category.description) console.log('✅ 找到分类:', category.name, '描述:', category.description)
@@ -465,11 +320,159 @@ const selectPrimaryCategory = async (categoryId: string) => {
console.log('=== selectPrimaryCategory函数执行完成 ===') console.log('=== selectPrimaryCategory函数执行完成 ===')
} }
// 生命周期
onMounted(() => {
loadCategories().then(() => {
setTimeout(() => {
if (!hasLoadedFromParams.value && activePrimary.value != '') {
loadProducts()
}
}, 300)
})
})
// 页面加载时处理参数 - 这是处理分类切换的主要入口
onLoad((options: any) => {
console.log('=== category页面onLoad被调用 ===')
console.log('页面加载时间:', Date.now())
console.log('传入的options参数:', options)
console.log('当前活动分类:', activePrimary.value)
let categoryId = ''
let categoryName = ''
// 首先检查传入的options参数
const optObj = (options instanceof UTSJSONObject) ? (options as UTSJSONObject) : (JSON.parse(JSON.stringify(options ?? {})) as UTSJSONObject)
const optCategoryId = optObj.getString('categoryId') ?? ''
if (optCategoryId !== '') {
categoryId = optCategoryId
categoryName = optObj.getString('name') ?? ''
console.log('✅ onLoad中找到分类参数:', categoryId, categoryName)
}
// 如果options中没有尝试从getCurrentPages()获取
if (categoryId == '') {
const pages = getCurrentPages()
if (pages.length > 0) {
const currentPage = pages[pages.length - 1]
const rawPageOptions = currentPage.options ?? {}
console.log('从getCurrentPages()获取参数:', rawPageOptions)
const pageOptObj = (rawPageOptions instanceof UTSJSONObject) ? (rawPageOptions as UTSJSONObject) : (JSON.parse(JSON.stringify(rawPageOptions)) as UTSJSONObject)
const pageCategoryId = pageOptObj.getString('categoryId') ?? ''
if (pageCategoryId !== '') {
categoryId = pageCategoryId
categoryName = pageOptObj.getString('name') ?? ''
console.log('✅ 从getCurrentPages()找到分类参数:', categoryId, categoryName)
}
}
}
// 如果有找到分类ID则选中对应的分类
if (categoryId != '') {
hasLoadedFromParams.value = true
console.log('✅ 准备选中分类:', categoryId)
console.log('分类名称:', categoryName ?? '未指定')
// 检查是否需要更新分类
if (activePrimary.value !== categoryId) {
console.log('当前分类:', activePrimary.value, '与目标分类:', categoryId, '不同,需要更新')
console.log('准备调用selectPrimaryCategory函数...')
selectPrimaryCategory(categoryId)
} else {
console.log('当前分类已经是目标分类,但可能用户想要刷新页面')
console.log('当前分类:', activePrimary.value, '目标分类:', categoryId)
// 即使分类相同,也重新加载数据,确保数据是最新的
// 添加一个小的延迟,确保页面完全显示后再更新数据
setTimeout(() => {
selectPrimaryCategory(categoryId)
}, 100)
}
} else {
console.log('⚠️ onLoad中未找到分类参数将使用从数据库加载的第一个分类')
// 不再使用硬编码的默认分类loadCategories 会设置第一个分类
}
console.log('=== category页面onLoad执行完成 ===')
})
// 页面显示时也检查参数,确保从其他页面返回时能正确显示
onShow(() => {
console.log('=== category页面onShow被调用 ===')
console.log('页面显示时间:', Date.now())
console.log('当前活动分类:', activePrimary.value)
// 1. 优先检查 Storage 中的参数 (由首页传入)
const storageCategoryId = (uni.getStorageSync('selectedCategory') as string) ?? ''
if (storageCategoryId !== '') {
console.log('✅ onShow中找到Storage分类参数:', storageCategoryId)
hasLoadedFromParams.value = true
// 清除Storage防止下次误读
uni.removeStorageSync('selectedCategory')
if (activePrimary.value !== storageCategoryId) {
selectPrimaryCategory(storageCategoryId)
}
// 如果分类还没加载完这里设置了ID等loadCategories完成后会自动匹配信息
return
}
// 在onShow中我们也需要检查是否有新的参数
// 因为当从主页再次点击分类跳转过来时可能不会触发onLoad
// 而是触发onShow
// 获取当前页面实例和参数
const pages = getCurrentPages()
if (pages.length > 0) {
const currentPage = pages[pages.length - 1]
const rawPageOptions = currentPage.options ?? {}
console.log('onShow中获取参数:', rawPageOptions)
const pageOptObj = (rawPageOptions instanceof UTSJSONObject) ? (rawPageOptions as UTSJSONObject) : (JSON.parse(JSON.stringify(rawPageOptions)) as UTSJSONObject)
// 检查是否有分类参数
const pageCategoryId = pageOptObj.getString('categoryId') ?? ''
if (pageCategoryId !== '') {
hasLoadedFromParams.value = true
const categoryId = pageCategoryId
const categoryName = pageOptObj.getString('name') ?? ''
console.log('✅ onShow中找到分类参数:', categoryId, categoryName)
console.log('URL中的时间戳参数:', pageOptObj.getString('timestamp') ?? '')
console.log('URL中的随机参数:', pageOptObj.getString('random') ?? '')
// 检查是否需要更新分类
if (activePrimary.value !== categoryId) {
console.log('当前分类:', activePrimary.value, '与目标分类:', categoryId, '不同,需要更新')
console.log('准备调用selectPrimaryCategory函数...')
selectPrimaryCategory(categoryId)
} else {
console.log('当前分类已经是目标分类,但可能用户想要刷新页面')
console.log('当前分类:', activePrimary.value, '目标分类:', categoryId)
// 即使分类相同,也重新加载数据,确保数据是最新的
// 添加一个小的延迟,确保页面完全显示后再更新数据
setTimeout(() => {
selectPrimaryCategory(categoryId)
}, 100)
}
} else {
console.log('⚠️ onShow中未找到分类参数')
}
}
console.log('=== category页面onShow执行完成 ===')
})
// 添加到购物车 // 添加到购物车
const addToCart = async (product: any) => { async function addToCart(product: Product): Promise<void> {
uni.showLoading({ title: '添加中...' }) uni.showLoading({ title: '添加中...' })
try { try {
const success = await supabaseService.addToCart(product.id, 1) const pid = (product.id ?? '').toString()
if (pid === '') {
uni.hideLoading()
uni.showToast({ title: '商品无效', icon: 'none' })
return
}
const success = await supabaseService.addToCart(pid, 1, '')
if (success) { if (success) {
uni.showToast({ uni.showToast({
title: '已添加到购物车', title: '已添加到购物车',
@@ -491,14 +494,15 @@ const addToCart = async (product: any) => {
} }
// 导航函数 // 导航函数
const navigateToSearch = () => uni.navigateTo({ url: '/pages/mall/consumer/search' }) function navigateToSearch(): void { uni.navigateTo({ url: '/pages/mall/consumer/search' }) }
const navigateToCart = () => uni.navigateTo({ url: '/pages/mall/consumer/cart' }) function navigateToCart(): void { uni.navigateTo({ url: '/pages/mall/consumer/cart' }) }
const navigateToProduct = (product: any) => { function navigateToProduct(product: Product): void {
const id = product.id const id = (product.id ?? '').toString()
const price = (product.base_price || 0).toString() if (id === '') return
const originalPrice = (product.market_price || '').toString() const price = (product.base_price ?? 0).toString()
const name = encodeURIComponent(product.name || '') const originalPrice = (product.market_price ?? '').toString()
const image = encodeURIComponent(product.main_image_url || '') const name = encodeURIComponent(product.name ?? '')
const image = encodeURIComponent(product.main_image_url ?? '')
uni.navigateTo({ uni.navigateTo({
url: `/pages/mall/consumer/product-detail?id=${id}&productId=${id}&price=${price}&originalPrice=${originalPrice}&name=${name}&image=${image}` url: `/pages/mall/consumer/product-detail?id=${id}&productId=${id}&price=${price}&originalPrice=${originalPrice}&name=${name}&image=${image}`
@@ -506,7 +510,7 @@ const navigateToProduct = (product: any) => {
} }
// 相机功能 // 相机功能
const onCamera = () => { function onCamera(): void {
uni.chooseImage({ uni.chooseImage({
count: 1, count: 1,
sourceType: ['camera'], sourceType: ['camera'],
@@ -531,7 +535,7 @@ const onCamera = () => {
} }
// 扫码功能 // 扫码功能
const onScan = () => { function onScan(): void {
uni.scanCode({ uni.scanCode({
success: (res) => { success: (res) => {
console.log('扫码成功:', res) console.log('扫码成功:', res)
@@ -550,7 +554,8 @@ const onScan = () => {
<style> <style>
.category-page { .category-page {
width: 100%; width: 100%;
min-height: 100vh; height: 100%;
overflow: hidden;
background-color: #f8fafc; background-color: #f8fafc;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -567,14 +572,20 @@ const onScan = () => {
box-shadow: 0 2px 12px rgba(76, 175, 80, 0.15); box-shadow: 0 2px 12px rgba(76, 175, 80, 0.15);
} }
/* 导航栏占位 */
.navbar-placeholder {
flex-shrink: 0;
}
/* 搜索栏 */ /* 搜索栏 */
/* 导航栏搜索框容器内边距调整 */ /* 导航栏搜索框容器内边距调整 */
.search-container { .search-container {
height: 44px; /* 调整为与消息页一致的高度 */ height: 44px;
padding: 0 16px; padding: 0 16px;
display: flex; display: flex;
flex-direction: row;
align-items: center; align-items: center;
justify-content: space-between; justify-content: center;
max-width: 1400px; max-width: 1400px;
margin: 0 auto; margin: 0 auto;
width: 100%; width: 100%;
@@ -593,12 +604,11 @@ const onScan = () => {
border-radius: 20px; border-radius: 20px;
padding: 0 4px 0 12px; padding: 0 4px 0 12px;
display: flex; display: flex;
flex-direction: row; /* UVUE 显式设置 row */ flex-direction: row;
align-items: center; align-items: center;
cursor: pointer;
transition: all 0.3s ease; transition: all 0.3s ease;
width: 100%; width: 100%;
height: 32px; /* 减小高度与顶部高度44px适配略小于顶部高度 */ height: 32px;
} }
.search-placeholder { .search-placeholder {
@@ -613,7 +623,7 @@ const onScan = () => {
.nav-inner-search-text { .nav-inner-search-text {
font-size: 12px; /* 字体稍微变小 */ font-size: 12px; /* 字体稍微变小 */
color: #ffffff; color: #ffffff;
font-weight: 500; font-weight: normal;
} }
.icon { .icon {
@@ -683,29 +693,28 @@ const onScan = () => {
/* 分类内容区 */ /* 分类内容区 */
.category-content { .category-content {
flex: 1;
height: 0px;
display: flex; display: flex;
flex-direction: row; /* 强制水平排列 */ flex-direction: row;
/* margin-top: 44px; 已通过 style 动态绑定 */
padding: 0 16px; padding: 0 16px;
max-width: 1400px; max-width: 1400px;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
width: 100%; width: 100%;
gap: 20px; overflow: hidden;
/* height: calc(100vh - 44px); 已通过 style 动态绑定 */
overflow: hidden; /* 防止整体滚动 */
} }
/* 左侧一级分类 */ /* 左侧一级分类 */
.primary-category { .primary-category {
width: 120px; width: 120px;
height: 100%; /* 占满父容器高度 */ height: 100%; /* 占满父容器高度 */
margin-right: 20px; /* gap replacement */
background: white; background: white;
border-radius: 12px; border-radius: 12px;
padding: 12px 0; padding: 12px 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
flex-shrink: 0; flex-shrink: 0;
overflow-y: auto; /* 允许内部滚动 */
} }
.primary-item { .primary-item {
@@ -716,7 +725,7 @@ const onScan = () => {
padding: 12px 8px; padding: 12px 8px;
margin: 4px 8px; margin: 4px 8px;
border-radius: 8px; border-radius: 8px;
cursor: pointer; /* cursor: pointer; removed for uniapp-x support */
transition: all 0.2s ease; transition: all 0.2s ease;
color: #666; color: #666;
text-align: center; text-align: center;
@@ -736,13 +745,13 @@ const onScan = () => {
margin-bottom: 6px; margin-bottom: 6px;
margin-right: 0; /* 移除右边距 */ margin-right: 0; /* 移除右边距 */
text-align: center; text-align: center;
display: block; /* display: block; removed for uniapp-x support */
} }
.primary-name { .primary-name {
font-size: 13px; font-size: 13px;
line-height: 1.4; line-height: 1.4;
display: block; /* display: block; removed for uniapp-x support */
} }
/* 右侧内容区 */ /* 右侧内容区 */
@@ -750,14 +759,13 @@ const onScan = () => {
flex: 1; flex: 1;
height: 100%; /* 占满父容器高度 */ height: 100%; /* 占满父容器高度 */
padding: 0; /* 移除内边距,交给内部元素 */ padding: 0; /* 移除内边距,交给内部元素 */
overflow-y: auto; /* 允许内部滚动 */
} }
.category-header { .category-header {
margin-bottom: 16px; margin-bottom: 16px;
padding: 16px 8px 0 8px; padding: 16px 8px 0 8px;
position: sticky; /* position: sticky; REMOVED for uniapp-x support */
top: 0; /* top: 0; */
background-color: #f8fafc; background-color: #f8fafc;
z-index: 10; z-index: 10;
} }
@@ -776,19 +784,27 @@ const onScan = () => {
/* 商品网格 */ /* 商品网格 */
.product-grid { .product-grid {
display: grid; display: flex;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); flex-direction: row;
gap: 20px; flex-wrap: wrap;
/* grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); REMOVED for uniapp-x support */
/* gap: 20px; removed for compatibility */
padding: 10px; /* add padding to compensate */
width: 100%;
} }
.product-card { .product-card {
background: white; background: white;
border-radius: 12px; border-radius: 12px;
overflow: hidden; overflow: hidden;
cursor: pointer; /* cursor: pointer; removed for uniapp-x support */
transition: all 0.3s ease; transition: all 0.3s ease;
border: 1px solid #e0e0e0; border: 1px solid #e0e0e0;
position: relative; position: relative;
/* margin: 10px; gap replacement - moved to logic */
width: 44%; /* Decreased to 44% to ensure it fits (44 + 3 + 3 = 50%) */
margin: 3%; /* Increased margin */
box-sizing: border-box; /* Ensure border IS included in width */
} }
.product-card:hover { .product-card:hover {
@@ -805,14 +821,14 @@ const onScan = () => {
font-size: 11px; font-size: 11px;
padding: 4px 12px; padding: 4px 12px;
border-radius: 12px; border-radius: 12px;
font-weight: 600; font-weight: 700;
z-index: 2; z-index: 2;
} }
.product-image { .product-image {
width: 100%; width: 100%;
height: 160px; height: 160px;
object-fit: cover; /* object-fit: cover; REMOVED for uniapp-x support - default behavior is often acceptable or handle via image mode */
background: white; background: white;
} }
@@ -822,10 +838,10 @@ const onScan = () => {
.product-name { .product-name {
font-size: 15px; font-size: 15px;
font-weight: 600; font-weight: 700;
color: #333; color: #333;
margin-bottom: 4px; margin-bottom: 4px;
display: block; /* display: block; REMOVED for uniapp-x support */
line-height: 1.4; line-height: 1.4;
} }
@@ -833,19 +849,22 @@ const onScan = () => {
font-size: 13px; font-size: 13px;
color: #666; color: #666;
margin-bottom: 12px; margin-bottom: 12px;
display: block; /* display: block; REMOVED for uniapp-x support */
} }
.price-section { .price-section {
display: flex; display: flex;
align-items: baseline; flex-direction: row;
gap: 8px; align-items: flex-end; /* changed from baseline */
/* gap: 8px; */
margin-bottom: 12px; margin-bottom: 12px;
} }
.current-price { .current-price {
display: flex; display: flex;
align-items: baseline; flex-direction: row;
align-items: flex-end; /* changed from baseline */
margin-right: 8px; /* gap replacement */
} }
.price-symbol { .price-symbol {
@@ -863,7 +882,7 @@ const onScan = () => {
.original-price { .original-price {
font-size: 13px; font-size: 13px;
color: #999; color: #999;
text-decoration: line-through; /* text-decoration: line-through; REMOVED for uniapp-x support */
} }
.product-meta { .product-meta {
@@ -890,14 +909,14 @@ const onScan = () => {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 6px; /* gap: 6px; */
background: #4CAF50; background: #4CAF50;
color: white; color: white;
padding: 8px 12px; padding: 8px 12px;
border-radius: 8px; border-radius: 8px;
font-size: 13px; font-size: 13px;
font-weight: 500; font-weight: bold;
cursor: pointer; /* cursor: pointer; removed for uniapp-x support */
transition: all 0.2s ease; transition: all 0.2s ease;
} }
@@ -907,6 +926,7 @@ const onScan = () => {
.cart-icon { .cart-icon {
font-size: 14px; font-size: 14px;
margin-right: 6px; /* gap replacement */
} }
/* 空状态 */ /* 空状态 */
@@ -946,7 +966,7 @@ const onScan = () => {
} }
.load-text { .load-text {
display: inline-block; /* display: inline-block; REMOVED for uniapp-x support */
padding: 8px 16px; padding: 8px 16px;
background: #f5f5f5; background: #f5f5f5;
border-radius: 20px; border-radius: 20px;
@@ -956,10 +976,19 @@ const onScan = () => {
/* 小屏手机 (小于414px) */ /* 小屏手机 (小于414px) */
@media screen and (max-width: 414px) { @media screen and (max-width: 414px) {
.search-container {
padding: 0 12px;
height: 44px;
}
.search-box {
padding: 0 4px 0 12px;
margin: 0;
height: 30px;
}
.category-content { .category-content {
/* flex-direction: column; 移除这一行,保持 row 布局 */
padding: 0 8px; padding: 0 8px;
gap: 10px;
} }
.primary-category { .primary-category {
@@ -967,6 +996,7 @@ const onScan = () => {
/* display: flex; 移除flex布局保持默认 */ /* display: flex; 移除flex布局保持默认 */
/* flex-wrap: wrap; 移除换行 */ /* flex-wrap: wrap; 移除换行 */
padding: 8px 0; padding: 8px 0;
margin-right: 10px; /* Gap replacement */
} }
.primary-item { .primary-item {
@@ -988,10 +1018,15 @@ const onScan = () => {
} }
.product-grid { .product-grid {
grid-template-columns: repeat(2, 1fr); /* 改为双列显示 */ /* grid-template-columns: repeat(2, 1fr); REMOVED */
gap: 8px; /* gap: 8px; REMOVED */
padding: 0 4px 20px 4px; /* 增加底部内边距 */ padding: 0 4px 20px 4px; /* 增加底部内边距 */
} }
.product-card {
width: 48%; /* 2 columns for mobile */
margin: 1%;
}
/* 手机端商品卡片极简模式 - 仿照主页样式 */ /* 手机端商品卡片极简模式 - 仿照主页样式 */
.product-spec, .product-spec,
@@ -1017,9 +1052,10 @@ const onScan = () => {
margin-bottom: 2px; margin-bottom: 2px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
display: -webkit-box; /* display: -webkit-box; REMOVED for support */
-webkit-line-clamp: 2; /* -webkit-line-clamp: 2; REMOVED for support */
-webkit-box-orient: vertical; /* -webkit-box-orient: vertical; REMOVED for support */
lines: 2; /* UTS text truncation */
} }
.price-section { .price-section {
@@ -1043,7 +1079,7 @@ const onScan = () => {
.search-container { .search-container {
padding: 0 12px; padding: 0 12px;
height: 55px; height: 44px;
} }
.search-box { .search-box {
@@ -1057,30 +1093,45 @@ const onScan = () => {
/* 中屏手机/小平板 (415px-768px) */ /* 中屏手机/小平板 (415px-768px) */
@media screen and (min-width: 415px) and (max-width: 768px) { @media screen and (min-width: 415px) and (max-width: 768px) {
.product-grid { .search-container {
grid-template-columns: repeat(2, 1fr); padding: 0 16px;
gap: 16px; height: 44px;
}
.product-card {
width: 46%;
margin: 2%;
} }
} }
/* 平板设备 (769px-1024px) */ /* 平板设备 (769px-1024px) */
@media screen and (min-width: 769px) and (max-width: 1024px) { @media screen and (min-width: 769px) and (max-width: 1024px) {
.product-grid { .search-container {
grid-template-columns: repeat(3, 1fr); padding: 0 16px;
gap: 20px; height: 44px;
}
.product-card {
width: 30%;
margin: 1.5%;
} }
} }
/* 桌面端 (1025px以上) */ /* 桌面端 (1025px以上) */
@media screen and (min-width: 1025px) { @media screen and (min-width: 1025px) {
.search-container {
padding: 0 16px;
height: 44px;
}
.category-content { .category-content {
gap: 30px;
padding: 0 24px; padding: 0 24px;
} }
.primary-category { .primary-category {
width: 160px; width: 160px;
padding: 16px 0; padding: 16px 0;
margin-right: 30px; /* Gap replacement */
} }
.primary-item { .primary-item {
@@ -1116,12 +1167,14 @@ const onScan = () => {
} }
.product-grid { .product-grid {
grid-template-columns: repeat(4, 1fr); /* grid-template-columns: repeat(4, 1fr); REMOVED */
gap: 24px; /* gap: 24px; REMOVED */
} }
.product-card { .product-card {
border-radius: 14px; border-radius: 14px;
width: 22%; /* 4 columns */
margin: 1.5%;
} }
.product-info { .product-info {
@@ -1145,10 +1198,14 @@ const onScan = () => {
@media screen and (min-width: 1400px) { @media screen and (min-width: 1400px) {
.category-content { .category-content {
max-width: 1600px; max-width: 1600px;
gap: 40px; /* gap: 40px; REMOVED */
padding: 0 32px; padding: 0 32px;
} }
.primary-category {
margin-right: 40px; /* Gap replacement */
}
.primary-category { .primary-category {
width: 200px; width: 200px;
padding: 20px 0; padding: 20px 0;
@@ -1187,12 +1244,14 @@ const onScan = () => {
} }
.product-grid { .product-grid {
grid-template-columns: repeat(5, 1fr); /* grid-template-columns: repeat(5, 1fr); REMOVED */
gap: 28px; /* gap: 28px; REMOVED */
} }
.product-card { .product-card {
border-radius: 16px; border-radius: 16px;
width: 17%; /* 5 columns */
margin: 1.5%;
} }
.product-image { .product-image {

View File

@@ -718,7 +718,7 @@ const onScan = () => {
font-size: 11px; font-size: 11px;
padding: 4px 12px; padding: 4px 12px;
border-radius: 12px; border-radius: 12px;
font-weight: 600; font-weight: 700;
z-index: 2; z-index: 2;
} }
@@ -735,7 +735,7 @@ const onScan = () => {
.product-name { .product-name {
font-size: 15px; font-size: 15px;
font-weight: 600; font-weight: 700;
color: #333; color: #333;
margin-bottom: 4px; margin-bottom: 4px;
display: block; display: block;

View File

@@ -31,7 +31,7 @@
<!-- 时间分割线 --> <!-- 时间分割线 -->
<view class="time-divider"> <view class="time-divider">
<text>今天 14:30</text> <text class="time-text">今天 14:30</text>
</view> </view>
<!-- 消息项 --> <!-- 消息项 -->
@@ -45,11 +45,11 @@
<view v-if="message.type === 'received'" class="message-wrapper"> <view v-if="message.type === 'received'" class="message-wrapper">
<image <image
class="avatar" class="avatar"
src="https://picsum.photos/40/40?random=1" src="/static/icons/shop-default.png"
mode="aspectFill" mode="aspectFill"
/> />
<view class="message-content-wrapper"> <view class="message-content-wrapper">
<text class="sender-name">客服小美</text> <text class="sender-name">{{ headerTitle }}</text>
<view class="message-bubble"> <view class="message-bubble">
<text class="message-text">{{ message.content }}</text> <text class="message-text">{{ message.content }}</text>
<text class="message-time">{{ message.time }}</text> <text class="message-time">{{ message.time }}</text>
@@ -67,7 +67,7 @@
</view> </view>
<image <image
class="avatar me" class="avatar me"
src="https://picsum.photos/40/40?random=2" src="/static/avatar-default.png"
mode="aspectFill" mode="aspectFill"
/> />
</view> </view>
@@ -103,7 +103,7 @@
</view> </view>
<!-- 表情选择器 --> <!-- 表情选择器 -->
<view v-if="showEmoji" class="emoji-picker"> <scroll-view v-if="showEmoji" class="emoji-picker" direction="vertical">
<view class="emoji-category"> <view class="emoji-category">
<text <text
v-for="emoji in emojiList" v-for="emoji in emojiList"
@@ -114,7 +114,7 @@
{{ emoji }} {{ emoji }}
</text> </text>
</view> </view>
</view> </scroll-view>
</view> </view>
</template> </template>
@@ -122,10 +122,18 @@
import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue' import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue'
import { supabaseService, type ChatMessage } from '@/utils/supabaseService.uts' import { supabaseService, type ChatMessage } from '@/utils/supabaseService.uts'
import supa from '@/components/supadb/aksupainstance.uts' import supa from '@/components/supadb/aksupainstance.uts'
import type { AkSupaRealtimeChannel } from '@/components/supadb/aksupa.uts'
import { getCurrentUser } from '@/utils/store.uts' import { getCurrentUser } from '@/utils/store.uts'
type UiChatMessage = {
id: string
type: string
content: string
time: string
}
// 响应式数据 // 响应式数据
const messages = ref<any[]>([]) const messages = ref<UiChatMessage[]>([])
const inputMessage = ref<string>('') const inputMessage = ref<string>('')
const inputFocus = ref<boolean>(false) const inputFocus = ref<boolean>(false)
const showEmoji = ref<boolean>(false) const showEmoji = ref<boolean>(false)
@@ -134,11 +142,106 @@ const currentUserId = ref<string>('')
const merchantId = ref<string>('') // 商家ID const merchantId = ref<string>('') // 商家ID
const headerTitle = ref<string>('在线客服') const headerTitle = ref<string>('在线客服')
const navPaddingTop = ref<string>('30px') // 默认值,包含状态栏高度+原有内边距 const navPaddingTop = ref<string>('30px') // 默认值,包含状态栏高度+原有内边距
let realtimeChannel: any = null let realtimeChannel: AkSupaRealtimeChannel | null = null
// 模拟表情列表 // 模拟表情列表
const emojiList = ['😊', '😂', '🤣', '😍', '😘', '🥰', '😭', '😡', '👍', '👏', '🙏', '🎉', '❤️', '🔥', '⭐'] const emojiList = ['😊', '😂', '🤣', '😍', '😘', '🥰', '😭', '😡', '👍', '👏', '🙏', '🎉', '❤️', '🔥', '⭐']
function scrollToBottom(): void {
nextTick(() => {
if (messages.value.length > 0) {
const lastMsgId = messages.value[messages.value.length - 1].id
scrollToView.value = 'msg-' + lastMsgId
}
})
}
function getCurrentTime(): string {
const now = new Date()
const hours = now.getHours().toString().padStart(2, '0')
const minutes = now.getMinutes().toString().padStart(2, '0')
return `${hours}:${minutes}`
}
function setupRealtimeSubscription(): void {
console.log('开始建立聊天实时订阅...')
const filter = ({
event: 'INSERT',
schema: 'public',
table: 'ml_chat_messages'
} as UTSJSONObject)
realtimeChannel = supa.channel('public:ml_chat_messages')
.on('postgres_changes', filter, (payload: any) => {
const payloadObj = (payload instanceof UTSJSONObject) ? (payload as UTSJSONObject) : (JSON.parse(JSON.stringify(payload ?? {})) as UTSJSONObject)
const newMsgAny = payloadObj.get('new')
if (newMsgAny == null) return
const newMsg = (newMsgAny instanceof UTSJSONObject) ? (newMsgAny as UTSJSONObject) : (JSON.parse(JSON.stringify(newMsgAny)) as UTSJSONObject)
console.log('收到新消息:', newMsg)
const senderId = newMsg.getString('sender_id') ?? ''
const receiverId = newMsg.getString('receiver_id') ?? ''
if (senderId === currentUserId.value) {
return
}
if (receiverId === currentUserId.value) {
if (merchantId.value != '' && senderId !== merchantId.value) {
return
}
const createdAt = newMsg.getString('created_at') ?? new Date().toISOString()
const date = new Date(createdAt)
const timeStr = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
const incomingMsg: UiChatMessage = {
id: newMsg.getString('id') ?? Date.now().toString(),
type: 'received',
content: newMsg.getString('content') ?? '',
time: timeStr
}
messages.value.push(incomingMsg)
scrollToBottom()
}
})
.subscribe((status: string, err: any | null) => {
console.log('订阅状态:', status)
})
}
async function loadChatHistory(): Promise<void> {
let rawMsgs: ChatMessage[] = []
if (merchantId.value != '') {
rawMsgs = await supabaseService.getChatMessages(merchantId.value)
} else {
console.warn("No merchant ID provided for chat")
return
}
messages.value = rawMsgs.reverse().map((m: ChatMessage): UiChatMessage => {
const date = new Date(m.created_at ?? new Date().toISOString())
const timeStr = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
const sender = m.sender_id ?? ''
const msgType = (currentUserId.value != '' && sender == currentUserId.value) ? 'sent' : 'received'
const rawId = (m.id ?? '').toString()
const msgId = rawId !== '' ? rawId : Date.now().toString()
return {
id: msgId,
type: msgType,
content: m.content ?? '',
time: timeStr
}
})
setTimeout(() => {
scrollToBottom()
}, 100)
}
// 生命周期 // 生命周期
onLoad((options: any) => { onLoad((options: any) => {
// 动态获取状态栏高度 // 动态获取状态栏高度
@@ -147,113 +250,47 @@ onLoad((options: any) => {
// 状态栏高度 + 10px 原有顶部内边距 // 状态栏高度 + 10px 原有顶部内边距
navPaddingTop.value = (statusBarH + 10) + 'px' navPaddingTop.value = (statusBarH + 10) + 'px'
if (options.merchantId) { const optObj = (options instanceof UTSJSONObject) ? (options as UTSJSONObject) : (JSON.parse(JSON.stringify(options ?? {})) as UTSJSONObject)
merchantId.value = options.merchantId const mid = optObj.getString('merchantId') ?? ''
} if (mid !== '') {
if (options.merchantName) { merchantId.value = mid
headerTitle.value = options.merchantName }
} const mname = optObj.getString('merchantName') ?? ''
if (mname !== '') {
headerTitle.value = mname
}
}) })
onMounted(async () => { onMounted(() => {
const user = await getCurrentUser() supabaseService.ensureSession().then((uid) => {
if (user) { if (uid != null) {
currentUserId.value = user.id currentUserId.value = uid
} } else {
loadChatHistory() getCurrentUser().then((user) => {
setupRealtimeSubscription() if (user != null) {
currentUserId.value = user.id ?? ''
}
})
}
loadChatHistory()
setupRealtimeSubscription()
})
}) })
onUnmounted(() => { onUnmounted(() => {
if (realtimeChannel) { if (realtimeChannel != null) {
supa.removeChannel(realtimeChannel) supa.removeChannel(realtimeChannel!!)
} }
}) })
// 建立实时订阅
const setupRealtimeSubscription = () => {
console.log('开始建立聊天实时订阅...')
realtimeChannel = supa.channel('public:ml_chat_messages')
.on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'ml_chat_messages' }, (payload: any) => {
const newMsg = payload.new
console.log('收到新消息:', newMsg)
// 如果是我发的消息因为已经乐观更新了所以忽略或者根据ID更新状态
if (newMsg.sender_id === currentUserId.value) {
return
}
// 如果是发给我的消息
if (newMsg.receiver_id === currentUserId.value) {
// 如果指定了商家,只接收该商家的消息
if (merchantId.value && newMsg.sender_id !== merchantId.value) {
return
}
// 转换为UI消息格式
const date = new Date(newMsg.created_at || new Date().toISOString())
const timeStr = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
const incomingMsg = {
id: newMsg.id || Date.now(), // 优先使用DB ID
type: 'received',
content: newMsg.content,
time: timeStr
}
messages.value.push(incomingMsg)
scrollToBottom()
}
})
.subscribe((status: string) => {
console.log('订阅状态:', status)
})
}
// 加载聊天记录
const loadChatHistory = async () => {
let rawMsgs: ChatMessage[] = []
if (merchantId.value) {
rawMsgs = await supabaseService.getChatMessages(merchantId.value)
} else {
rawMsgs = await supabaseService.getUserChatMessages()
}
messages.value = rawMsgs.reverse().map((m: ChatMessage) => {
const date = new Date(m.created_at || new Date().toISOString())
const timeStr = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
const msg : any = {
id: m.id,
type: m.is_from_user ? 'sent' : 'received', // 假设is_from_user标志是准确的或者比较 sender_id
content: m.content,
time: timeStr
}
// 双重确认类型
if (currentUserId.value && m.sender_id === currentUserId.value) {
msg.type = 'sent'
} else if (currentUserId.value && m.sender_id !== currentUserId.value) {
msg.type = 'received'
}
return msg
})
// 滚动到底部
setTimeout(() => {
scrollToBottom()
}, 100)
}
// 发送消息
const sendMessage = async () => { const sendMessage = async () => {
const content = inputMessage.value.trim() const content = inputMessage.value.trim()
if (!content) return if (content == '') return
// 添加发送的消息 (乐观更新) // 添加发送的消息 (乐观更新)
const newMessage = { const newMessage: UiChatMessage = {
id: Date.now(), id: Date.now().toString(),
type: 'sent', type: 'sent',
content: content, content: content,
time: getCurrentTime() time: getCurrentTime()
@@ -266,23 +303,15 @@ const sendMessage = async () => {
scrollToBottom() scrollToBottom()
// 发送到 Supabase // 发送到 Supabase
// 如果有 merchantId,发送给指定商家,否则可能是发给系统或默认客服 if (merchantId.value != '') {
const success = await supabaseService.sendChatMessage(content, merchantId.value || null) const success = await supabaseService.sendMessage(merchantId.value, content)
if (!success) {
if (!success) { uni.showToast({
uni.showToast({ title: '发送失败',
title: '发送失败', icon: 'none'
icon: 'none' })
}) }
// 实际项目中可能需要标记消息为发送失败状态
} }
// 移除模拟回复,依赖 Realtime 接收真实回复
/*
setTimeout(() => {
simulateCustomerReply()
}, 2000)
*/
} }
// 模拟客服回复 (已禁用,改用 Realtime) // 模拟客服回复 (已禁用,改用 Realtime)
@@ -294,37 +323,19 @@ const simulateCustomerReply = async () => {
/* 移除不再使用的 simulateCustomerReply 和 addReceivedMessage */ /* 移除不再使用的 simulateCustomerReply 和 addReceivedMessage */
// 滚动到底部
const scrollToBottom = () => {
nextTick(() => {
if (messages.value.length > 0) {
const lastMsgId = messages.value[messages.value.length - 1].id
scrollToView.value = 'msg-' + lastMsgId
}
})
}
// 获取当前时间
const getCurrentTime = (): string => {
const now = new Date()
const hours = now.getHours().toString().padStart(2, '0')
const minutes = now.getMinutes().toString().padStart(2, '0')
return `${hours}:${minutes}`
}
// 插入表情 // 插入表情
const insertEmoji = (emoji: string) => { function insertEmoji(emoji: string): void {
inputMessage.value += emoji inputMessage.value += emoji
inputFocus.value = true inputFocus.value = true
} }
// 显示表情选择器 // 显示表情选择器
const showEmojiPicker = () => { function showEmojiPicker(): void {
showEmoji.value = !showEmoji.value showEmoji.value = !showEmoji.value
} }
// 显示图片选择器 // 显示图片选择器
const showImagePicker = () => { function showImagePicker(): void {
uni.chooseImage({ uni.chooseImage({
count: 1, count: 1,
success: (res) => { success: (res) => {
@@ -335,7 +346,7 @@ const showImagePicker = () => {
} }
// 显示更多工具 // 显示更多工具
const showMoreTools = () => { function showMoreTools(): void {
uni.showActionSheet({ uni.showActionSheet({
itemList: ['发送位置', '发送文件', '发送语音'], itemList: ['发送位置', '发送文件', '发送语音'],
success: (res) => { success: (res) => {
@@ -345,7 +356,7 @@ const showMoreTools = () => {
} }
// 显示更多操作 // 显示更多操作
const showMoreActions = () => { function showMoreActions(): void {
uni.showActionSheet({ uni.showActionSheet({
itemList: ['投诉客服', '结束对话', '清除记录'], itemList: ['投诉客服', '结束对话', '清除记录'],
success: (res) => { success: (res) => {
@@ -389,7 +400,7 @@ const goBack = () => {
<style> <style>
.chat-page { .chat-page {
width: 100%; width: 100%;
height: 100vh; flex: 1;
background-color: #f5f5f5; background-color: #f5f5f5;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -425,7 +436,6 @@ const goBack = () => {
font-size: 16px; font-size: 16px;
font-weight: bold; font-weight: bold;
color: #333; color: #333;
display: block;
margin-bottom: 2px; margin-bottom: 2px;
} }
@@ -460,7 +470,7 @@ const goBack = () => {
background-color: #f0f0f0; background-color: #f0f0f0;
padding: 5px 15px; padding: 5px 15px;
border-radius: 15px; border-radius: 15px;
display: inline-block;
} }
/* 时间分割线 */ /* 时间分割线 */
@@ -469,7 +479,7 @@ const goBack = () => {
margin: 20px 0; margin: 20px 0;
} }
.time-divider text { .time-text {
font-size: 12px; font-size: 12px;
color: #999; color: #999;
background-color: #f0f0f0; background-color: #f0f0f0;
@@ -499,18 +509,17 @@ const goBack = () => {
.avatar.me { .avatar.me {
margin-right: 0; margin-right: 0;
margin-left: 10px; margin-left: 10px;
order: 2; /* order: 2; removed for uni-app-x */
} }
.message-content-wrapper { .message-content-wrapper {
max-width: 70%; /* max-width removed */
} }
.sender-name { .sender-name {
font-size: 12px; font-size: 12px;
color: #999; color: #999;
margin-bottom: 5px; margin-bottom: 5px;
display: block;
} }
.message-bubble { .message-bubble {
@@ -519,8 +528,7 @@ const goBack = () => {
border-radius: 18px; border-radius: 18px;
position: relative; position: relative;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
max-width: 100%; /* max-width wrap removed */
word-wrap: break-word;
} }
.message-bubble.me { .message-bubble.me {
@@ -528,7 +536,7 @@ const goBack = () => {
border-bottom-right-radius: 4px; border-bottom-right-radius: 4px;
} }
.message-bubble:not(.me) { .message-bubble-not-me .message-content {
border-bottom-left-radius: 4px; border-bottom-left-radius: 4px;
} }
@@ -536,14 +544,12 @@ const goBack = () => {
font-size: 15px; font-size: 15px;
color: #333; color: #333;
line-height: 1.4; line-height: 1.4;
display: block;
margin-bottom: 5px; margin-bottom: 5px;
} }
.message-time { .message-time {
font-size: 11px; font-size: 11px;
color: #999; color: #999;
display: block;
text-align: right; text-align: right;
} }
@@ -608,8 +614,8 @@ const goBack = () => {
background-color: white; background-color: white;
border-top: 1px solid #eee; border-top: 1px solid #eee;
padding: 10px; padding: 10px;
max-height: 200px; height: 200px;
overflow-y: auto; /* overflow-y removed */
position: fixed; position: fixed;
bottom: 60px; bottom: 60px;
left: 0; left: 0;
@@ -633,24 +639,5 @@ const goBack = () => {
justify-content: center; justify-content: center;
} }
/* 响应式适配 */ /* 响应式适配 removed for strict uv-app-x compliance */
@media screen and (max-width: 320px) { </style>
.message-content-wrapper {
max-width: 65%;
}
.message-text {
font-size: 14px;
}
}
@media screen and (min-width: 415px) {
.message-content-wrapper {
max-width: 75%;
}
.chat-input {
padding: 15px 20px;
}
}
</style>

View File

@@ -146,24 +146,24 @@ const currentUserId = ref('')
const messages = ref<any[]>([]) const messages = ref<any[]>([])
const emojiList = ['😊', '😂', '👍', '👌', '❤️', '🌹', '🙏', '🎉', '😡', '😭', '🤔', '👋', '🤝', '💊', '🏥'] const emojiList = ['😊', '😂', '👍', '👌', '❤️', '🌹', '🙏', '🎉', '😡', '😭', '🤔', '👋', '🤝', '💊', '🏥']
let realtimeChannel: any = null let realtimeChannel: any | null = null
onLoad((options: any) => { onLoad((options: any) => {
// 获取状态栏高度 // 获取状态栏高度
const sys = uni.getSystemInfoSync() const sys = uni.getSystemInfoSync()
statusBarHeight.value = sys.statusBarHeight || 0 statusBarHeight.value = sys.statusBarHeight ?? 0
// 获取参数 // 获取参数
if (options.merchantId) { const optObj = (options instanceof UTSJSONObject) ? (options as UTSJSONObject) : (JSON.parse(JSON.stringify(options ?? {})) as UTSJSONObject)
merchantId.value = options.merchantId const mid = optObj.getString('merchantId') ?? ''
merchantName.value = options.merchantName || '商家' if (mid !== '') {
merchantLogo.value = options.merchantLogo || '' merchantId.value = mid
console.log('开始聊天商家ID:', merchantId.value) merchantName.value = optObj.getString('merchantName') ?? '商家'
} else { merchantLogo.value = optObj.getString('merchantLogo') ?? ''
// 测试模式或默认客服 console.log('开始聊天商家ID:', merchantId.value)
// uni.showToast({ title: '参数缺失', icon: 'none' }) } else {
merchantName.value = '平台客服' merchantName.value = '平台客服'
} }
// 获取当前用户 // 获取当前用户
const uid = supabaseService.getCurrentUserId() const uid = supabaseService.getCurrentUserId()
@@ -210,31 +210,35 @@ const loadMoreHistory = () => {
// 开启实时订阅 // 开启实时订阅
const startRealtimeSubscription = () => { const startRealtimeSubscription = () => {
if (!currentUserId.value) return if (currentUserId.value == '') return
console.log('开启消息监听...') console.log('开启消息监听...')
const filterObj = ({
event: 'INSERT',
schema: 'public',
table: 'ml_chat_messages'
} as UTSJSONObject)
realtimeChannel = supa.channel(`chat_${currentUserId.value}`) realtimeChannel = supa.channel(`chat_${currentUserId.value}`)
.on( .on(
'postgres_changes', 'postgres_changes',
{ filterObj,
event: 'INSERT', (payload: any) => {
schema: 'public',
table: 'ml_chat_messages',
filter: `receiver_id=eq.${currentUserId.value}`
},
(payload) => {
console.log('收到新消息:', payload) console.log('收到新消息:', payload)
const newMsg = payload.new as any const payloadObj = (payload instanceof UTSJSONObject) ? (payload as UTSJSONObject) : (JSON.parse(JSON.stringify(payload ?? {})) as UTSJSONObject)
const newAny = payloadObj.get('new')
if (newAny == null) return
const newMsg = (newAny instanceof UTSJSONObject) ? (newAny as UTSJSONObject) : (JSON.parse(JSON.stringify(newAny)) as UTSJSONObject)
// 只有来自当前聊天的商家的消息才显示,或者如果是全局客服模式 // 只有来自当前聊天的商家的消息才显示,或者如果是全局客服模式
if (newMsg.sender_id === merchantId.value || !merchantId.value) { const senderId = newMsg.getString('sender_id') ?? ''
if (senderId === merchantId.value || merchantId.value == '') {
const formatted = formatMessage({ const formatted = formatMessage({
id: newMsg.id, id: newMsg.getString('id') ?? '',
content: newMsg.content, content: newMsg.getString('content') ?? '',
msg_type: newMsg.msg_type, msg_type: newMsg.getString('msg_type') ?? '',
sender_id: newMsg.sender_id, sender_id: senderId,
receiver_id: newMsg.receiver_id, receiver_id: newMsg.getString('receiver_id') ?? '',
is_from_user: false, // 收到的一定不是自己发的 is_from_user: false, // 收到的一定不是自己发的
created_at: newMsg.created_at created_at: newMsg.getString('created_at') ?? ''
} as ChatMessage) } as ChatMessage)
messages.value.push(formatted) messages.value.push(formatted)
@@ -271,7 +275,7 @@ const formatMessage = (m: ChatMessage): any => {
const sendMessage = async () => { const sendMessage = async () => {
const text = inputMessage.value.trim() const text = inputMessage.value.trim()
if (!text) return if (text == '') return
// 乐观更新 UI // 乐观更新 UI
const tempId = 'temp_' + Date.now() const tempId = 'temp_' + Date.now()

File diff suppressed because it is too large Load Diff

View File

@@ -35,20 +35,20 @@ type Coupon = {
const coupons = ref<Coupon[]>([]) const coupons = ref<Coupon[]>([])
onMounted(() => {
loadCoupons()
})
const loadCoupons = async () => { const loadCoupons = async () => {
uni.showLoading({ title: '加载中...' }) uni.showLoading({ title: '加载中...' })
try { try {
const userCoupons = await supabaseService.getUserCoupons(1) // 1: unused const userCoupons = await supabaseService.getUserCoupons(1)
coupons.value = userCoupons.map((item: UserCoupon) => { coupons.value = userCoupons.map((item: UserCoupon): Coupon => {
const amountVal = item.amount ?? 0
const expiryVal = (item.expire_at != null && item.expire_at !== '')
? item.expire_at.substring(0, 10)
: '长期有效'
return { return {
id: item.id, id: item.id,
title: item.template_name || '优惠券', title: (item.template_name != null && item.template_name !== '') ? item.template_name : '优惠券',
amount: `¥${item.amount || 0}`, amount: `¥${amountVal}`,
expiry: item.expire_at ? item.expire_at.substring(0, 10) : '长期有效' expiry: expiryVal
} as Coupon } as Coupon
}) })
} catch (e) { } catch (e) {
@@ -59,6 +59,10 @@ const loadCoupons = async () => {
} }
} }
onMounted(() => {
loadCoupons()
})
const useCoupon = (coupon: Coupon) => { const useCoupon = (coupon: Coupon) => {
uni.switchTab({ uni.switchTab({
url: '/pages/mall/consumer/index' url: '/pages/mall/consumer/index'
@@ -70,7 +74,7 @@ const useCoupon = (coupon: Coupon) => {
.coupons-page { .coupons-page {
padding: 15px; padding: 15px;
background-color: #f5f5f5; background-color: #f5f5f5;
min-height: 100vh; flex: 1;
} }
.empty-state { .empty-state {

View File

@@ -0,0 +1,76 @@
1、在 uni-app x 的 UTS 语法中:
- 不能直接访问 `any` 类型对象的属性
- 需要将对象转换为 `UTSJSONObject` 类型后使用 `getString()`、`getNumber()` 等方法访问属性
- 数组元素需要明确的类型定义才能在模板中正确访问属性
- 空数组需要明确指定类型,如 `[] as string[]`
- 函数必须在调用前定义(不支持函数提升)
- 访问动态对象属性需要使用 `UTSJSONObject` 的 `getString()`、`getNumber()` 等方法
- 类型导入需要使用 `type` 关键字
-UTS 不允许直接访问 `any` 类型的属性。需要将参数类型改为 `MessageItem`
- UTS 不允许直接访问 `any` 类型参数的属性
- 必须使用明确的类型定义才能访问属性
- 函数必须先定义后使用(包括生命周期钩子调用的函数)
- 不允许直接访问 `any` 类型参数的属性
- `if` 条件必须是 `boolean` 类型,不能是 `Any?`
- `forEach` 箭头函数中不能使用赋值表达式作为函数体
- 函数必须先定义后使用
- `any` 类型不能直接使用点语法访问属性,必须使用索引访问 `obj['property']`
- 访问后需要类型转换 `as number` / `as string`
- `if` 条件必须是 `boolean` 类型
- 函数必须先定义后调用
- `any` 类型不能直接使用点语法访问属性
- 需要转换为 `Record<string, any>` 后使用索引访问 `obj['property']`
- 模板中的 `||` 运算符左边必须是 boolean 类型
- 可空类型使用可选链 `?.` 和空值合并 `??`
- `uni.showModal` 的 `success` 回调不能是 `async` 函数
- 解决方案:创建独立的 async 函数(如 `doCancelOrder`、`doConfirmReceive`、`doApplyRefund`),在回调中调用
- 模板中访问可空类型属性前必须先判空 `v-if="order != null"`
- 使用可选链 `?.` 和空值合并 `??` 处理可空类型
- `uni.showModal` 的 `success` 回调不能是 `async` 函数
- 函数必须在使用前定义
- 不支持 `Record<K, V>` 对象字面量语法
- 模板中可空类型必须使用 `?.` 安全访问
- `uni.showModal` 的 `success` 回调不能是 `async` 函数
- `any` 类型属性访问需转换为 `Record<string, any>` 后用索引访问
- 不支持 `Record<K, V>` 对象字面量语法
- `any` 类型属性访问需转换为 `Record<string, any>` 后用索引访问
- `uni.showModal` 的 `success` 回调不能是 `async` 函数
- 模板中可空类型必须使用 `?.` 安全访问
- 不支持 `Object.values()` 和 `Object.entries()`
- 嵌套的数组方法调用可能导致类型推断失败,应拆分为独立变量
- `any` 类型属性访问需转换为 `Record<string, any>` 后用索引访问
- 不支持 `Record<K, V>` 对象字面量语法
- 不支持 `Object.keys()`、`Object.values()`、`Object.entries()`
- 嵌套的数组方法调用可能导致类型推断失败,应改用 for 循环
- `any` 类型属性访问需转换为 `Record<string, any>` 后用索引访问
- 函数必须在使用前定义
- 对象字面量 `{...}` 只能用于构造类型class不能用于接口interface
- 需要创建动态对象时,应使用 `new UTSJSONObject()` 然后调用 `.set()` 方法
- 对于 `type` 定义的对象类型,同样需要使用 `UTSJSONObject`
- 对于 `any[]` 或 `reactive` 数组,访问元素属性时需要先转换为 `Record<string, any>` 或 `any[]`
- 使用索引访问属性时,推荐使用方括号语法 `obj['property']` 而非点语法 `obj.property`
- 这是因为 UTS 的类型系统比 TypeScript 更严格,需要在运行时明确类型转换
- `reactive` 对象在 UTS 中不支持索引器赋值操作
- 对于需要整体替换的数组,推荐使用 `ref` 而非 `reactive`
- 使用 `ref` 时,通过 `.value` 进行整体替换可以正确触发响应式更新
- 函数必须在使用前声明(不支持函数提升)
- 访问 `any` 类型对象的属性需要先转换为 `Record<string, any>`
- `ref` 数组元素不能直接整体替换,需要修改元素属性
- 对于可能为 `null` 的参数,需要显式检查后再传递给函数
- 函数必须在使用前声明(不支持函数提升)
- 依赖关系需要明确:被调用的函数必须先定义
- 这与 JavaScript 的函数提升行为不同UTS 更接近 C/Java 的编译方式
- UTS中箭头函数 `() => {}` 有时会导致"Parenthesized expression cannot be empty"错误
- 解决方案:使用普通函数 `function name(): Type {}` 代替箭头函数
- 变量声明必须有显式类型或初始化值
- 对象属性访问使用 `obj['key']` 而非 `obj.key`
- 数组赋值时类型必须匹配
PS D:\companyproject\mall-trae2> npm run build:app 2>&1
npm error Missing script: "build:app"
npm error
npm error To see a list of scripts, run:
npm error npm run
npm error A complete log of this run can be found in: C:\Users\Huawei\AppData\Local\npm-cache\_logs\2026-02-24T00_58_23_018Z-debug-0.log

View File

@@ -1,5 +1,5 @@
<template> <template>
<view class="favorites-page"> <scroll-view class="favorites-page" direction="vertical">
<view class="product-grid"> <view class="product-grid">
<view v-if="favorites.length === 0" class="empty-state"> <view v-if="favorites.length === 0" class="empty-state">
<text class="empty-icon">❤️</text> <text class="empty-icon">❤️</text>
@@ -26,7 +26,7 @@
</view> </view>
</view> </view>
</view> </view>
</view> </scroll-view>
</template> </template>
<script setup lang="uts"> <script setup lang="uts">
@@ -45,13 +45,9 @@ type Product = {
const favorites = ref<Product[]>([]) const favorites = ref<Product[]>([])
onMounted(() => {
loadFavorites()
})
const addToCart = async (product: Product) => { const addToCart = async (product: Product) => {
uni.showLoading({ title: '添加中' }) uni.showLoading({ title: '添加中' })
const success = await supabaseService.addToCart(product.id, 1) const success = await supabaseService.addToCart(product.id, 1, '')
uni.hideLoading() uni.hideLoading()
if (success) { if (success) {
uni.showToast({ title: '已添加到购物车', icon: 'success' }) uni.showToast({ title: '已添加到购物车', icon: 'success' })
@@ -63,13 +59,16 @@ const addToCart = async (product: Product) => {
const loadFavorites = async () => { const loadFavorites = async () => {
const res = await supabaseService.getFavorites() const res = await supabaseService.getFavorites()
// Map response
favorites.value = res.map((item: any): Product => { favorites.value = res.map((item: any): Product => {
let prod: any = null let prod: any | null = null
let itemObj: UTSJSONObject | null = null
if (item instanceof UTSJSONObject) { if (item instanceof UTSJSONObject) {
prod = item.get('ml_products') itemObj = item as UTSJSONObject
prod = itemObj.get('ml_products')
} else { } else {
prod = item['ml_products'] itemObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
prod = itemObj.get('ml_products')
} }
let image = '/static/default-product.png' let image = '/static/default-product.png'
@@ -79,46 +78,33 @@ const loadFavorites = async () => {
let sales = 0 let sales = 0
if (prod != null) { if (prod != null) {
let prodObj: UTSJSONObject
if (prod instanceof UTSJSONObject) { if (prod instanceof UTSJSONObject) {
id = prod.getString('id') || '' prodObj = prod as UTSJSONObject
name = prod.getString('name') || '未知商品'
price = prod.getNumber('base_price') || 0
image = prod.getString('main_image_url') || image
sales = prod.getNumber('sale_count') || 0
// 如果 main_image_url 为空,尝试解析 image_urls
if (image === '/static/default-product.png') {
const imgUrls = prod.getString('image_urls')
if (imgUrls) {
try {
const arr = JSON.parse(imgUrls)
if (Array.isArray(arr) && arr.length > 0) image = arr[0] as string
} catch(e) {}
}
}
} else { } else {
id = (prod['id'] as string) || '' prodObj = JSON.parse(JSON.stringify(prod)) as UTSJSONObject
name = (prod['name'] as string) || '未知商品' }
price = (prod['base_price'] as number) || 0
image = (prod['main_image_url'] as string) || image id = prodObj.getString('id') ?? ''
sales = (prod['sale_count'] as number) || 0 name = prodObj.getString('name') ?? '未知商品'
price = prodObj.getNumber('base_price') ?? 0
if (image === '/static/default-product.png') { image = prodObj.getString('main_image_url') ?? image
const imgUrls = prod['image_urls'] as string sales = prodObj.getNumber('sale_count') ?? 0
if (imgUrls) {
try { if (image === '/static/default-product.png') {
const arr = JSON.parse(imgUrls) const imgUrls = prodObj.getString('image_urls')
if (Array.isArray(arr) && arr.length > 0) image = arr[0] as string if (imgUrls != null) {
} catch(e) {} try {
} const arr = JSON.parse(imgUrls)
} if (Array.isArray(arr) && arr.length > 0) {
image = arr[0] as string
}
} catch(e) {}
}
} }
} else { } else {
// 如果没取到商品,尝试直接从 item 取 target_id if (itemObj != null) {
if (item instanceof UTSJSONObject) { id = itemObj.getString('target_id') ?? ''
id = item.getString('target_id') || ''
} else {
id = (item['target_id'] as string) || ''
} }
} }
@@ -130,7 +116,7 @@ const loadFavorites = async () => {
sales: sales, sales: sales,
shopId: '', shopId: '',
shopName: '' shopName: ''
} } as Product
}) })
} }
@@ -146,50 +132,53 @@ const goToDetail = (id: string) => {
}) })
} }
const removeFavorite = async (id: string) => { const removeFavorite = (id: string) => {
uni.showModal({ uni.showModal({
title: '取消收藏', title: '取消收藏',
content: '确定要取消收藏该商品吗?', content: '确定要取消收藏该商品吗?',
success: async (res) => { success: (res) => {
if (res.confirm) { if (res.confirm) {
// toggleFavorite 返回最新的状态true=已收藏false=未收藏 supabaseService.toggleFavorite(id).then((isStillFavorite) => {
const isStillFavorite = await supabaseService.toggleFavorite(id) if (!isStillFavorite) {
const index = favorites.value.findIndex((item): Boolean => {
if (!isStillFavorite) { return item.id === id
// 现在的状态是"未收藏",说明取消成功 })
// Remove from local list
const index = favorites.value.findIndex(item => item.id === id)
if (index !== -1) { if (index !== -1) {
favorites.value.splice(index, 1) favorites.value.splice(index, 1)
} }
uni.showToast({ uni.showToast({
title: '已取消收藏', title: '已取消收藏',
icon: 'none' icon: 'none'
}) })
} else { } else {
uni.showToast({ uni.showToast({
title: '取消失败', title: '取消失败',
icon: 'none' icon: 'none'
}) })
} }
})
} }
} }
}) })
} }
onMounted(() => {
loadFavorites()
})
</script> </script>
<style> <style>
.favorites-page { .favorites-page {
padding: 15px; padding: 15px;
background-color: #f5f5f5; background-color: #f5f5f5;
min-height: 100vh; flex: 1;
} }
.product-grid { .product-grid {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
gap: 15px; justify-content: space-between;
width: 100%; width: 100%;
} }
@@ -224,7 +213,7 @@ const removeFavorite = async (id: string) => {
} }
.product-item { .product-item {
width: calc(50% - 8px); /* Default Mobile: 2 items per row */ width: 48%; /* Default Mobile: 2 items per row */
background-color: white; background-color: white;
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
@@ -236,13 +225,13 @@ const removeFavorite = async (id: string) => {
/* PC/Tablet Responsive */ /* PC/Tablet Responsive */
@media (min-width: 768px) { @media (min-width: 768px) {
.product-item { .product-item {
width: calc(33.33% - 10px) !important; /* Tablet: 3 items (gap 15px roughly distributed) */ width: 31% !important; /* Tablet: 3 items (gap 15px roughly distributed) */
} }
} }
@media (min-width: 1024px) { @media (min-width: 1024px) {
.product-item { .product-item {
width: calc(16.66% - 13px) !important; /* PC: 6 items */ width: 15% !important; /* PC: 6 items */
} }
/* Center content on large screens */ /* Center content on large screens */
@@ -269,9 +258,8 @@ const removeFavorite = async (id: string) => {
font-size: 14px; font-size: 14px;
color: #333; color: #333;
margin-bottom: 6px; margin-bottom: 6px;
display: -webkit-box; text-overflow: ellipsis;
-webkit-line-clamp: 2; lines: 2;
-webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
height: 40px; height: 40px;
line-height: 20px; line-height: 20px;
@@ -300,16 +288,16 @@ const removeFavorite = async (id: string) => {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
gap: 8px;
} }
.cart-btn, .remove-btn { .cart-btn, .remove-btn {
width: 28px; width: 28px;
height: 28px; height: 28px;
border-radius: 50%; border-radius: 14px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin-left: 8px; /* Replacement for gap */
} }
.cart-btn { .cart-btn {
@@ -328,4 +316,4 @@ const removeFavorite = async (id: string) => {
.remove-icon { .remove-icon {
font-size: 14px; font-size: 14px;
} }
</style> </style>

View File

@@ -1,21 +1,14 @@
<!-- 足迹页面 --> <!-- 足迹页面 -->
<template> <template>
<view class="footprint-page"> <view class="footprint-page">
<!-- 顶部栏 -->
<view class="footprint-header"> <view class="footprint-header">
<!-- Title hidden as requested -->
<!-- <view class="header-title">
<text class="title-text">我的足迹</text>
</view> -->
<view v-if="footprints.length > 0" class="header-actions"> <view v-if="footprints.length > 0" class="header-actions">
<text class="action-btn" @click="toggleEditMode">{{ isEditMode ? '完成' : '编辑' }}</text> <text class="action-btn" @click="toggleEditMode">{{ isEditMode ? '完成' : '编辑' }}</text>
<text class="action-btn" @click="clearAll">清空</text> <text class="action-btn" @click="clearAll">清空</text>
</view> </view>
</view> </view>
<!-- 日期分组 -->
<scroll-view class="footprint-content" scroll-y @scrolltolower="loadMore"> <scroll-view class="footprint-content" scroll-y @scrolltolower="loadMore">
<!-- 空状态 -->
<view v-if="footprints.length === 0 && !isLoading" class="empty-footprints"> <view v-if="footprints.length === 0 && !isLoading" class="empty-footprints">
<text class="empty-icon">👣</text> <text class="empty-icon">👣</text>
<text class="empty-text">暂无浏览记录</text> <text class="empty-text">暂无浏览记录</text>
@@ -23,20 +16,19 @@
<button class="go-shopping-btn" @click="goShopping">去逛逛</button> <button class="go-shopping-btn" @click="goShopping">去逛逛</button>
</view> </view>
<!-- 按日期分组 --> <view v-for="(group, index) in groupedFootprints" :key="index" class="date-group">
<view v-for="(group, date) in groupedFootprints" :key="date" class="date-group">
<view class="group-header"> <view class="group-header">
<text class="group-date">{{ formatGroupDate(date) }}</text> <text class="group-date">{{ group.dateLabel }}</text>
<text v-if="isEditMode" class="group-select" @click="toggleGroupSelect(date)"> <text v-if="isEditMode" class="group-select" @click="toggleGroupSelect(index)">
{{ isGroupSelected(date) ? '取消全选' : '全选' }} {{ isGroupSelected(index) ? '取消全选' : '全选' }}
</text> </text>
</view> </view>
<view class="group-items"> <view class="group-items">
<view v-for="item in group" :key="item.id" class="footprint-item"> <view v-for="item in group.items" :key="item.id" class="footprint-item">
<view v-if="isEditMode" class="item-selector" @click="toggleSelect(item)"> <view v-if="isEditMode" class="item-selector" @click="toggleSelect(item)">
<view :class="['select-icon', { selected: item.selected }]"> <view :class="['select-icon', { selected: item.selected === true }]">
<text v-if="item.selected" class="icon-text">✓</text> <text v-if="item.selected === true" class="icon-text">✓</text>
</view> </view>
</view> </view>
<view class="item-content" @click="viewProduct(item)"> <view class="item-content" @click="viewProduct(item)">
@@ -46,13 +38,7 @@
<view class="product-bottom"> <view class="product-bottom">
<view class="product-price-row"> <view class="product-price-row">
<text class="current-price">¥{{ item.price }}</text> <text class="current-price">¥{{ item.price }}</text>
<!-- <text v-if="item.original_price && item.original_price > item.price"
class="original-price">¥{{ item.original_price }}</text> -->
</view> </view>
<!-- <view class="product-meta">
<text class="sales-text">已售{{ item.sales }}</text>
<text class="time-text">{{ formatTime(item.viewTime) }}</text>
</view> -->
</view> </view>
</view> </view>
</view> </view>
@@ -60,7 +46,6 @@
</view> </view>
</view> </view>
<!-- 加载更多 -->
<view v-if="isLoading" class="loading-more"> <view v-if="isLoading" class="loading-more">
<text class="loading-text">加载中...</text> <text class="loading-text">加载中...</text>
</view> </view>
@@ -69,7 +54,6 @@
</view> </view>
</scroll-view> </scroll-view>
<!-- 编辑操作栏 -->
<view v-if="isEditMode && footprints.length > 0" class="edit-bar"> <view v-if="isEditMode && footprints.length > 0" class="edit-bar">
<view class="select-all" @click="toggleSelectAll"> <view class="select-all" @click="toggleSelectAll">
<view :class="['all-select-icon', { selected: isAllSelected }]"> <view :class="['all-select-icon', { selected: isAllSelected }]">
@@ -92,151 +76,46 @@ type FootprintType = {
id: string id: string
name: string name: string
price: number price: number
original_price?: number original_price: number
image: string image: string
sales: number sales: number
shopId: string shopId: string
shopName: string shopName: string
viewTime: number viewTime: number
selected?: boolean selected: boolean
} }
const footprints = ref<Array<FootprintType>>([]) type FootprintGroup = {
dateLabel: string
dateKey: string
items: FootprintType[]
}
type FootprintSaveType = {
id: string
name: string
price: number
original_price: number
image: string
sales: number
shopId: string
shopName: string
viewTime: number
}
const footprints = ref<FootprintType[]>([])
const isEditMode = ref<boolean>(false) const isEditMode = ref<boolean>(false)
const isLoading = ref<boolean>(false) const isLoading = ref<boolean>(false)
const hasMore = ref<boolean>(false) const hasMore = ref<boolean>(false)
// 计算属性 const selectedCount = computed((): number => {
const selectedCount = computed(() => { return footprints.value.filter((item): Boolean => item.selected === true).length
return footprints.value.filter(item => item.selected).length
}) })
const isAllSelected = computed(() => { const isAllSelected = computed((): boolean => {
return footprints.value.length > 0 && footprints.value.every(item => item.selected) return footprints.value.length > 0 && footprints.value.every((item): Boolean => item.selected === true)
}) })
const groupedFootprints = computed(() => {
const groups: Record<string, FootprintType[]> = {}
footprints.value.forEach(item => {
const date = new Date(item.viewTime).toDateString()
if (!groups[date]) {
groups[date] = []
}
groups[date].push(item)
})
return groups
})
// 生命周期
onMounted(() => {
loadFootprints()
})
// 加载足迹数据
const loadFootprints = async (loadMore: boolean = false) => {
isLoading.value = true
try {
const remoteData = await supabaseService.getFootprints()
if (remoteData.length > 0) {
console.log('获取到远程足迹数据:', remoteData.length)
// 使用远程数据
footprints.value = remoteData.map((item: any): FootprintType => {
let id = ''
let name = ''
let price = 0
let original_price = 0
let image = ''
let sales = 0
let shopId = ''
let shopName = ''
let viewTime = 0
if (item instanceof UTSJSONObject) {
id = item.getString('id') || ''
name = item.getString('name') || ''
price = item.getNumber('price') || 0
original_price = item.getNumber('original_price') || 0
image = item.getString('image') || ''
sales = item.getNumber('sales') || 0
shopId = item.getString('shopId') || ''
shopName = item.getString('shopName') || ''
viewTime = item.getNumber('viewTime') || 0
} else {
id = (item['id'] as string) || ''
name = (item['name'] as string) || ''
price = (item['price'] as number) || 0
original_price = (item['original_price'] as number) || 0
image = (item['image'] as string) || ''
sales = (item['sales'] as number) || 0
shopId = (item['shopId'] as string) || ''
shopName = (item['shopName'] as string) || ''
viewTime = (item['viewTime'] as number) || 0
}
return {
id: id,
name: name,
price: price,
original_price: original_price,
image: image,
sales: sales,
shopId: shopId,
shopName: shopName,
viewTime: viewTime,
selected: false
} as FootprintType
})
// 更新本地缓存
const dataToSave = footprints.value.map(item => {
const { selected, ...rest } = item
return rest
})
uni.setStorageSync('footprints', JSON.stringify(dataToSave))
} else {
// 如果远程为空,尝试加载本地
const storedFootprints = uni.getStorageSync('footprints')
if (storedFootprints) {
try {
const data = JSON.parse(storedFootprints as string) as any[]
footprints.value = data.map(item => ({
...item,
selected: false
}))
} catch (e) {
console.error('Failed to parse footprints', e)
footprints.value = []
}
} else {
footprints.value = []
}
}
} catch (e) {
console.error('加载足迹失败', e)
// 失败时加载本地
const storedFootprints = uni.getStorageSync('footprints')
if (storedFootprints) {
try {
const data = JSON.parse(storedFootprints as string) as any[]
footprints.value = data.map(item => ({
...item,
selected: false
}))
} catch (err) {
footprints.value = []
}
}
}
isLoading.value = false
hasMore.value = false // 本地存储一次性加载完
}
// 格式化日期分组
const formatGroupDate = (dateStr: string): string => { const formatGroupDate = (dateStr: string): string => {
const date = new Date(dateStr) const date = new Date(dateStr)
const today = new Date() const today = new Date()
@@ -254,24 +133,43 @@ const formatGroupDate = (dateStr: string): string => {
} }
} }
// 格式化时间 const groupedFootprints = computed((): FootprintGroup[] => {
const formatTime = (timestamp: number): string => { const result: FootprintGroup[] = []
const date = new Date(timestamp)
const hours = date.getHours().toString().padStart(2, '0') for (let i = 0; i < footprints.value.length; i++) {
const minutes = date.getMinutes().toString().padStart(2, '0') const item = footprints.value[i]
return `${hours}:${minutes}` const dateKey = new Date(item.viewTime).toDateString()
}
let foundGroup: FootprintGroup | null = null
for (let j = 0; j < result.length; j++) {
if (result[j].dateKey === dateKey) {
foundGroup = result[j]
break
}
}
if (foundGroup != null) {
foundGroup.items.push(item)
} else {
const newGroup: FootprintGroup = {
dateLabel: formatGroupDate(dateKey),
dateKey: dateKey,
items: [item]
} as FootprintGroup
result.push(newGroup)
}
}
return result
})
// 切换编辑模式
const toggleEditMode = () => { const toggleEditMode = () => {
isEditMode.value = !isEditMode.value isEditMode.value = !isEditMode.value
// 重置选择状态 for (let i = 0; i < footprints.value.length; i++) {
footprints.value.forEach(item => { footprints.value[i].selected = false
item.selected = false }
})
} }
// 清空所有足迹
const clearAll = () => { const clearAll = () => {
if (footprints.value.length === 0) return if (footprints.value.length === 0) return
@@ -292,46 +190,41 @@ const clearAll = () => {
}) })
} }
// 切换选择状态
const toggleSelect = (item: FootprintType) => { const toggleSelect = (item: FootprintType) => {
item.selected = !item.selected item.selected = !(item.selected === true)
footprints.value = [...footprints.value] footprints.value = [...footprints.value]
} }
// 切换分组全选 const toggleGroupSelect = (groupIndex: number) => {
const toggleGroupSelect = (dateStr: string) => { const group = groupedFootprints.value[groupIndex]
const group = groupedFootprints.value[dateStr] if (group == null) return
if (!group) return
const isAllSelected = group.every(item => item.selected) const allSelected = group.items.every((item): Boolean => item.selected === true)
const newSelectedState = !isAllSelected const newSelectedState = !allSelected
group.forEach(item => { for (let i = 0; i < group.items.length; i++) {
item.selected = newSelectedState group.items[i].selected = newSelectedState
}) }
footprints.value = [...footprints.value] footprints.value = [...footprints.value]
} }
// 检查组是否全选 const isGroupSelected = (groupIndex: number): boolean => {
const isGroupSelected = (dateStr: string): boolean => { const group = groupedFootprints.value[groupIndex]
const group = groupedFootprints.value[dateStr] if (group == null || group.items.length === 0) return false
if (!group || group.length === 0) return false return group.items.every((item): Boolean => item.selected === true)
return group.every(item => item.selected)
} }
// 全选/取消全选
const toggleSelectAll = () => { const toggleSelectAll = () => {
const newSelectedState = !isAllSelected.value const newSelectedState = !isAllSelected.value
footprints.value.forEach(item => { for (let i = 0; i < footprints.value.length; i++) {
item.selected = newSelectedState footprints.value[i].selected = newSelectedState
}) }
footprints.value = [...footprints.value] footprints.value = [...footprints.value]
} }
// 删除选中项
const deleteSelected = () => { const deleteSelected = () => {
const selectedItems = footprints.value.filter(item => item.selected) const selectedItems = footprints.value.filter((item): Boolean => item.selected === true)
if (selectedItems.length === 0) { if (selectedItems.length === 0) {
uni.showToast({ uni.showToast({
title: '请选择要删除的记录', title: '请选择要删除的记录',
@@ -343,25 +236,29 @@ const deleteSelected = () => {
uni.showModal({ uni.showModal({
title: '确认删除', title: '确认删除',
content: `确定要删除选中的${selectedItems.length}条记录吗?`, content: `确定要删除选中的${selectedItems.length}条记录吗?`,
success: async (res) => { success: (res) => {
if (res.confirm) { if (res.confirm) {
uni.showLoading({ title: '删除中' }) uni.showLoading({ title: '删除中' })
// 远程删除
for (const item of selectedItems) {
await supabaseService.deleteFootprint(item.id)
}
uni.hideLoading() uni.hideLoading()
// 从列表移除 footprints.value = footprints.value.filter((item): Boolean => item.selected !== true)
footprints.value = footprints.value.filter(item => !item.selected)
// 保存回本地存储 const dataToSave: FootprintSaveType[] = []
const dataToSave = footprints.value.map(item => { for (let i = 0; i < footprints.value.length; i++) {
const { selected, ...rest } = item const item = footprints.value[i]
return rest dataToSave.push({
}) id: item.id,
name: item.name,
price: item.price,
original_price: item.original_price,
image: item.image,
sales: item.sales,
shopId: item.shopId,
shopName: item.shopName,
viewTime: item.viewTime
} as FootprintSaveType)
}
uni.setStorageSync('footprints', JSON.stringify(dataToSave)) uni.setStorageSync('footprints', JSON.stringify(dataToSave))
uni.showToast({ uni.showToast({
@@ -369,7 +266,6 @@ const deleteSelected = () => {
icon: 'success' icon: 'success'
}) })
// 如果删完了,退出编辑模式
if (footprints.value.length === 0) { if (footprints.value.length === 0) {
isEditMode.value = false isEditMode.value = false
} }
@@ -378,34 +274,124 @@ const deleteSelected = () => {
}) })
} }
// 查看商品
const viewProduct = (item: FootprintType) => { const viewProduct = (item: FootprintType) => {
if (isEditMode.value) return if (isEditMode.value) return
uni.navigateTo({ uni.navigateTo({
url: `/pages/mall/consumer/product-detail?productId=${item.id}&price=${item.price}&originalPrice=${item.original_price || ''}` url: `/pages/mall/consumer/product-detail?productId=${item.id}&price=${item.price}&originalPrice=${item.original_price}`
}) })
} }
// 加载更多
const loadMore = () => { const loadMore = () => {
// 本地存储模式下暂不需要加载更多逻辑
} }
// 去逛逛
const goShopping = () => { const goShopping = () => {
uni.switchTab({ uni.switchTab({
url: '/pages/mall/consumer/index' url: '/pages/mall/consumer/index'
}) })
} }
const parseFootprintItem = (item: any): FootprintType => {
let itemObj: UTSJSONObject
if (item instanceof UTSJSONObject) {
itemObj = item as UTSJSONObject
} else {
itemObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
}
return {
id: itemObj.getString('id') ?? '',
name: itemObj.getString('name') ?? '',
price: itemObj.getNumber('price') ?? 0,
original_price: itemObj.getNumber('original_price') ?? 0,
image: itemObj.getString('image') ?? '',
sales: itemObj.getNumber('sales') ?? 0,
shopId: itemObj.getString('shopId') ?? '',
shopName: itemObj.getString('shopName') ?? '',
viewTime: itemObj.getNumber('viewTime') ?? 0,
selected: false
} as FootprintType
}
const loadFootprints = async () => {
isLoading.value = true
try {
const remoteData = await supabaseService.getFootprints()
if (remoteData.length > 0) {
console.log('获取到远程足迹数据:', remoteData.length)
const newFootprints: FootprintType[] = []
for (let i = 0; i < remoteData.length; i++) {
newFootprints.push(parseFootprintItem(remoteData[i]))
}
footprints.value = newFootprints
const dataToSave: FootprintSaveType[] = []
for (let i = 0; i < footprints.value.length; i++) {
const item = footprints.value[i]
dataToSave.push({
id: item.id,
name: item.name,
price: item.price,
original_price: item.original_price,
image: item.image,
sales: item.sales,
shopId: item.shopId,
shopName: item.shopName,
viewTime: item.viewTime
} as FootprintSaveType)
}
uni.setStorageSync('footprints', JSON.stringify(dataToSave))
} else {
const storedFootprints = uni.getStorageSync('footprints')
if (storedFootprints != null) {
try {
const data = JSON.parse(storedFootprints as string) as any[]
const newFootprints: FootprintType[] = []
for (let i = 0; i < data.length; i++) {
newFootprints.push(parseFootprintItem(data[i]))
}
footprints.value = newFootprints
} catch (e) {
console.error('Failed to parse footprints', e)
footprints.value = []
}
} else {
footprints.value = []
}
}
} catch (e) {
console.error('加载足迹失败', e)
const storedFootprints = uni.getStorageSync('footprints')
if (storedFootprints != null) {
try {
const data = JSON.parse(storedFootprints as string) as any[]
const newFootprints: FootprintType[] = []
for (let i = 0; i < data.length; i++) {
newFootprints.push(parseFootprintItem(data[i]))
}
footprints.value = newFootprints
} catch (err) {
footprints.value = []
}
}
}
isLoading.value = false
hasMore.value = false
}
onMounted(() => {
loadFootprints()
})
</script> </script>
<style scoped> <style scoped>
.footprint-page { .footprint-page {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100vh; flex: 1;
background-color: #f5f5f5;
} }
.footprint-header { .footprint-header {
@@ -417,32 +403,20 @@ const goShopping = () => {
justify-content: space-between; justify-content: space-between;
} }
.header-title {
/* margin-left: 15px; */ /* Removed */
display: none;
}
.title-text {
font-size: 18px;
font-weight: bold;
color: #333333;
}
.header-actions { .header-actions {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 20px; flex: 1;
flex: 1; justify-content: flex-end;
justify-content: flex-end; /* Align Edit/Clear to the right */ align-items: center;
align-items: center; padding-right: 0;
padding-right: 0;
} }
.action-btn { .action-btn {
color: #007aff; color: #007aff;
font-size: 14px; font-size: 14px;
padding: 5px; padding: 5px;
margin-left: 20px; margin-left: 20px;
} }
.footprint-content { .footprint-content {
@@ -492,7 +466,6 @@ const goShopping = () => {
.group-header { .group-header {
padding: 15px 5px; padding: 15px 5px;
/* border-bottom: 1px solid #f5f5f5; */
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
@@ -514,28 +487,19 @@ const goShopping = () => {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
gap: 10px; justify-content: space-between;
} }
.footprint-item { .footprint-item {
display: flex; display: flex;
flex-direction: column; /* 垂直排列内容 */ flex-direction: column;
/* padding: 15px 0; */ /* Grid layout uses gap */
margin-bottom: 10px; margin-bottom: 10px;
border-bottom: none; border-bottom: none;
width: calc(50% - 5px); /* Mobile: 2 items per row */ width: 48%;
background-color: #fff; background-color: #fff;
/* border-radius: 8px; */ /* Optional card style */ position: relative;
/* overflow: hidden; */
position: relative; /* For absolute positioning of selector */
} }
/*
.footprint-item:last-child {
border-bottom: none;
}
*/
.item-selector { .item-selector {
position: absolute; position: absolute;
top: 5px; top: 5px;
@@ -543,7 +507,6 @@ const goShopping = () => {
z-index: 10; z-index: 10;
width: 30px; width: 30px;
height: 30px; height: 30px;
/* background-color: rgba(255,255,255,0.8); */
border-radius: 15px; border-radius: 15px;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -574,12 +537,12 @@ const goShopping = () => {
.item-content { .item-content {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; /* 垂直堆叠 */ flex-direction: column;
} }
.product-image { .product-image {
width: 100%; width: 100%;
height: 170px; /* Adjust height based on aspect ratio preference */ height: 170px;
border-radius: 5px; border-radius: 5px;
margin-right: 0; margin-right: 0;
margin-bottom: 8px; margin-bottom: 8px;
@@ -599,11 +562,10 @@ const goShopping = () => {
color: #333333; color: #333333;
line-height: 1.4; line-height: 1.4;
margin-bottom: 6px; margin-bottom: 6px;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden; overflow: hidden;
height: 40px; text-overflow: ellipsis;
lines: 2;
height: 40px;
} }
.product-bottom { .product-bottom {
@@ -615,8 +577,7 @@ const goShopping = () => {
.product-price-row { .product-price-row {
display: flex; display: flex;
align-items: baseline; align-items: flex-end;
margin-bottom: 0;
} }
.current-price { .current-price {
@@ -626,44 +587,21 @@ const goShopping = () => {
margin-right: 0; margin-right: 0;
} }
/* PC/Tablet Responsive */
@media (min-width: 768px) { @media (min-width: 768px) {
.footprint-item { .footprint-item {
width: calc(33.33% - 7px) !important; /* Tablet: 3 items */ width: 32% !important;
} }
} }
@media (min-width: 1024px) { @media (min-width: 1024px) {
.footprint-item { .footprint-item {
width: calc(16.66% - 9px) !important; /* PC: 6 items */ width: 16% !important;
} }
.footprint-content, .footprint-header { .footprint-content, .footprint-header {
max-width: 1200px; max-width: 1200px;
margin: 0 auto; margin: 0 auto;
} }
}
.original-price {
font-size: 12px;
color: #999999;
text-decoration: line-through;
}
.product-meta {
display: flex;
justify-content: space-between;
align-items: center;
}
.sales-text {
font-size: 12px;
color: #999999;
}
.time-text {
font-size: 12px;
color: #666666;
} }
.loading-more, .loading-more,
@@ -725,4 +663,4 @@ const goShopping = () => {
font-size: 14px; font-size: 14px;
font-weight: bold; font-weight: bold;
} }
</style> </style>

File diff suppressed because it is too large Load Diff

View File

@@ -674,7 +674,7 @@ const addToCart = async (product: any) => {
uni.showLoading({ title: '添加中...' }) uni.showLoading({ title: '添加中...' })
try { try {
// 尝试调用 Supabase 服务添加 // 尝试调用 Supabase 服务添加
const success = await supabaseService.addToCart(product.id, 1) const success = await supabaseService.addToCart(product.id, 1, '')
if (success) { if (success) {
uni.showToast({ uni.showToast({
title: '已添加到购物车', title: '已添加到购物车',
@@ -990,7 +990,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
.card-name { .card-name {
font-size: 15px; font-size: 15px;
font-weight: 600; font-weight: 700;
color: #333; color: #333;
margin-bottom: 4px; margin-bottom: 4px;
text-align: center; text-align: center;
@@ -1046,7 +1046,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
.news-image { .news-image {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: block; /* display: block; REMOVED for uniapp-x support */
} }
.news-overlay { .news-overlay {
@@ -1071,8 +1071,8 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
.news-caption { .news-caption {
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 700;
display: block; /* display: block; REMOVED for uniapp-x support */
line-height: 1.4; line-height: 1.4;
} }
@@ -1086,9 +1086,11 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
} }
.services-grid { .services-grid {
display: grid; display: flex;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); flex-direction: row;
gap: 20px; flex-wrap: wrap;
/* grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); REMOVED */
/* gap: 20px; REMOVED */
} }
.service-card { .service-card {
@@ -1098,7 +1100,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
padding: 20px; padding: 20px;
background: #f8f9fa; background: #f8f9fa;
border-radius: 12px; border-radius: 12px;
cursor: pointer; /* cursor: pointer; REMOVED for uniapp-x support */
transition: all 0.3s ease; transition: all 0.3s ease;
} }
@@ -1124,7 +1126,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
.service-name { .service-name {
font-size: 15px; font-size: 15px;
font-weight: 600; font-weight: 700;
color: #333; color: #333;
margin-bottom: 4px; margin-bottom: 4px;
} }
@@ -1154,13 +1156,14 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
.title-section { .title-section {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; /* gap: 8px; REMOVED */
width: 100%; width: 100%;
} }
.section-icon { .section-icon {
font-size: 20px; font-size: 20px;
color: #4CAF50; color: #4CAF50;
margin-right: 8px;
} }
.sort-tabs { .sort-tabs {
@@ -1180,9 +1183,9 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
padding: 8px 12px; /* 增加左右内边距 */ padding: 8px 12px; /* 增加左右内边距 */
border-radius: 20px; border-radius: 20px;
border: 1px solid #e0e0e0; border: 1px solid #e0e0e0;
cursor: pointer; /* cursor: pointer; REMOVED */
transition: all 0.2s ease; transition: all 0.2s ease;
white-space: nowrap; /* white-space: nowrap; REMOVED for uniapp-x support on view */
flex: 1; /* 均分宽度 */ flex: 1; /* 均分宽度 */
min-width: 70px; /* 设置最小宽度防止过窄 */ min-width: 70px; /* 设置最小宽度防止过窄 */
text-align: center; text-align: center;
@@ -1207,13 +1210,18 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
/* 产品网格 */ /* 产品网格 */
.products-grid { .products-grid {
display: grid; display: flex;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); flex-direction: row;
gap: 20px; flex-wrap: wrap;
/* grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); REMOVED */
/* gap: 20px; REMOVED */
margin-top: 20px; margin-top: 20px;
margin: 20px -1.5% 0 -1.5%;
} }
.product-card { .product-card {
width: 47%;
margin: 0 1.5% 20px 1.5%;
background: #f8f9fa; background: #f8f9fa;
border-radius: 12px; border-radius: 12px;
overflow: hidden; overflow: hidden;
@@ -1237,7 +1245,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
font-size: 11px; font-size: 11px;
padding: 4px 12px; padding: 4px 12px;
border-radius: 12px; border-radius: 12px;
font-weight: 600; font-weight: 700;
z-index: 2; z-index: 2;
} }
@@ -1254,10 +1262,10 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
.product-name { .product-name {
font-size: 15px; font-size: 15px;
font-weight: 600; font-weight: 700;
color: #333; color: #333;
margin-bottom: 4px; margin-bottom: 4px;
display: block; /* display: block; REMOVED */
line-height: 1.4; line-height: 1.4;
} }
@@ -1265,7 +1273,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
font-size: 13px; font-size: 13px;
color: #666; color: #666;
margin-bottom: 12px; margin-bottom: 12px;
display: block; /* display: block; REMOVED */
} }
.price-section { .price-section {
@@ -1361,9 +1369,11 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
} }
.family-grid { .family-grid {
display: grid; display: flex;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); flex-direction: row;
gap: 16px; flex-wrap: wrap;
/* grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); REMOVED */
/* gap: 16px; REMOVED */
margin-top: 20px; margin-top: 20px;
} }
@@ -1400,7 +1410,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
.family-name { .family-name {
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 700;
color: #333; color: #333;
margin-bottom: 4px; margin-bottom: 4px;
} }
@@ -1436,9 +1446,9 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
padding: 8px 12px; /* 增加左右内边距 */ padding: 8px 12px; /* 增加左右内边距 */
border-radius: 20px; border-radius: 20px;
border: 1px solid #e0e0e0; border: 1px solid #e0e0e0;
cursor: pointer; /* cursor: pointer; REMOVED */
transition: all 0.2s ease; transition: all 0.2s ease;
white-space: nowrap; /* white-space: nowrap; REMOVED */
flex: 1; /* 均分宽度 */ flex: 1; /* 均分宽度 */
min-width: 80px; /* 设置最小宽度防止过窄 */ min-width: 80px; /* 设置最小宽度防止过窄 */
text-align: center; text-align: center;
@@ -1462,13 +1472,18 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
} }
.recommend-grid { .recommend-grid {
display: grid; display: flex;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); flex-direction: row;
gap: 20px; flex-wrap: wrap;
/* grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); REMOVED */
/* gap: 20px; REMOVED */
margin-top: 20px; margin-top: 20px;
margin: 20px -1.5% 0 -1.5%;
} }
.recommend-product { .recommend-product {
width: 97%; /* Single col by default or adjust */
margin: 0 1.5% 20px 1.5%;
background: #f8f9fa; background: #f8f9fa;
border-radius: 12px; border-radius: 12px;
overflow: hidden; overflow: hidden;
@@ -1506,7 +1521,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
padding: 4px 10px; padding: 4px 10px;
border-radius: 10px; border-radius: 10px;
font-size: 11px; font-size: 11px;
font-weight: 600; font-weight: 700;
color: white; color: white;
} }
@@ -1524,7 +1539,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
.product-title { .product-title {
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 700;
color: #333; color: #333;
margin-bottom: 4px; margin-bottom: 4px;
display: block; display: block;
@@ -1558,7 +1573,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
} }
.rating-value { .rating-value {
font-weight: 600; font-weight: 700;
color: #333; color: #333;
} }
@@ -1622,7 +1637,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
.reminder-title { .reminder-title {
font-size: 15px; font-size: 15px;
font-weight: 600; font-weight: 700;
} }
.reminder-desc { .reminder-desc {
@@ -1661,15 +1676,17 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
height: 32px; height: 32px;
border: 3px solid #f0f0f0; border: 3px solid #f0f0f0;
border-top-color: #4CAF50; border-top-color: #4CAF50;
border-radius: 50%; border-radius: 16px;
animation: spin 1s linear infinite; /* animation: spin 1s linear infinite; REMOVED for uniapp-x support */
margin-bottom: 12px; margin-bottom: 12px;
} }
/*
@keyframes spin { @keyframes spin {
0% { transform: rotate(0deg); } 0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); } 100% { transform: rotate(360deg); }
} }
*/
.loading-text { .loading-text {
font-size: 14px; font-size: 14px;
@@ -1705,7 +1722,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
.search-box { .search-box {
padding: 8px 16px; /* 与分类页保持一致 */ padding: 8px 16px; /* 与分类页保持一致 */
max-width: 100%; /* max-width: 100%; REMOVED for uniapp-x support */
margin: 0; margin: 0;
} }
@@ -1783,9 +1800,16 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
.products-grid, .products-grid,
.recommend-grid { .recommend-grid {
grid-template-columns: repeat(2, 1fr); /* 手机端调整为双列显示 */ /* grid-template-columns: repeat(2, 1fr); REMOVED */
gap: 8px; /* 减小间距 */ /* gap: 8px; REMOVED */
margin: 20px -1% 0 -1%;
} }
.products-grid .product-card,
.recommend-grid .recommend-product {
width: 48%;
margin: 0 1% 8px 1%;
}
.product-info, .product-info,
.product-details { .product-details {
@@ -1858,7 +1882,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
.hot-products .price-value, .hot-products .price-value,
.smart-recommend .price-value { .smart-recommend .price-value {
font-size: 14px; /* 字体变小 */ font-size: 14px; /* 字体变小 */
font-weight: 600; font-weight: 700;
} }
.family-grid { .family-grid {

View File

@@ -70,17 +70,22 @@ const trackList = ref<TrackItem[]>([
onMounted(() => { onMounted(() => {
const pages = getCurrentPages() const pages = getCurrentPages()
const currentPage = pages[pages.length - 1] const currentPage = pages[pages.length - 1]
const options = currentPage.options as any const options = currentPage.options
if (options.orderId) { if (options != null) {
orderId.value = options.orderId const optionsObj = options as UTSJSONObject
// 这里可以根据orderId去请求真实的物流信息 const orderIdValue = optionsObj.getString('orderId')
if (orderIdValue != null) {
orderId.value = orderIdValue
// 这里可以根据orderId去请求真实的物流信息
}
} }
}) })
</script> </script>
<style scoped> <style scoped>
.logistics-page { .logistics-page {
min-height: 100vh; /* min-height: 100vh; */
flex: 1;
background-color: #f5f5f5; background-color: #f5f5f5;
padding-bottom: 20px; padding-bottom: 20px;
} }
@@ -158,10 +163,7 @@ onMounted(() => {
.dot { .dot {
width: 8px; width: 8px;
height: 8px; height: 8px;
border-radius: 50%; border-radius: 4px;
background-color: #ccc;
margin-top: 6px;
z-index: 1;
} }
.first .dot { .first .dot {

View File

@@ -86,7 +86,7 @@
mode="aspectFill" mode="aspectFill"
/> />
<view v-else class="message-icon-default" :style="{ backgroundColor: message.color }"> <view v-else class="message-icon-default" :style="{ backgroundColor: message.color }">
<text>{{ message.icon }}</text> <text class="message-icon-text">{{ message.icon }}</text>
</view> </view>
<view v-if="message.online" class="online-dot"></view> <view v-if="message.online" class="online-dot"></view>
</view> </view>
@@ -202,20 +202,53 @@
<view class="safe-area"></view> <view class="safe-area"></view>
</scroll-view> </scroll-view>
<!-- 底部固定按钮 --> <!-- 底部固定按钮 (Hidden) -->
<view class="floating-action"> <!-- <view class="floating-action">
<button class="action-button" @click="startNewChat"> <button class="action-button" @click="startNewChat">
<text class="button-icon">✏️</text> <text class="button-icon">✏️</text>
<text class="button-text">新建聊天</text> <text class="button-text">新建聊天</text>
</button> </button>
</view> </view> -->
</view> </view>
</template> </template>
<script setup lang="uts"> <script setup lang="uts">
import { ref, reactive, computed, onMounted } from 'vue' import { ref, reactive, computed, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app' import { onShow } from '@dcloudio/uni-app'
import { supabaseService, type Notification, type ChatMessage } from '@/utils/supabaseService.uts' import { supabaseService, type Notification, type ChatMessage, type ChatRoom } from '@/utils/supabaseService.uts'
// 定义消息项类型
type MessageItem = {
id: string,
title: string,
content: string,
time: string,
read: boolean,
type: string,
avatar: string | null,
important: boolean,
coupon: string,
expiry: string,
claimed: boolean,
order_no: string,
status: string,
statusText: string,
role: string,
lastMessage: string,
online: boolean,
unreadCount: number,
tags: string[],
icon: string,
color: string,
active: boolean
}
// 定义标签类型
type MessageTab = {
id: string,
name: string,
unread: number
}
// 响应式数据 // 响应式数据
const activeTab = ref<string>('service') const activeTab = ref<string>('service')
@@ -226,33 +259,22 @@ const statusBarHeight = ref(0)
const scrollTop = ref(0) const scrollTop = ref(0)
const scrollHeight = ref(0) const scrollHeight = ref(0)
// 初始化页面布局数据
const initPage = () => {
const systemInfo = uni.getSystemInfoSync()
statusBarHeight.value = systemInfo.statusBarHeight || 0
// 计算滚动区域高度:屏幕高度 - 状态栏 - 导航栏(44) - 标签栏(42)
const windowHeight = systemInfo.windowHeight
scrollHeight.value = windowHeight - statusBarHeight.value - 44 - 42
}
// 消息分类标签 // 消息分类标签
const messageTabs = reactive([ const messageTabs = reactive<MessageTab[]>([
{ id: 'service', name: '客服消息', unread: 5 }, { id: 'service', name: '客服消息', unread: 5 },
{ id: 'system', name: '系统通知', unread: 3 }, { id: 'system', name: '系统通知', unread: 3 },
{ id: 'order', name: '订单消息', unread: 2 }, { id: 'order', name: '订单消息', unread: 2 },
{ id: 'promo', name: '优惠活动', unread: 2 } { id: 'promo', name: '优惠活动', unread: 2 }
]) ])
// Mock 客服消息数据 // 消息数据
const serviceMessages = reactive<any[]>([]) const serviceMessages = reactive<MessageItem[]>([])
const systemMessages = reactive<any[]>([]) const systemMessages = reactive<MessageItem[]>([])
const orderMessages = reactive<any[]>([]) const orderMessages = reactive<MessageItem[]>([])
// Mock 优惠活动数据 const promoMessages = reactive<MessageItem[]>([])
const promoMessages = reactive<any[]>([])
// 计算当前显示的消息 // 计算当前显示的消息
const currentMessages = computed(() => { const currentMessages = computed<MessageItem[]>(() => {
switch (activeTab.value) { switch (activeTab.value) {
case 'system': return systemMessages case 'system': return systemMessages
case 'order': return orderMessages case 'order': return orderMessages
@@ -262,21 +284,9 @@ const currentMessages = computed(() => {
} }
}) })
// 生命周期
onMounted(() => {
console.log('Messages Page Mounted')
initPage()
// loadMessages() // 移至 onShow 调用
})
onShow(() => {
console.log('Messages Page Show')
loadMessages()
})
// 简单的日期格式化 // 简单的日期格式化
const formatTime = (isoString: string): string => { const formatTime = (isoString: string): string => {
if (!isoString) return '' if (isoString == '') return ''
try { try {
return isoString.split('T')[0] return isoString.split('T')[0]
} catch(e) { } catch(e) {
@@ -284,12 +294,56 @@ const formatTime = (isoString: string): string => {
} }
} }
// 加载消息 // 更新未读数量 - 必须在 loadMessages 之前定义
const updateUnreadCount = () => {
let totalUnread = 0
let serviceUnread = 0
serviceMessages.forEach((msg: MessageItem) => {
if (!msg.read) serviceUnread++
})
messageTabs[0].unread = serviceUnread
totalUnread += serviceUnread
let systemUnread = 0
systemMessages.forEach((msg: MessageItem) => {
if (!msg.read) systemUnread++
})
messageTabs[1].unread = systemUnread
totalUnread += systemUnread
let orderUnread = 0
orderMessages.forEach((msg: MessageItem) => {
if (!msg.read) orderUnread++
})
messageTabs[2].unread = orderUnread
totalUnread += orderUnread
let promoUnread = 0
promoMessages.forEach((msg: MessageItem) => {
if (!msg.read) promoUnread++
})
messageTabs[3].unread = promoUnread
totalUnread += promoUnread
unreadCount.value = totalUnread
}
// 初始化页面布局数据
const initPage = () => {
const systemInfo = uni.getSystemInfoSync()
statusBarHeight.value = systemInfo.statusBarHeight
const windowHeight = systemInfo.windowHeight
scrollHeight.value = windowHeight - statusBarHeight.value - 44 - 42
}
// 加载消息函数 - 必须在 updateUnreadCount 之后定义
const loadMessages = async () => { const loadMessages = async () => {
loading.value = true loading.value = true
try { try {
// 清空现有Mock数据 // 清空现有数据
serviceMessages.length = 0 serviceMessages.length = 0
systemMessages.length = 0 systemMessages.length = 0
orderMessages.length = 0 orderMessages.length = 0
@@ -299,17 +353,15 @@ const loadMessages = async () => {
const notes = await supabaseService.getUserNotifications() const notes = await supabaseService.getUserNotifications()
notes.forEach((note: Notification) => { notes.forEach((note: Notification) => {
// 这里使用 any 类型构建对象,以匹配 reactive 数组的结构 const item: MessageItem = {
const item = {
id: note.id, id: note.id,
title: note.title, title: note.title,
content: note.content, content: note.content,
time: formatTime(note.created_at || ''), time: formatTime(note.created_at ?? ''),
read: note.is_read, read: note.is_read,
type: note.type, // 'system', 'order', 'promotion' => 'promo' type: note.type,
// 默认填充字段以避免渲染报错
avatar: note.icon_url, avatar: note.icon_url,
important: note.type === 'system', // 简单逻辑 important: note.type === 'system',
coupon: '点击查看', coupon: '点击查看',
expiry: '', expiry: '',
claimed: false, claimed: false,
@@ -320,9 +372,10 @@ const loadMessages = async () => {
lastMessage: '', lastMessage: '',
online: false, online: false,
unreadCount: 0, unreadCount: 0,
tags: [], tags: [] as string[],
icon: '', icon: '',
color: '' color: '',
active: false
} }
if (note.type === 'system') { if (note.type === 'system') {
@@ -330,85 +383,44 @@ const loadMessages = async () => {
} else if (note.type === 'order') { } else if (note.type === 'order') {
orderMessages.push(item) orderMessages.push(item)
} else if (note.type === 'promotion') { } else if (note.type === 'promotion') {
// map type 'promotion' to 'promo' for tab
item.type = 'promo' item.type = 'promo'
promoMessages.push(item) promoMessages.push(item)
} }
}) })
// 2. 获取客服消息 (Chat) // 2. 获取客服消息 (Chat)
const chats = await supabaseService.getUserChatMessages() const rooms = await supabaseService.getChatRooms()
// console.log('Raw chats:', chats) rooms.forEach((room: ChatRoom) => {
const msgItem: MessageItem = {
if (chats.length > 0) { id: room.merchant_id,
const currentUserId = supabaseService.getCurrentUserId() title: room.shop_name,
const conversations = new Map<string, any>() role: '商家客服',
content: room.last_message ?? '暂无消息',
// 1. Group by conversation partner lastMessage: room.last_message ?? '暂无消息',
for (const msg of chats) { time: formatTime(room.last_message_at ?? ''),
const partnerId = (msg.sender_id == currentUserId) ? msg.receiver_id : msg.sender_id read: room.unread_count === 0,
type: 'service',
// Skip if partner is null/invalid avatar: room.shop_logo ?? '/static/icons/shop-default.png',
if (!partnerId) continue; online: true,
unreadCount: room.unread_count,
if (!conversations.has(partnerId)) { tags: [] as string[],
conversations.set(partnerId, { icon: '🏪',
partnerId: partnerId, color: '#FF9800',
lastMessage: msg, important: false,
unreadCount: 0 coupon: '',
}) expiry: '',
} claimed: false,
order_no: '',
const conv = conversations.get(partnerId) status: '',
// Since chats are likely sorted desc, the first one seen is the latest. statusText: '',
// Just count unread: if I am the receiver and it's not read active: false
if (msg.receiver_id == currentUserId && !msg.is_read) { }
conv.unreadCount++ serviceMessages.push(msgItem)
} })
}
console.log('Conversations found:', conversations.size)
// 2. Fetch shop details for each conversation
const convList = Array.from(conversations.values())
const promises = convList.map(async (conv) => {
const shop = await supabaseService.getShopByMerchantId(conv.partnerId)
const shopName = shop ? shop.shop_name : '未知商家'
const shopAvatar = (shop && shop.logo_url) ? shop.logo_url : '/static/icons/shop-default.png'
return {
id: conv.partnerId, // Use partnerId as the ID for navigation
title: shopName,
role: '商家客服',
content: conv.lastMessage.content,
lastMessage: conv.lastMessage.content,
time: formatTime(conv.lastMessage.created_at || ''),
read: conv.unreadCount === 0,
type: 'service',
avatar: shopAvatar,
online: true,
unreadCount: conv.unreadCount,
tags: shop ? ['官方认证'] : [],
icon: '🏪',
color: '#FF9800',
important: false,
coupon: '',
expiry: '',
claimed: false,
order_no: '',
status: '',
statusText: ''
}
})
const renderedMessages = await Promise.all(promises)
serviceMessages.push(...renderedMessages)
}
// 如果没有消息,为了演示效果(或者真的需要),可以保留一个默认的系统客服 // 如果没有消息,添加默认客服
if (serviceMessages.length === 0) { if (serviceMessages.length === 0) {
serviceMessages.push({ const defaultService: MessageItem = {
id: 'default_service', id: 'default_service',
title: '平台客服', title: '平台客服',
role: '智能助手', role: '智能助手',
@@ -429,8 +441,10 @@ const loadMessages = async () => {
claimed: false, claimed: false,
order_no: '', order_no: '',
status: '', status: '',
statusText: '' statusText: '',
}) active: false
}
serviceMessages.push(defaultService)
} }
} catch (e) { } catch (e) {
@@ -441,28 +455,16 @@ const loadMessages = async () => {
} }
} }
// 更新未读数量 // 生命周期钩子
const updateUnreadCount = () => { onMounted(() => {
let totalUnread = 0 console.log('Messages Page Mounted')
initPage()
const serviceUnread = serviceMessages.filter(msg => !msg.read).length })
messageTabs[0].unread = serviceUnread
totalUnread += serviceUnread onShow(() => {
console.log('Messages Page Show')
const systemUnread = systemMessages.filter(msg => !msg.read).length loadMessages()
messageTabs[1].unread = systemUnread })
totalUnread += systemUnread
const orderUnread = orderMessages.filter(msg => !msg.read).length
messageTabs[2].unread = orderUnread
totalUnread += orderUnread
const promoUnread = promoMessages.filter(msg => !msg.read).length
messageTabs[3].unread = promoUnread
totalUnread += promoUnread
unreadCount.value = totalUnread
}
// 切换标签 // 切换标签
const switchTab = (tabId: string) => { const switchTab = (tabId: string) => {
@@ -472,7 +474,7 @@ const switchTab = (tabId: string) => {
} }
// 开始与客服聊天 // 开始与客服聊天
const startChatWithService = (message: any) => { const startChatWithService = (message: MessageItem) => {
message.read = true message.read = true
message.unreadCount = 0 message.unreadCount = 0
updateUnreadCount() updateUnreadCount()
@@ -506,7 +508,7 @@ const startNewChat = () => {
} }
// 查看系统消息 // 查看系统消息
const viewSystemMessage = (message: any) => { const viewSystemMessage = (message: MessageItem) => {
message.read = true message.read = true
updateUnreadCount() updateUnreadCount()
uni.navigateTo({ uni.navigateTo({
@@ -515,7 +517,7 @@ const viewSystemMessage = (message: any) => {
} }
// 查看订单消息 // 查看订单消息
const viewOrderMessage = (message: any) => { const viewOrderMessage = (message: MessageItem) => {
message.read = true message.read = true
updateUnreadCount() updateUnreadCount()
uni.navigateTo({ uni.navigateTo({
@@ -524,7 +526,7 @@ const viewOrderMessage = (message: any) => {
} }
// 查看优惠活动 // 查看优惠活动
const viewPromoMessage = (message: any) => { const viewPromoMessage = (message: MessageItem) => {
message.read = true message.read = true
updateUnreadCount() updateUnreadCount()
uni.navigateTo({ uni.navigateTo({
@@ -533,7 +535,7 @@ const viewPromoMessage = (message: any) => {
} }
// 领取优惠券 // 领取优惠券
const claimCoupon = (message: any) => { const claimCoupon = (message: MessageItem) => {
if (message.claimed) { if (message.claimed) {
uni.showToast({ uni.showToast({
title: '您已领取该优惠券', title: '您已领取该优惠券',
@@ -544,13 +546,14 @@ const claimCoupon = (message: any) => {
message.claimed = true message.claimed = true
// 保存领取状态到本地存储,供个人页读取 // 保存领取状态到本地存储,供个人页读取
const claimedCouponsCount = uni.getStorageSync('claimedCoupons') || 0 const claimedCouponsCount = uni.getStorageSync('claimedCoupons')
uni.setStorageSync('claimedCoupons', (claimedCouponsCount as number) + 1) const count = (claimedCouponsCount != null) ? (claimedCouponsCount as number) : 0
uni.setStorageSync('claimedCoupons', count + 1)
// 保存详细的优惠券信息到 myCoupons 列表 // 保存详细的优惠券信息到 myCoupons 列表
const myCoupons = uni.getStorageSync('myCoupons') const myCoupons = uni.getStorageSync('myCoupons')
let couponsList: any[] = [] let couponsList: any[] = []
if (myCoupons) { if (myCoupons != null) {
try { try {
couponsList = JSON.parse(myCoupons as string) as any[] couponsList = JSON.parse(myCoupons as string) as any[]
} catch (e) { } catch (e) {
@@ -579,15 +582,23 @@ const clearAllUnread = () => {
content: '确定要标记所有消息为已读吗?', content: '确定要标记所有消息为已读吗?',
success: (res) => { success: (res) => {
if (res.confirm) { if (res.confirm) {
serviceMessages.forEach(msg => { serviceMessages.forEach((msg: MessageItem) => {
msg.read = true msg.read = true
msg.unreadCount = 0 msg.unreadCount = 0
}) })
systemMessages.forEach(msg => msg.read = true) systemMessages.forEach((msg: MessageItem) => {
orderMessages.forEach(msg => msg.read = true) msg.read = true
promoMessages.forEach(msg => msg.read = true) })
orderMessages.forEach((msg: MessageItem) => {
msg.read = true
})
promoMessages.forEach((msg: MessageItem) => {
msg.read = true
})
messageTabs.forEach(tab => tab.unread = 0) messageTabs.forEach((tab: MessageTab) => {
tab.unread = 0
})
unreadCount.value = 0 unreadCount.value = 0
uni.showToast({ uni.showToast({
@@ -617,7 +628,7 @@ const onRefresh = () => {
/* 页面结构优化 - 避免双滚动条 */ /* 页面结构优化 - 避免双滚动条 */
.messages-page { .messages-page {
width: 100%; width: 100%;
height: 100vh; height: 100%;
background-color: #f8fafc; background-color: #f8fafc;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -673,7 +684,7 @@ const onRefresh = () => {
background: rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.2);
padding: 4px 12px; padding: 4px 12px;
border-radius: 20px; border-radius: 20px;
cursor: pointer; /* cursor: pointer; removed for uniapp-x support */
transition: all 0.2s ease; transition: all 0.2s ease;
} }
@@ -689,7 +700,7 @@ const onRefresh = () => {
.action-text { .action-text {
font-size: 12px; font-size: 12px;
color: white; color: white;
font-weight: 500; font-weight: bold;
} }
/* 导航栏占位符 */ /* 导航栏占位符 */
@@ -716,7 +727,7 @@ const onRefresh = () => {
/* overflow-x: auto; 移除自动滚动,改为自适应宽度 */ /* overflow-x: auto; 移除自动滚动,改为自适应宽度 */
max-width: 1400px; max-width: 1400px;
margin: 0 auto; margin: 0 auto;
gap: 4px; /* 减小间距 */ /* gap: 4px; removed for uniapp-x support */
justify-content: space-between; /* 均匀分布 */ justify-content: space-between; /* 均匀分布 */
} }
@@ -726,6 +737,7 @@ const onRefresh = () => {
.tab-item { .tab-item {
padding: 0 4px; padding: 0 4px;
margin: 0 2px; /* replaced gap */
display: flex; /* 改为 flex 布局 */ display: flex; /* 改为 flex 布局 */
flex-direction: row; /* 关键:横向排列 文字和数字 */ flex-direction: row; /* 关键:横向排列 文字和数字 */
align-items: center; /* 垂直居中 */ align-items: center; /* 垂直居中 */
@@ -804,7 +816,7 @@ const onRefresh = () => {
font-size: 12px; font-size: 12px;
padding: 4px 10px; padding: 4px 10px;
border-radius: 12px; border-radius: 12px;
font-weight: 500; font-weight: bold;
} }
.service-status.online { .service-status.online {
@@ -820,9 +832,12 @@ const onRefresh = () => {
} }
.service-categories { .service-categories {
display: grid; display: flex;
grid-template-columns: repeat(2, 1fr); flex-direction: row;
gap: 12px; flex-wrap: wrap; /* allow wrapping to simulate grid */
/* grid-template-columns: repeat(2, 1fr); REMOVED */
/* gap: 12px; removed for uniapp-x support */
padding: 6px; /* compensated padding */
} }
.category-item { .category-item {
@@ -832,8 +847,10 @@ const onRefresh = () => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
cursor: pointer; /* cursor: pointer; removed for uniapp-x support */
transition: all 0.3s ease; transition: all 0.3s ease;
margin: 1%; /* replaced gap */
width: 48%; /* 2 columns */
} }
.category-item:hover { .category-item:hover {
@@ -850,7 +867,7 @@ const onRefresh = () => {
.category-name { .category-name {
font-size: 13px; font-size: 13px;
color: #333; color: #333;
font-weight: 500; font-weight: bold;
} }
/* 消息项 */ /* 消息项 */
@@ -867,7 +884,7 @@ const onRefresh = () => {
align-items: flex-start; align-items: flex-start;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease; transition: all 0.3s ease;
cursor: pointer; /* cursor: pointer; removed for uniapp-x support */
} }
.message-item:hover { .message-item:hover {
@@ -906,7 +923,7 @@ const onRefresh = () => {
justify-content: center; justify-content: center;
} }
.message-icon-default text { .message-icon-text {
font-size: 24px; font-size: 24px;
color: white; color: white;
} }
@@ -950,7 +967,7 @@ const onRefresh = () => {
font-size: 16px; font-size: 16px;
color: #333; color: #333;
font-weight: bold; font-weight: bold;
display: block; /* display: block; REMOVED for uniapp-x support */
margin-bottom: 4px; margin-bottom: 4px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -963,20 +980,21 @@ const onRefresh = () => {
background: #E8F5E9; background: #E8F5E9;
padding: 2px 8px; padding: 2px 8px;
border-radius: 10px; border-radius: 10px;
display: inline-block; /* display: inline-block; REMOVED for uniapp-x support */
} }
.message-header-right { .message-header-right {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-end; align-items: flex-end;
gap: 4px; /* gap: 4px; removed for uniapp-x support */
} }
.message-time { .message-time {
font-size: 12px; font-size: 12px;
color: #999; color: #999;
white-space: nowrap; white-space: nowrap;
margin-bottom: 4px; /* replaced gap */
} }
.message-unread-count { .message-unread-count {
@@ -999,21 +1017,23 @@ const onRefresh = () => {
color: #666; color: #666;
line-height: 1.4; line-height: 1.4;
margin-bottom: 4px; margin-bottom: 4px;
display: -webkit-box; /* display: -webkit-box; REMOVED for uniapp-x support */
-webkit-line-clamp: 2; /* -webkit-line-clamp: 2; REMOVED for uniapp-x support */
-webkit-box-orient: vertical; /* -webkit-box-orient: vertical; REMOVED for uniapp-x support */
lines: 2; /* UTS text truncation */
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; /* Ensure standard CSS property is present */
} }
.last-message { .last-message {
font-size: 13px; font-size: 13px;
color: #999; color: #999;
display: block; /* display: block; REMOVED for uniapp-x support */
} }
.message-tags { .message-tags {
display: flex; display: flex;
gap: 6px; /* gap: 6px; removed for uniapp-x support */
flex-wrap: wrap; flex-wrap: wrap;
} }
@@ -1023,6 +1043,8 @@ const onRefresh = () => {
background: #f0f0f0; background: #f0f0f0;
padding: 3px 8px; padding: 3px 8px;
border-radius: 10px; border-radius: 10px;
margin-right: 6px; /* replaced gap */
margin-bottom: 4px; /* for wrapping */
} }
.order-info { .order-info {
@@ -1031,17 +1053,19 @@ const onRefresh = () => {
background-color: #E8F5E9; background-color: #E8F5E9;
padding: 4px 10px; padding: 4px 10px;
border-radius: 4px; border-radius: 4px;
display: inline-block; /* display: inline-block; REMOVED for uniapp-x support */
margin-top: 8px; margin-top: 8px;
align-self: flex-start; /* Ensure it doesn't stretch */
} }
.order-status { .order-status {
display: inline-block; /* display: inline-block; REMOVED for uniapp-x support */
font-size: 12px; font-size: 12px;
padding: 4px 10px; padding: 4px 10px;
border-radius: 12px; border-radius: 12px;
margin-top: 8px; margin-top: 8px;
margin-left: 8px; margin-left: 8px;
align-self: flex-start; /* Ensure it doesn't stretch */
} }
.order-status.shipping { .order-status.shipping {
@@ -1060,13 +1084,14 @@ const onRefresh = () => {
} }
.important-tag { .important-tag {
display: inline-block; /* display: inline-block; REMOVED for uniapp-x support */
background-color: #FF5722; background-color: #FF5722;
color: white; color: white;
font-size: 11px; font-size: 11px;
padding: 3px 8px; padding: 3px 8px;
border-radius: 10px; border-radius: 10px;
margin-top: 8px; margin-top: 8px;
align-self: flex-start; /* Ensure it doesn't stretch */
} }
.coupon-info { .coupon-info {
@@ -1080,7 +1105,7 @@ const onRefresh = () => {
.coupon-text { .coupon-text {
font-size: 14px; font-size: 14px;
font-weight: bold; font-weight: bold;
display: block; /* display: block; REMOVED for uniapp-x support */
margin-bottom: 4px; margin-bottom: 4px;
} }
@@ -1107,7 +1132,7 @@ const onRefresh = () => {
margin: 15px; margin: 15px;
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
gap: 10px; /* gap: 10px; removed for uniapp-x support */
} }
.tip-icon { .tip-icon {
@@ -1115,6 +1140,7 @@ const onRefresh = () => {
color: #FF9800; color: #FF9800;
flex-shrink: 0; flex-shrink: 0;
margin-top: 2px; margin-top: 2px;
margin-right: 10px; /* replaced gap */
} }
.tip-text { .tip-text {
@@ -1167,7 +1193,7 @@ const onRefresh = () => {
display: flex; display: flex;
align-items: center; align-items: center;
box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3); box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3);
font-weight: 500; font-weight: bold;
} }
.button-icon { .button-icon {
@@ -1191,8 +1217,12 @@ const onRefresh = () => {
} }
.service-categories { .service-categories {
grid-template-columns: 1fr; /* grid-template-columns: 1fr; REMOVED */
} }
.category-item {
width: 100%; /* 1 column */
}
} }
@media screen and (min-width: 415px) { @media screen and (min-width: 415px) {
@@ -1206,7 +1236,7 @@ const onRefresh = () => {
border-radius: 30px; border-radius: 30px;
} }
.message-icon-default text { .message-icon-text {
font-size: 28px; font-size: 28px;
} }
@@ -1215,9 +1245,16 @@ const onRefresh = () => {
} }
.service-categories { .service-categories {
grid-template-columns: repeat(4, 1fr); /* grid-template-columns: repeat(4, 1fr); REMOVED */
display: flex;
flex-direction: row;
flex-wrap: wrap;
} }
.service-categories .category-item {
width: 25%;
}
/* 平板和桌面端优化标签栏显示 */ /* 平板和桌面端优化标签栏显示 */
.message-tabs { .message-tabs {
justify-content: flex-start; /* 左对齐 */ justify-content: flex-start; /* 左对齐 */

View File

@@ -664,7 +664,7 @@ const onRefresh = () => {
/* overflow-x: auto; 移除自动滚动,改为自适应宽度 */ /* overflow-x: auto; 移除自动滚动,改为自适应宽度 */
max-width: 1400px; max-width: 1400px;
margin: 0 auto; margin: 0 auto;
gap: 4px; /* 减小间距 */ /* gap: 4px; removed for uniapp-x support */
justify-content: space-between; /* 均匀分布 */ justify-content: space-between; /* 均匀分布 */
} }
@@ -674,6 +674,7 @@ const onRefresh = () => {
.tab-item { .tab-item {
padding: 0 4px; padding: 0 4px;
margin: 0 2px;
display: flex; /* 改为 flex 布局 */ display: flex; /* 改为 flex 布局 */
flex-direction: row; /* 关键:横向排列 文字和数字 */ flex-direction: row; /* 关键:横向排列 文字和数字 */
align-items: center; /* 垂直居中 */ align-items: center; /* 垂直居中 */

View File

@@ -14,7 +14,7 @@
</view> </view>
<!-- 配送信息 --> <!-- 配送信息 -->
<view v-if="order.order_status >= 2" class="delivery-info card"> <view v-if="order != null && (order?.order_status ?? 0) >= 2" class="delivery-info card">
<view class="delivery-header"> <view class="delivery-header">
<text class="section-title">配送信息</text> <text class="section-title">配送信息</text>
</view> </view>
@@ -22,17 +22,17 @@
<view class="address-icon">📍</view> <view class="address-icon">📍</view>
<view class="address-content"> <view class="address-content">
<view class="address-user"> <view class="address-user">
<text class="recipient">{{ deliveryAddress.name }}</text> <text class="recipient">{{ deliveryAddress?.name ?? '' }}</text>
<text class="phone">{{ deliveryAddress.phone }}</text> <text class="phone">{{ deliveryAddress?.phone ?? '' }}</text>
</view> </view>
<text class="address-detail">{{ getFullAddress(deliveryAddress) }}</text> <text class="address-detail">{{ getFullAddress(deliveryAddress as any) }}</text>
</view> </view>
</view> </view>
<!-- 如果有物流信息显示 --> <!-- 如果有物流信息显示 -->
<view v-if="deliveryInfo.tracking_no" class="courier-info"> <view v-if="deliveryInfo != null && deliveryInfo?.tracking_no != ''" class="courier-info">
<text class="courier-label">物流单号:</text> <text class="courier-label">物流单号:</text>
<text class="courier-value">{{ deliveryInfo.tracking_no }}</text> <text class="courier-value">{{ deliveryInfo?.tracking_no ?? '' }}</text>
<text class="copy-btn" @click="copyText(deliveryInfo.tracking_no)">复制</text> <text class="copy-btn" @click="copyText(deliveryInfo?.tracking_no ?? '')">复制</text>
</view> </view>
</view> </view>
@@ -44,7 +44,7 @@
<text class="arrow-right">></text> <text class="arrow-right">></text>
</view> </view>
<view v-for="item in orderItems" :key="item.id" class="product-item" @click="goToProduct(item.product_id)"> <view v-for="item in orderItems" :key="item.id" class="product-item" @click="goToProduct(item.product_id)">
<image :src="item.image_url || '/static/default-product.png'" class="product-image" mode="aspectFill"/> <image :src="item.image_url != null && item.image_url != '' ? item.image_url : '/static/default-product.png'" class="product-image" mode="aspectFill"/>
<view class="product-info"> <view class="product-info">
<text class="product-name">{{ item.product_name }}</text> <text class="product-name">{{ item.product_name }}</text>
<text v-if="item.specifications" class="product-spec">{{ getSpecText(item.specifications) }}</text> <text v-if="item.specifications" class="product-spec">{{ getSpecText(item.specifications) }}</text>
@@ -57,56 +57,56 @@
</view> </view>
<!-- 订单信息 --> <!-- 订单信息 -->
<view class="order-info card"> <view class="order-info card" v-if="order != null">
<view class="info-row"> <view class="info-row">
<text class="info-label">订单编号</text> <text class="info-label">订单编号</text>
<text class="info-value copyable" @click="copyText(order.order_no)">{{ order.order_no }} <text class="copy-icon">📄</text></text> <text class="info-value copyable" @click="copyText(order?.order_no ?? '')">{{ order?.order_no ?? '' }} <text class="copy-icon">📄</text></text>
</view> </view>
<view class="info-row"> <view class="info-row">
<text class="info-label">下单时间</text> <text class="info-label">下单时间</text>
<text class="info-value">{{ formatTime(order.created_at) }}</text> <text class="info-value">{{ formatTime(order?.created_at ?? '') }}</text>
</view> </view>
<view class="info-row" v-if="order.payment_method"> <view class="info-row" v-if="order?.payment_method != null && order?.payment_method != ''">
<text class="info-label">支付方式</text> <text class="info-label">支付方式</text>
<text class="info-value">{{ getPaymentMethodText(order.payment_method) }}</text> <text class="info-value">{{ getPaymentMethodText(order?.payment_method as any) }}</text>
</view> </view>
<view class="info-row" v-if="order.paid_at"> <view class="info-row" v-if="order?.paid_at != null && order?.paid_at != ''">
<text class="info-label">支付时间</text> <text class="info-label">支付时间</text>
<text class="info-value">{{ formatTime(order.paid_at) }}</text> <text class="info-value">{{ formatTime(order?.paid_at ?? '') }}</text>
</view> </view>
<view class="info-row" v-if="order.shipped_at"> <view class="info-row" v-if="order?.shipped_at != null && order?.shipped_at != ''">
<text class="info-label">发货时间</text> <text class="info-label">发货时间</text>
<text class="info-value">{{ formatTime(order.shipped_at) }}</text> <text class="info-value">{{ formatTime(order?.shipped_at ?? '') }}</text>
</view> </view>
<view class="info-row" v-if="order.completed_at"> <view class="info-row" v-if="order?.completed_at != null && order?.completed_at != ''">
<text class="info-label">完成时间</text> <text class="info-label">完成时间</text>
<text class="info-value">{{ formatTime(order.completed_at) }}</text> <text class="info-value">{{ formatTime(order?.completed_at ?? '') }}</text>
</view> </view>
</view> </view>
<!-- 费用明细 --> <!-- 费用明细 -->
<view class="cost-detail card"> <view class="cost-detail card" v-if="order != null">
<view class="cost-row"> <view class="cost-row">
<text class="cost-label">商品总额</text> <text class="cost-label">商品总额</text>
<text class="cost-value">¥{{ order.product_amount }}</text> <text class="cost-value">¥{{ order?.product_amount ?? 0 }}</text>
</view> </view>
<view class="cost-row"> <view class="cost-row">
<text class="cost-label">运费</text> <text class="cost-label">运费</text>
<text class="cost-value">+¥{{ order.shipping_fee || 0 }}</text> <text class="cost-value">+¥{{ order?.shipping_fee != null ? order?.shipping_fee : 0 }}</text>
</view> </view>
<view class="cost-row" v-if="order.discount_amount > 0"> <view class="cost-row" v-if="(order?.discount_amount ?? 0) > 0">
<text class="cost-label">优惠金额</text> <text class="cost-label">优惠金额</text>
<text class="cost-value">-¥{{ order.discount_amount }}</text> <text class="cost-value">-¥{{ order?.discount_amount ?? 0 }}</text>
</view> </view>
<view class="cost-row total"> <view class="cost-row total">
<text class="cost-label">实付金额</text> <text class="cost-label">实付金额</text>
<text class="cost-value price">¥{{ order.total_amount }}</text> <text class="cost-value price">¥{{ order?.total_amount ?? 0 }}</text>
</view> </view>
</view> </view>
</scroll-view> </scroll-view>
<!-- 底部操作 --> <!-- 底部操作 -->
<view class="bottom-actions"> <view class="bottom-actions" v-if="order != null">
<view class="action-left"> <view class="action-left">
<view class="service-btn" @click="contactService"> <view class="service-btn" @click="contactService">
<text class="service-icon">🎧</text> <text class="service-icon">🎧</text>
@@ -114,20 +114,20 @@
</view> </view>
</view> </view>
<view class="action-right"> <view class="action-right">
<button v-if="order.order_status === 1" class="btn primary" @click="payOrder">立即支付</button> <button v-if="order?.order_status === 1" class="btn primary" @click="payOrder">立即支付</button>
<button v-if="order.order_status === 1" class="btn" @click="cancelOrder">取消订单</button> <button v-if="order?.order_status === 1" class="btn" @click="cancelOrder">取消订单</button>
<button v-if="order.order_status === 2" class="btn" @click="remindDelivery">提醒发货</button> <button v-if="order?.order_status === 2" class="btn" @click="remindDelivery">提醒发货</button>
<button v-if="order.order_status === 2" class="btn" @click="applyRefund">申请退款</button> <button v-if="order?.order_status === 2" class="btn" @click="applyRefund">申请退款</button>
<button v-if="order.order_status === 3" class="btn primary" @click="confirmReceive">确认收货</button> <button v-if="order?.order_status === 3" class="btn primary" @click="confirmReceive">确认收货</button>
<button v-if="order.order_status === 3" class="btn" @click="viewLogistics">查看物流</button> <button v-if="order?.order_status === 3" class="btn" @click="viewLogistics">查看物流</button>
<button v-if="order.order_status === 4" class="btn primary" @click="goToReview">评价</button> <button v-if="order?.order_status === 4" class="btn primary" @click="goToReview">评价</button>
<button v-if="order.order_status === 4" class="btn" @click="rePurchase">再次购买</button> <button v-if="order?.order_status === 4" class="btn" @click="rePurchase">再次购买</button>
<button v-if="order.order_status === 4" class="btn" @click="applyAfterSales">申请售后</button> <button v-if="order?.order_status === 4" class="btn" @click="applyAfterSales">申请售后</button>
<button v-if="order.order_status === 5" class="btn" @click="rePurchase">重新购买</button> <button v-if="order?.order_status === 5" class="btn" @click="rePurchase">重新购买</button>
</view> </view>
</view> </view>
</view> </view>
@@ -139,60 +139,57 @@ import { onLoad } from '@dcloudio/uni-app'
import { supabaseService } from '@/utils/supabaseService.uts' import { supabaseService } from '@/utils/supabaseService.uts'
import supa from '@/components/supadb/aksupainstance.uts' import supa from '@/components/supadb/aksupainstance.uts'
// 定义订单类型
type OrderType = {
order_no: string,
order_status: number,
total_amount: number,
product_amount: number,
shipping_fee: number,
discount_amount: number,
payment_method: string,
created_at: string,
paid_at: string,
shipped_at: string,
completed_at: string,
merchant_id: string,
shipping_address: any
}
type OrderItemType = {
id: string,
product_id: string,
product_name: string,
image_url: string,
price: number,
quantity: number,
specifications: any
}
type AddressType = {
name: string,
phone: string,
province: string,
city: string,
district: string,
detail: string,
address: string
}
type DeliveryInfoType = {
tracking_no: string
}
const orderId = ref('') const orderId = ref('')
const order = ref<any>({}) const order = ref<OrderType | null>(null)
const orderItems = ref<any[]>([]) const orderItems = ref<OrderItemType[]>([])
const shopName = ref('店铺名称') const shopName = ref('店铺名称')
const deliveryAddress = ref<any>({}) const deliveryAddress = ref<AddressType | null>(null)
const deliveryInfo = ref<any>({}) const deliveryInfo = ref<DeliveryInfoType | null>(null)
onLoad((options) => { // 辅助函数 - 必须在调用前定义
if (options['id']) { const getStatusText = (): string => {
orderId.value = options['id'] as string const status = order.value?.order_status ?? 0
loadOrderDetail()
} else if (options['orderId']) {
orderId.value = options['orderId'] as string
loadOrderDetail()
}
})
const loadOrderDetail = async () => {
uni.showLoading({ title: '加载中' })
try {
const data = await supabaseService.getOrderDetail(orderId.value)
if (data) {
order.value = data
orderItems.value = data.ml_order_items || []
deliveryAddress.value = data.shipping_address || {}
// 获取店铺信息
if (data.merchant_id) {
loadShopInfo(data.merchant_id)
}
} else {
uni.showToast({ title: '订单不存在', icon: 'none' })
}
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
uni.hideLoading()
}
}
const loadShopInfo = async (merchantId: string) => {
const { data } = await supa
.from('ml_shops')
.select('shop_name')
.eq('merchant_id', merchantId)
.single()
if (data) {
shopName.value = data['shop_name'] as string
}
}
// 辅助函数
const getStatusText = () => {
const status = order.value.order_status
if (status == 1) return '待付款' if (status == 1) return '待付款'
if (status == 2) return '待发货' if (status == 2) return '待发货'
if (status == 3) return '待收货' if (status == 3) return '待收货'
@@ -203,8 +200,8 @@ const getStatusText = () => {
return '未知状态' return '未知状态'
} }
const getStatusDesc = () => { const getStatusDesc = (): string => {
const status = order.value.order_status const status = order.value?.order_status ?? 0
if (status == 1) return '请尽快完成支付' if (status == 1) return '请尽快完成支付'
if (status == 2) return '商家正在打包商品' if (status == 2) return '商家正在打包商品'
if (status == 3) return '商品正在赶往您的地址' if (status == 3) return '商品正在赶往您的地址'
@@ -215,8 +212,8 @@ const getStatusDesc = () => {
return '' return ''
} }
const getStatusIcon = () => { const getStatusIcon = (): string => {
const status = order.value.order_status const status = order.value?.order_status ?? 0
if (status === 1) return '💳' if (status === 1) return '💳'
if (status === 2) return '📦' if (status === 2) return '📦'
if (status === 3) return '🚚' if (status === 3) return '🚚'
@@ -224,43 +221,85 @@ const getStatusIcon = () => {
return '📝' return '📝'
} }
const getStatusClass = () => { const getStatusClass = (): string => {
const status = order.value.order_status const status = order.value?.order_status ?? 0
return `status-${status}` return `status-${status}`
} }
const getFullAddress = (addr: any) => { const getFullAddress = (addr: any): string => {
if (!addr) return '' if (addr == null) return ''
// 兼容简单的字符串地址和对象地址 // 兼容简单的字符串地址和对象地址
if (typeof addr === 'string') return addr if (typeof addr === 'string') return addr
if (addr.address) return addr.address const addrObj = addr as Record<string, any>
return (addr.province || '') + (addr.city || '') + (addr.district || '') + (addr.detail || addr.address_detail || '') if (addrObj['address'] != null) return addrObj['address'] as string
return ((addrObj['province'] as string) ?? '') + ((addrObj['city'] as string) ?? '') + ((addrObj['district'] as string) ?? '') + ((addrObj['detail'] as string) ?? (addrObj['address_detail'] as string) ?? '')
} }
const getSpecText = (specs: any) => { const getSpecText = (specs: any): string => {
if (!specs) return '' if (specs == null) return ''
if (typeof specs === 'string') return specs if (typeof specs === 'string') return specs
return Object.keys(specs).map(k => `${k}:${specs[k]}`).join(' ') // 简化处理:直接返回字符串形式
return JSON.stringify(specs)
} }
const formatTime = (iso: string) => { const formatTime = (iso: string): string => {
if (!iso) return '' if (iso == '') return ''
const d = new Date(iso) const d = new Date(iso)
return `${d.getFullYear()}-${d.getMonth()+1}-${d.getDate()} ${d.getHours()}:${d.getMinutes()}` return `${d.getFullYear()}-${d.getMonth()+1}-${d.getDate()} ${d.getHours()}:${d.getMinutes()}`
} }
const getPaymentMethodText = (method: any) => { const getPaymentMethodText = (method: any): string => {
return '在线支付' return '在线支付'
} }
const copyText = (text: string) => { const copyText = (text: string) => {
if(!text) return if(text == '') return
uni.setClipboardData({ uni.setClipboardData({
data: text, data: text,
success: () => uni.showToast({ title: '已复制' }) success: () => uni.showToast({ title: '已复制' })
}) })
} }
const loadShopInfo = async (merchantId: string) => {
const result = await supa
.from('ml_shops')
.select('shop_name')
.eq('merchant_id', merchantId)
.single()
const resultObj = result as Record<string, any>
const resultData = resultObj['data']
if (resultData != null) {
const dataObj = resultData as Record<string, any>
shopName.value = dataObj['shop_name'] as string
}
}
const loadOrderDetail = async () => {
uni.showLoading({ title: '加载中' })
try {
const data = await supabaseService.getOrderDetail(orderId.value)
if (data != null) {
const dataObj = data as Record<string, any>
order.value = data as OrderType
const items = dataObj['ml_order_items']
orderItems.value = items != null ? (items as OrderItemType[]) : []
deliveryAddress.value = dataObj['shipping_address'] as AddressType
// 获取店铺信息
const merchantId = dataObj['merchant_id'] as string
if (merchantId != null && merchantId != '') {
loadShopInfo(merchantId)
}
} else {
uni.showToast({ title: '订单不存在', icon: 'none' })
}
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
uni.hideLoading()
}
}
// 动作函数 // 动作函数
const contactService = () => { const contactService = () => {
uni.showActionSheet({ uni.showActionSheet({
@@ -278,22 +317,31 @@ const contactService = () => {
} }
const payOrder = () => { const payOrder = () => {
const totalAmount = order.value?.total_amount ?? 0
uni.navigateTo({ uni.navigateTo({
url: `/pages/mall/consumer/payment?orderId=${orderId.value}&amount=${order.value.total_amount}` url: `/pages/mall/consumer/payment?orderId=${orderId.value}&amount=${totalAmount}`
}) })
} }
const doCancelOrder = async () => {
const result = await supa.from('ml_orders').update({ order_status: 5 }).eq('id', orderId.value)
const resultObj = result as Record<string, any>
const resultError = resultObj['error']
if(resultError == null) {
if (order.value != null) {
order.value.order_status = 5
}
uni.showToast({ title: '订单已取消' })
}
}
const cancelOrder = () => { const cancelOrder = () => {
uni.showModal({ uni.showModal({
title: '提示', title: '提示',
content: '确定要取消订单吗?', content: '确定要取消订单吗?',
success: async (res) => { success: (res) => {
if (res.confirm) { if (res.confirm) {
const { error } = await supa.from('ml_orders').update({ order_status: 5 }).eq('id', orderId.value) doCancelOrder()
if(!error) {
order.value.order_status = 5
uni.showToast({ title: '订单已取消' })
}
} }
} }
}) })
@@ -307,32 +355,39 @@ const viewLogistics = () => {
uni.navigateTo({ url: `/pages/mall/consumer/logistics?orderId=${orderId.value}` }) uni.navigateTo({ url: `/pages/mall/consumer/logistics?orderId=${orderId.value}` })
} }
const confirmReceive = async () => { const goToReview = () => {
uni.navigateTo({ url: `/pages/mall/consumer/review?orderId=${orderId.value}` })
}
const doConfirmReceive = async () => {
const result = await supabaseService.confirmReceipt(orderId.value)
if (result.success) {
if (order.value != null) {
order.value.order_status = 4
}
uni.showToast({ title: '收货成功' })
setTimeout(() => goToReview(), 1500)
} else {
uni.showToast({ title: result.error ?? '失败', icon: 'none' })
}
}
const confirmReceive = () => {
uni.showModal({ uni.showModal({
title: '确认收货', title: '确认收货',
content: '确保您已收到货物', content: '确保您已收到货物',
success: async (res) => { success: (res) => {
if (res.confirm) { if (res.confirm) {
const result = await supabaseService.confirmReceipt(orderId.value) doConfirmReceive()
if (result.success) {
order.value.order_status = 4
uni.showToast({ title: '收货成功' })
setTimeout(() => goToReview(), 1500)
} else {
uni.showToast({ title: result.error || '失败', icon: 'none' })
}
} }
} }
}) })
} }
const goToReview = () => {
uni.navigateTo({ url: `/pages/mall/consumer/review?orderId=${orderId.value}` })
}
const rePurchase = async () => { const rePurchase = async () => {
uni.showLoading({ title: '处理中' }) uni.showLoading({ title: '处理中' })
const success = await supabaseService.rePurchase(order.value) const orderData = order.value as any
const success = await supabaseService.rePurchase(orderData)
uni.hideLoading() uni.hideLoading()
if (success) { if (success) {
uni.showToast({ title: '已加入购物车' }) uni.showToast({ title: '已加入购物车' })
@@ -344,21 +399,27 @@ const rePurchase = async () => {
} }
} }
const doApplyRefund = async (reason: string) => {
const success = await supabaseService.applyRefund(orderId.value, reason)
if (success) {
if (order.value != null) {
order.value.order_status = 6
}
uni.showToast({ title: '申请已提交' })
} else {
uni.showToast({ title: '提交失败', icon: 'none' })
}
}
const applyRefund = () => { const applyRefund = () => {
uni.showModal({ uni.showModal({
title: '申请退款', title: '申请退款',
editable: true, editable: true,
placeholderText: '请输入退款原因', placeholderText: '请输入退款原因',
success: async (res) => { success: (res) => {
if (res.confirm) { if (res.confirm) {
const reason = res.content || '用户主动申请' const reason = res.content ?? '用户主动申请'
const success = await supabaseService.applyRefund(orderId.value, reason) doApplyRefund(reason)
if (success) {
order.value.order_status = 6
uni.showToast({ title: '申请已提交' })
} else {
uni.showToast({ title: '提交失败', icon: 'none' })
}
} }
} }
}) })
@@ -379,19 +440,31 @@ const goToProduct = (pid: string) => {
uni.navigateTo({ url: `/pages/mall/consumer/product-detail?id=${pid}` }) uni.navigateTo({ url: `/pages/mall/consumer/product-detail?id=${pid}` })
} }
// 生命周期 - 在所有函数定义之后
onLoad((options) => {
const id = options['id']
const orderIdParam = options['orderId']
if (id != null && id != '') {
orderId.value = id as string
loadOrderDetail()
} else if (orderIdParam != null && orderIdParam != '') {
orderId.value = orderIdParam as string
loadOrderDetail()
}
})
</script> </script>
<style scoped> <style scoped>
.order-detail-page { .order-detail-page {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100vh; flex: 1;
background-color: #f5f5f5; background-color: #f5f5f5;
} }
.scroll-content { .scroll-content {
flex: 1; flex: 1;
overflow-y: auto;
padding-bottom: 20px; padding-bottom: 20px;
} }
@@ -419,14 +492,12 @@ const goToProduct = (pid: string) => {
.status-text { .status-text {
font-size: 18px; font-size: 18px;
font-weight: bold; font-weight: bold;
display: block;
} }
.status-desc { .status-desc {
font-size: 12px; font-size: 12px;
opacity: 0.9; opacity: 0.9;
margin-top: 5px; margin-top: 5px;
display: block;
} }
/* 配送信息 */ /* 配送信息 */
@@ -434,7 +505,6 @@ const goToProduct = (pid: string) => {
font-weight: bold; font-weight: bold;
font-size: 16px; font-size: 16px;
margin-bottom: 10px; margin-bottom: 10px;
display: block;
} }
.delivery-address { .delivery-address {
@@ -535,10 +605,9 @@ const goToProduct = (pid: string) => {
font-size: 14px; font-size: 14px;
line-height: 1.4; line-height: 1.4;
color: #333; color: #333;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis;
lines: 2; /* uvue specific */
} }
.product-spec { .product-spec {
@@ -618,7 +687,7 @@ const goToProduct = (pid: string) => {
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
box-shadow: 0 -2px 10px rgba(0,0,0,0.05); box-shadow: 0 -2px 10px rgba(0,0,0,0.05);
padding-bottom: calc(10px + env(safe-area-inset-bottom)); padding-bottom: 30px;
} }
.action-left { .action-left {
@@ -631,7 +700,7 @@ const goToProduct = (pid: string) => {
align-items: center; align-items: center;
font-size: 10px; font-size: 10px;
color: #666; color: #666;
background: none; background-color: transparent;
line-height: 1.2; line-height: 1.2;
} }
@@ -642,7 +711,11 @@ const goToProduct = (pid: string) => {
.action-right { .action-right {
display: flex; display: flex;
gap: 10px; }
/* Add margin to buttons inside action-right for spacing */
.action-right .btn {
margin-left: 10px;
} }
.btn { .btn {
@@ -665,4 +738,4 @@ const goToProduct = (pid: string) => {
/* 状态样式 */ /* 状态样式 */
.status-4 .status-text { /* Completed */ } .status-4 .status-text { /* Completed */ }
</style> </style>

View File

@@ -29,6 +29,7 @@
> >
<text class="tab-name">{{ tab.name }}</text> <text class="tab-name">{{ tab.name }}</text>
<text v-if="tab.count > 0" class="tab-count">{{ tab.count }}</text> <text v-if="tab.count > 0" class="tab-count">{{ tab.count }}</text>
<view v-if="activeTab === tab.id" class="active-indicator"></view>
</view> </view>
</view> </view>
</scroll-view> </scroll-view>
@@ -179,9 +180,38 @@ onBackPress((options) => {
return false return false
}) })
// 定义标签页类型
type OrderTabItem = {
id: string,
name: string,
count: number
}
// 定义订单产品类型
type OrderProduct = {
id: string,
name: string,
price: number,
image: string,
spec: string,
quantity: number
}
// 定义订单类型
type OrderItem = {
id: string,
order_no: string,
status: number,
create_time: string,
product_amount: number,
shipping_fee: number,
total_amount: number,
products: OrderProduct[]
}
// 响应式数据 // 响应式数据
const orders = ref<any[]>([]) const orders = ref<OrderItem[]>([])
const allOrdersList = ref<any[]>([]) // Store all fetched orders for client-side filtering const allOrdersList = ref<OrderItem[]>([]) // Store all fetched orders for client-side filtering
const loading = ref<boolean>(false) const loading = ref<boolean>(false)
const loadingMore = ref<boolean>(false) const loadingMore = ref<boolean>(false)
const hasMore = ref<boolean>(true) const hasMore = ref<boolean>(true)
@@ -190,8 +220,8 @@ const page = ref<number>(1)
const activeTab = ref<string>('all') const activeTab = ref<string>('all')
const searchKeyword = ref<string>('') const searchKeyword = ref<string>('')
// 订单标签页 // 订单标签页 - 使用 ref 以便整体替换
const orderTabs = reactive([ const orderTabs = ref<OrderTabItem[]>([
{ id: 'all', name: '全部', count: 0 }, { id: 'all', name: '全部', count: 0 },
{ id: 'pending', name: '待付款', count: 0 }, { id: 'pending', name: '待付款', count: 0 },
{ id: 'shipping', name: '待发货', count: 0 }, { id: 'shipping', name: '待发货', count: 0 },
@@ -203,44 +233,82 @@ const orderTabs = reactive([
// Removed Mock Data // Removed Mock Data
// 计算属性:根据当前标签筛选订单 // 辅助函数:获取状态码
const filteredOrders = computed(() => { const getStatusByTab = (tabId: string): number => {
if (tabId == 'pending') return 1
if (tabId == 'shipping') return 2
if (tabId == 'delivering') return 3
if (tabId == 'completed') return 4
if (tabId == 'cancelled') return 5
return 0
}
// 辅助函数:解析规格文本
const parseSpecText = (specs: any): string => {
if (specs == null) return ''
if (typeof specs === 'string') return specs
// 对于对象类型尝试转为JSON字符串或简单处理
try {
return JSON.stringify(specs)
} catch (e) {
return ''
}
}
// 辅助函数:更新标签计数
const updateTabsCounts = (allOrders: any[]) => {
// 直接重新赋值整个数组
const tabsData = orderTabs.value
// 计算各状态数量
const countAll = allOrders.length
const countPending = allOrders.filter((o: any) => {
const obj = o as Record<string, any>
return obj['status'] === 1
}).length
const countShipping = allOrders.filter((o: any) => {
const obj = o as Record<string, any>
return obj['status'] === 2
}).length
const countDelivering = allOrders.filter((o: any) => {
const obj = o as Record<string, any>
return obj['status'] === 3
}).length
const countCompleted = allOrders.filter((o: any) => {
const obj = o as Record<string, any>
return obj['status'] === 4
}).length
const countCancelled = allOrders.filter((o: any) => {
const obj = o as Record<string, any>
return obj['status'] === 5
}).length
// 更新数组元素
const tabsArr = tabsData as any[]
const tab0 = tabsArr[0] as Record<string, any>
tab0['count'] = countAll
const tab1 = tabsArr[1] as Record<string, any>
tab1['count'] = countPending
const tab2 = tabsArr[2] as Record<string, any>
tab2['count'] = countShipping
const tab3 = tabsArr[3] as Record<string, any>
tab3['count'] = countDelivering
const tab4 = tabsArr[4] as Record<string, any>
tab4['count'] = countCompleted
const tab5 = tabsArr[5] as Record<string, any>
tab5['count'] = countCancelled
}
// 辅助函数:按标签筛选订单
const filterOrdersByTab = () => {
if (activeTab.value === 'all') { if (activeTab.value === 'all') {
return orders.value orders.value = allOrdersList.value
} else {
const targetStatus = getStatusByTab(activeTab.value)
orders.value = allOrdersList.value.filter((o: OrderItem) => {
return o.status === targetStatus
})
} }
}
const statusMap: Record<string, number> = {
'pending': 1,
'shipping': 2,
'delivering': 3,
'completed': 4,
'cancelled': 5
}
const targetStatus = statusMap[activeTab.value]
return orders.value.filter(order => order.status === targetStatus)
})
// 生命周期
onLoad((options) => {
if (options['status']) {
const status = options['status'] as string
if (['all', 'pending', 'shipping', 'delivering', 'completed', 'cancelled'].includes(status)) {
activeTab.value = status
}
}
if (options['type']) {
const type = options['type'] as string
if (type === 'pending') activeTab.value = 'pending'
else if (type === 'shipped') activeTab.value = 'delivering' // 映射到待收货
else if (type === 'review') activeTab.value = 'completed' // 映射到已完成
else if (type === 'refund') activeTab.value = 'all' // 申请售后默认显示全部
}
})
onShow(() => {
loadOrders()
})
// 加载订单数据 // 加载订单数据
const loadOrders = async () => { const loadOrders = async () => {
@@ -251,32 +319,67 @@ const loadOrders = async () => {
const fetchedOrders = await supabaseService.getOrders(0) const fetchedOrders = await supabaseService.getOrders(0)
// Map to View Model // Map to View Model
const mappedOrders = fetchedOrders.map((order: any) => ({ const mappedOrders: any[] = []
id: order.id, for (let i = 0; i < fetchedOrders.length; i++) {
order_no: order.order_no, const order = fetchedOrders[i]
status: order.order_status, const orderObj = order as Record<string, any>
create_time: order.created_at, const items = orderObj['ml_order_items'] as any[]
product_amount: order.product_amount || order.actual_amount, const productsList: any[] = []
shipping_fee: order.delivery_fee,
total_amount: order.actual_amount, if (items != null) {
products: (order.ml_order_items || []).map((item: any) => ({ for (let j = 0; j < items.length; j++) {
id: item.product_id, const item = items[j]
name: item.product_name, const itemObj = item as Record<string, any>
price: item.price, const specRaw = itemObj['specifications']
image: item.image_url || '/static/default-product.png', const specText = specRaw != null ? parseSpecText(specRaw) : ''
spec: item.specifications ? (typeof item.specifications === 'string' ? item.specifications : Object.values(item.specifications).join(' ')) : '', productsList.push({
quantity: item.quantity id: itemObj['product_id'],
})) name: itemObj['product_name'],
})) price: itemObj['price'],
image: itemObj['image_url'] ?? '/static/default-product.png',
spec: specText,
quantity: itemObj['quantity']
})
}
}
mappedOrders.push({
id: orderObj['id'],
order_no: orderObj['order_no'],
status: orderObj['order_status'],
create_time: orderObj['created_at'],
product_amount: orderObj['product_amount'] ?? 0,
shipping_fee: orderObj['shipping_fee'] ?? 0,
total_amount: orderObj['total_amount'] ?? orderObj['paid_amount'] ?? 0,
products: productsList
})
}
// Sort by created_at desc // Sort by created_at desc
mappedOrders.sort((a: any, b: any) => { mappedOrders.sort((a: any, b: any) => {
const timeA = new Date(a.create_time).getTime() const aObj = a as Record<string, any>
const timeB = new Date(b.create_time).getTime() const bObj = b as Record<string, any>
const timeA = new Date(aObj['create_time'] as string).getTime()
const timeB = new Date(bObj['create_time'] as string).getTime()
return timeB - timeA return timeB - timeA
}) })
allOrdersList.value = mappedOrders // 将 mappedOrders 转换为 OrderItem[] 类型
const typedOrders: OrderItem[] = []
for (let i = 0; i < mappedOrders.length; i++) {
const mo = mappedOrders[i] as Record<string, any>
typedOrders.push({
id: mo['id'] as string,
order_no: mo['order_no'] as string,
status: mo['status'] as number,
create_time: mo['create_time'] as string,
product_amount: mo['product_amount'] as number,
shipping_fee: mo['shipping_fee'] as number,
total_amount: mo['total_amount'] as number,
products: mo['products'] as OrderProduct[]
})
}
allOrdersList.value = typedOrders
// Update tab counts // Update tab counts
updateTabsCounts(mappedOrders) updateTabsCounts(mappedOrders)
@@ -292,41 +395,77 @@ const loadOrders = async () => {
} }
} }
const updateTabsCounts = (allOrders: any[]) => { // 生命周期
orderTabs[0].count = allOrders.length onLoad((options) => {
orderTabs[1].count = allOrders.filter((o: any) => o.status === 1).length if (options['status'] != null) {
orderTabs[2].count = allOrders.filter((o: any) => o.status === 2).length const status = options['status'] as string
orderTabs[3].count = allOrders.filter((o: any) => o.status === 3).length if (['all', 'pending', 'shipping', 'delivering', 'completed', 'cancelled'].includes(status)) {
orderTabs[4].count = allOrders.filter((o: any) => o.status === 4).length activeTab.value = status
orderTabs[5].count = allOrders.filter((o: any) => o.status === 5).length }
} }
if (options['type'] != null) {
const type = options['type'] as string
if (type === 'pending') activeTab.value = 'pending'
else if (type === 'shipped') activeTab.value = 'delivering' // 映射到待收货
else if (type === 'review') activeTab.value = 'completed' // 映射到已完成
else if (type === 'refund') activeTab.value = 'all' // 申请售后默认显示全部
}
})
const filterOrdersByTab = () => { onShow(() => {
const statusMap: Record<string, number> = { loadOrders()
'pending': 1, })
'shipping': 2,
'delivering': 3,
'completed': 4,
'cancelled': 5
}
if (activeTab.value === 'all') {
orders.value = allOrdersList.value
} else {
const targetStatus = statusMap[activeTab.value]
orders.value = allOrdersList.value.filter((o: any) => o.status === targetStatus)
}
}
const formatDate = (isoString: string): string => { const formatDate = (isoString: string): string => {
if (!isoString) return '' if (isoString == '') return ''
const date = new Date(isoString) const date = new Date(isoString)
return `${date.getFullYear()}-${(date.getMonth()+1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}` return `${date.getFullYear()}-${(date.getMonth()+1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
} }
// 辅助函数:获取当前订单数据(必须在 performSearch 之前定义)
function getCurrentOrderData(): OrderItem[] {
return allOrdersList.value
}
// 搜索执行函数(必须在 onSearchInput 等之前定义)
const performSearch = () => {
const keyword = searchKeyword.value.trim().toLowerCase()
if (keyword == '') {
loadOrders()
return
}
// 在当前订单数据中搜索
const allOrders = getCurrentOrderData()
const filtered = allOrders.filter((order: any) => {
const orderObj = order as Record<string, any>
// 搜索订单号
const orderNo = orderObj['order_no'] as string
if (orderNo != null && orderNo.toLowerCase().includes(keyword)) {
return true
}
// 搜索商品名称
const products = orderObj['products']
if (products != null && Array.isArray(products)) {
return products.some((product: any) => {
const productObj = product as Record<string, any>
const name = productObj['name'] as string
return name != null && name.toLowerCase().includes(keyword)
})
}
return false
})
orders.value = filtered
}
// 搜索相关函数 // 搜索相关函数
const onSearchInput = (e: any) => { const onSearchInput = (e: any) => {
searchKeyword.value = e.detail.value const eObj = e as Record<string, any>
const detail = eObj['detail'] as Record<string, any>
searchKeyword.value = detail['value'] as string
performSearch() performSearch()
} }
@@ -339,44 +478,13 @@ const clearSearch = () => {
performSearch() performSearch()
} }
const performSearch = () => {
const keyword = searchKeyword.value.trim().toLowerCase()
if (!keyword) {
loadOrders()
return
}
// 在当前订单数据中搜索
const allOrders = getCurrentOrderData() // 这里需要获取完整的订单数据
const filtered = allOrders.filter((order: any) => {
// 搜索订单号
if (order.order_no && order.order_no.toLowerCase().includes(keyword)) {
return true
}
// 搜索商品名称
if (order.products && Array.isArray(order.products)) {
return order.products.some((product: any) => {
return product.name && product.name.toLowerCase().includes(keyword)
})
}
return false
})
orders.value = filtered
}
const getCurrentOrderData = () => {
return allOrdersList.value
}
const formatSpec = (specs: any): string => { const formatSpec = (specs: any): string => {
if (!specs) return '' if (specs == null) return ''
if (typeof specs === 'string') return specs
if (typeof specs === 'object') { if (typeof specs === 'object') {
return Object.keys(specs).map(key => `${key}:${specs[key]}`).join(' ') return JSON.stringify(specs)
} }
return String(specs) return ''
} }
// 切换标签 // 切换标签
@@ -387,26 +495,22 @@ const switchTab = (tabId: string) => {
// 获取状态文本 // 获取状态文本
const getStatusText = (status: number): string => { const getStatusText = (status: number): string => {
const statusMap: Record<number, string> = { if (status == 1) return '待付款'
1: '待付款', if (status == 2) return '待发货'
2: '待货', if (status == 3) return '待货'
3: '待收货', if (status == 4) return '已完成'
4: '已完成', if (status == 5) return '已取消'
5: '已取消' return '未知状态'
}
return statusMap[status] || '未知状态'
} }
// 获取状态类名 // 获取状态类名
const getStatusClass = (status: number): string => { const getStatusClass = (status: number): string => {
const classMap: Record<number, string> = { if (status == 1) return 'status-pending'
1: 'status-pending', if (status == 2) return 'status-shipping'
2: 'status-shipping', if (status == 3) return 'status-delivering'
3: 'status-delivering', if (status == 4) return 'status-completed'
4: 'status-completed', if (status == 5) return 'status-cancelled'
5: 'status-cancelled' return 'status-unknown'
}
return classMap[status] || 'status-unknown'
} }
// 下拉刷新 // 下拉刷新
@@ -444,9 +548,13 @@ const cancelOrder = (orderId: string) => {
}) })
// 更新订单状态 // 更新订单状态
const index = orders.value.findIndex(order => order.id === orderId) const index = orders.value.findIndex((o: any) => {
const obj = o as Record<string, any>
return obj['id'] === orderId
})
if (index !== -1) { if (index !== -1) {
orders.value[index].status = 5 const orderObj = orders.value[index] as Record<string, any>
orderObj['status'] = 5
orders.value = [...orders.value] orders.value = [...orders.value]
} }
} }
@@ -473,59 +581,78 @@ const viewLogistics = (orderId: string) => {
}) })
} }
const confirmReceipt = async (orderId: string) => { // goReview 必须在 doConfirmReceipt 之前定义,因为 doConfirmReceipt 会调用它
uni.showModal({ const goReview = (order: any) => {
title: '确认收货', const orderObj = order as Record<string, any>
content: '请确认您已收到商品,且商品无误', const products = orderObj['products'] as any[]
success: async (res) => { const productIds = products.map((p: any) => {
if (res.confirm) { const pObj = p as Record<string, any>
uni.showLoading({ title: '处理中...' }) const pid = pObj['id']
try { return pid != null ? pid as string : ''
const result = await supabaseService.confirmReceipt(orderId) }).join(',')
uni.hideLoading() const orderId = orderObj['id'] as string
uni.navigateTo({
if (result.success) { url: `/pages/mall/consumer/review?orderId=${orderId}&productIds=${productIds}`
uni.showToast({
title: '收货成功',
icon: 'success'
})
// 更新本地状态
const index = orders.value.findIndex(order => order.id === orderId)
if (index !== -1) {
orders.value[index].status = 4
orders.value = [...orders.value]
}
// 跳转到评价页面
setTimeout(() => {
const order = orders.value.find(o => o.id === orderId)
if (order) {
goReview(order)
}
}, 1000)
} else {
uni.showToast({
title: result.error || '确认收货失败',
icon: 'none'
})
}
} catch (e) {
uni.hideLoading()
uni.showToast({
title: '系统异常',
icon: 'none'
})
}
}
}
}) })
} }
const goReview = (order: any) => { const doConfirmReceipt = async (orderId: string) => {
const productIds = order.products.map((p: any) => p.id).join(',') uni.showLoading({ title: '处理中...' })
uni.navigateTo({ try {
url: `/pages/mall/consumer/review?orderId=${order.id}&productIds=${productIds}` const result = await supabaseService.confirmReceipt(orderId)
uni.hideLoading()
if (result.success) {
uni.showToast({
title: '收货成功',
icon: 'success'
})
// 更新本地状态
const index = orders.value.findIndex((o: any) => {
const obj = o as Record<string, any>
return obj['id'] === orderId
})
if (index !== -1) {
const orderObj = orders.value[index] as Record<string, any>
orderObj['status'] = 4
orders.value = [...orders.value]
}
// 跳转到评价页面
setTimeout(() => {
const order = orders.value.find((o: any) => {
const obj = o as Record<string, any>
return obj['id'] === orderId
})
if (order != null) {
goReview(order)
}
}, 1000)
} else {
uni.showToast({
title: result.error ?? '确认收货失败',
icon: 'none'
})
}
} catch (e) {
uni.hideLoading()
uni.showToast({
title: '系统异常',
icon: 'none'
})
}
}
const confirmReceipt = (orderId: string) => {
uni.showModal({
title: '确认收货',
content: '请确认您已收到商品,且商品无误',
success: (res) => {
if (res.confirm) {
doConfirmReceipt(orderId)
}
}
}) })
} }
@@ -552,8 +679,10 @@ const viewOrderDetail = (orderId: string) => {
} }
const onApplyRefund = (order: any) => { const onApplyRefund = (order: any) => {
const orderObj = order as Record<string, any>
const orderId = orderObj['id']
uni.navigateTo({ uni.navigateTo({
url: `/pages/mall/consumer/apply-refund?orderId=${order.id}` url: `/pages/mall/consumer/apply-refund?orderId=${orderId}`
}) })
} }
@@ -563,7 +692,9 @@ const navigateToSearch = () => {
} }
const navigateToProduct = (product: any) => { const navigateToProduct = (product: any) => {
uni.navigateTo({ url: `/pages/mall/consumer/product-detail?id=${product.id}` }) const productObj = product as Record<string, any>
const productId = productObj['id']
uni.navigateTo({ url: `/pages/mall/consumer/product-detail?id=${productId}` })
} }
const goShopping = () => { const goShopping = () => {
@@ -574,7 +705,9 @@ const goShopping = () => {
<style> <style>
.orders-page { .orders-page {
width: 100%; width: 100%;
min-height: 100vh; flex: 1;
display: flex;
flex-direction: column;
background-color: #f5f5f5; background-color: #f5f5f5;
} }
@@ -586,8 +719,8 @@ const goShopping = () => {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border-bottom: 1px solid #eee; border-bottom: 1px solid #eee;
position: sticky; /* position: sticky; removed */
top: 0; /* top: 0; removed */
z-index: 10; z-index: 10;
} }
@@ -614,9 +747,7 @@ const goShopping = () => {
color: #999; color: #999;
font-size: 12px; font-size: 12px;
} }
.search-input:focus { .search-input:focus {
outline: none;
border-color: #ff5000; border-color: #ff5000;
background-color: white; background-color: white;
} }
@@ -637,17 +768,17 @@ const goShopping = () => {
height: 20px; height: 20px;
line-height: 18px; line-height: 18px;
text-align: center; text-align: center;
border-radius: 50%; border-radius: 10px; /* fixed 50% */
background-color: #ddd; background-color: #ddd;
cursor: pointer; /* cursor: pointer; removed */
} }
/* 标签页 */ /* 标签页 */
.order-tabs { .order-tabs {
background-color: #ffffff; background-color: #ffffff;
border-bottom: 1px solid #e5e5e5; border-bottom: 1px solid #e5e5e5;
position: sticky; /* position: sticky; removed */
top: 50px; /* top: 50px; removed */
z-index: 10; z-index: 10;
} }
@@ -660,9 +791,8 @@ const goShopping = () => {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
padding: 0 10px; padding: 0 10px;
/* 关键:确保宽度包含所有子元素,允许滚动 */ /* width: max-content; removed */
width: max-content; /* min-width: 100%; removed */
min-width: 100%;
} }
.tab-item { .tab-item {
@@ -683,7 +813,17 @@ const goShopping = () => {
} }
.tab-item.active::after { .tab-item.active::after {
content: ''; /* content: ''; removed */
/* content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background-color: #ff5000; */
}
.active-indicator {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
left: 0; left: 0;
@@ -709,7 +849,7 @@ const goShopping = () => {
/* 内容区 */ /* 内容区 */
.orders-content { .orders-content {
height: calc(100vh - 100px); flex: 1;
} }
/* 空状态 */ /* 空状态 */
@@ -830,7 +970,6 @@ const goShopping = () => {
font-size: 15px; font-size: 15px;
color: #333; color: #333;
margin-bottom: 5px; margin-bottom: 5px;
display: block;
line-height: 1.4; line-height: 1.4;
} }
@@ -838,7 +977,6 @@ const goShopping = () => {
font-size: 13px; font-size: 13px;
color: #999; color: #999;
margin-bottom: 10px; margin-bottom: 10px;
display: block;
} }
.product-footer { .product-footer {
@@ -906,7 +1044,7 @@ const goShopping = () => {
.action-buttons { .action-buttons {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
gap: 10px; /* gap: 10px; removed */
} }
.action-btn { .action-btn {
@@ -914,7 +1052,8 @@ const goShopping = () => {
border-radius: 15px; border-radius: 15px;
font-size: 13px; font-size: 13px;
border: 1px solid; border: 1px solid;
background: none; background-color: transparent; /* fixed background: none */
margin-left: 10px; /* alternative to gap */
} }
.action-btn.cancel { .action-btn.cancel {
@@ -966,16 +1105,10 @@ const goShopping = () => {
height: 24px; height: 24px;
border: 2px solid #f0f5ff; border: 2px solid #f0f5ff;
border-top-color: #ff5000; border-top-color: #ff5000;
border-radius: 50%; border-radius: 12px;
animation: spin 1s linear infinite;
margin-bottom: 10px; margin-bottom: 10px;
} }
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.no-more { .no-more {
text-align: center; text-align: center;
color: #999; color: #999;

View File

@@ -22,61 +22,71 @@
<script setup lang="uts"> <script setup lang="uts">
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
// import supa from '@/components/supadb/aksupainstance.uts' // 暂时注释掉数据库连接 import { supabaseService } from '@/utils/supabaseService.uts'
const orderId = ref('') const orderId = ref('')
const orderNo = ref('') const orderNo = ref('')
const amount = ref(0) const amount = ref(0)
// 定义 loadOrderInfo 函数(必须在 onMounted 之前)
const loadOrderInfo = async () => {
try {
const response = await supabaseService.getOrderById(orderId.value)
console.log('[payment-success] getOrderById response:', JSON.stringify(response))
if (response != null) {
const orderData = response as UTSJSONObject
const totalAmount = orderData.getNumber('total_amount')
const paidAmount = orderData.getNumber('paid_amount')
console.log('[payment-success] total_amount:', totalAmount, 'paid_amount:', paidAmount)
if (paidAmount != null && paidAmount > 0) {
amount.value = paidAmount
} else if (totalAmount != null && totalAmount > 0) {
amount.value = totalAmount
}
const orderNoVal = orderData.getString('order_no')
if (orderNoVal != null && orderNoVal != '') {
orderNo.value = orderNoVal
}
}
} catch (err) {
console.error('[payment-success] 加载订单信息失败:', err)
}
}
onMounted(() => { onMounted(() => {
const pages = getCurrentPages() const pages = getCurrentPages()
const currentPage = pages[pages.length - 1] const currentPage = pages[pages.length - 1]
const options = currentPage.options as any const options = currentPage.options as Record<string, any>
if (options.orderId) { console.log('[payment-success] options:', JSON.stringify(options))
orderId.value = options.orderId
orderNo.value = options.orderId // 使用订单ID作为订单号 const orderIdValue = options['orderId']
if (orderIdValue != null) {
orderId.value = orderIdValue as string
orderNo.value = orderIdValue as string
// 优先使用传递的 amount const amountValue = options['amount']
if (options.amount) { if (amountValue != null) {
amount.value = parseFloat(options.amount) const amountStr = amountValue.toString()
} else { console.log('[payment-success] amountStr:', amountStr)
// 如果没有传 amount尝试从本地存储查找订单 const parsed = parseFloat(amountStr)
try { console.log('[payment-success] parsed:', parsed)
const ordersStr = uni.getStorageSync('orders') if (isNaN(parsed) == false) {
if (ordersStr) { amount.value = parsed
const orders = JSON.parse(ordersStr as string) as any[]
const order = orders.find((o: any) => o.id === orderId.value)
if (order) {
amount.value = order.actual_amount || order.total_amount || 0
}
}
} catch (e) {
console.error('读取本地订单失败', e)
} }
} }
// loadOrderInfo() // 暂时注释掉数据库查询 if (amount.value == 0) {
console.log('[payment-success] amount为0尝试从数据库查询')
}
loadOrderInfo()
} }
}) })
// const loadOrderInfo = async () => {
// try {
// const { data, error } = await supa
// .from('orders')
// .select('order_no, actual_amount')
// .eq('id', orderId.value)
// .single()
//
// if (error == null && data != null) {
// orderNo.value = data['order_no'] as string
// amount.value = data['actual_amount'] as number
// }
// } catch (err) {
// console.error('加载订单信息失败', err)
// }
// }
const viewOrder = () => { const viewOrder = () => {
uni.navigateTo({ uni.navigateTo({
url: '/pages/mall/consumer/orders' url: '/pages/mall/consumer/orders'
@@ -96,7 +106,8 @@ const goHome = () => {
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 100vh; /* height: 100vh; */
flex: 1;
background-color: #ffffff; background-color: #ffffff;
padding: 0 30px; padding: 0 30px;
} }
@@ -162,7 +173,7 @@ const goHome = () => {
width: 100%; width: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 15px; /* gap: 15px; */
} }
.btn { .btn {
@@ -174,6 +185,7 @@ const goHome = () => {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin-bottom: 15px;
} }
.primary-btn { .primary-btn {

View File

@@ -66,7 +66,7 @@
<view v-for="(_, index) in 6" <view v-for="(_, index) in 6"
:key="index" :key="index"
class="password-dot"> class="password-dot">
<text v-if="password.length > index">●</text> <text v-if="password.length > index" class="password-dot-text">●</text>
</view> </view>
</view> </view>
<text class="forgot-password" @click="forgotPassword">忘记密码?</text> <text class="forgot-password" @click="forgotPassword">忘记密码?</text>
@@ -136,109 +136,9 @@ const productAmount = ref<number>(0) // 商品总价
const deliveryFee = ref<number>(0) // 运费 const deliveryFee = ref<number>(0) // 运费
const discountAmount = ref<number>(0) // 优惠减免 const discountAmount = ref<number>(0) // 优惠减免
// 生命周期 // 加载支付方式(必须在 onMounted 之前定义)
onMounted(() => {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const options = currentPage.options as any
if (options.orderId) {
orderId.value = options.orderId
loadOrderInfo()
}
if (options.amount) {
amount.value = parseFloat(options.amount)
}
// 获取传递的价格详情
if (options.productAmount) {
productAmount.value = parseFloat(options.productAmount)
}
if (options.deliveryFee) {
deliveryFee.value = parseFloat(options.deliveryFee)
}
if (options.discountAmount) {
discountAmount.value = parseFloat(options.discountAmount)
}
// 如果没有传详情,尝试根据总价估算(兼容旧逻辑,但优先使用传参)
if (!options.productAmount && amount.value > 0) {
calculatePriceDetails(amount.value)
}
loadPaymentMethods()
loadUserBalance()
})
// 监听返回操作(包含系统返回键和导航栏返回按钮)
onBackPress((options) => {
// 如果是通过代码主动调用 navigateBack 返回,则允许
if (options.from === 'navigateBack') {
return false
}
// 否则拦截返回,显示确认弹窗
goBack()
return true
})
// 更新本地存储中的订单状态
const updateOrderInStorage = (status: number) => {
try {
// 尝试从 'orders' 读取 (checkout页面写入的key)
const ordersStr = uni.getStorageSync('orders')
let orders: any[] = []
if (ordersStr) {
orders = JSON.parse(ordersStr as string) as any[]
}
const index = orders.findIndex((o: any) => o.id === orderId.value)
if (index !== -1) {
orders[index].status = status
orders[index].payment_status = status === 2 ? 1 : 0 // 2=待发货(已支付), 1=待支付(未支付)
orders[index].updated_at = new Date().toISOString()
// 确保更新的是 'orders' key
uni.setStorageSync('orders', JSON.stringify(orders))
console.log('订单状态已更新到Storage (orders):', orderId.value, status)
} else {
console.warn('在Storage (orders)中未找到订单:', orderId.value)
}
} catch (e) {
console.error('更新订单状态失败', e)
}
}
// 加载订单信息
const loadOrderInfo = async () => {
try {
if (!orderId.value) return
const order = await supabaseService.getOrderDetail(orderId.value)
if (order) {
orderNo.value = order.order_no
// Only update amount if not passed via options (options is priority for UI flow usually, but DB is source of truth)
// But checking consistency is good
const dbAmount = Number(order.total_amount)
if (dbAmount > 0) {
amount.value = dbAmount
}
if (order.items && order.items.length > 0) {
// Could update product name etc if displayed
}
} else {
// Fallback or error
console.warn('Order not found in DB', orderId.value)
if (!orderNo.value) orderNo.value = 'ORD_PENDING_' + Date.now()
}
} catch (err) {
console.error('加载订单信息异常:', err)
}
}
// 加载支付方式
const loadPaymentMethods = () => { const loadPaymentMethods = () => {
paymentMethods.value = [ const methods: PaymentMethodType[] = [
{ {
id: 'wechat', id: 'wechat',
name: '微信支付', name: '微信支付',
@@ -268,9 +168,10 @@ const loadPaymentMethods = () => {
enabled: true enabled: true
} }
] ]
paymentMethods.value = methods
} }
// 加载用户余额 // 加载用户余额(必须在 onMounted 之前定义)
const loadUserBalance = async () => { const loadUserBalance = async () => {
try { try {
const balance = await supabaseService.getUserBalance() const balance = await supabaseService.getUserBalance()
@@ -281,21 +182,215 @@ const loadUserBalance = async () => {
} }
} }
// 计算价格明细(必须在 onMounted 之前定义)
const calculatePriceDetails = (totalAmount: number) => {
// 模拟计算各项费用
// 假设商品总价占总金额的80%运费占10%优惠减免占10%
productAmount.value = totalAmount * 0.8
deliveryFee.value = totalAmount * 0.1
discountAmount.value = totalAmount * 0.1
// 确保总和等于应付金额
const calculatedTotal = productAmount.value + deliveryFee.value - discountAmount.value
if (Math.abs(calculatedTotal - totalAmount) > 0.01) {
// 调整商品总价以匹配应付金额
productAmount.value = totalAmount + discountAmount.value - deliveryFee.value
}
}
// 更新本地存储中的订单状态(必须在 onMounted 之前定义)
const updateOrderInStorage = (targetOrderId: string, status: number) => {
try {
// 尝试从 'orders' 读取 (checkout页面写入的key)
const ordersStr = uni.getStorageSync('orders')
let orders: Record<string, any>[] = []
if (ordersStr != null) {
const parsed = JSON.parse(ordersStr as string)
if (Array.isArray(parsed)) {
for (let i = 0; i < parsed.length; i++) {
orders.push(parsed[i] as Record<string, any>)
}
}
}
let foundIndex = -1
for (let i = 0; i < orders.length; i++) {
const o = orders[i]
if (o['id'] === targetOrderId) {
foundIndex = i
break
}
}
if (foundIndex !== -1) {
orders[foundIndex]['status'] = status
orders[foundIndex]['payment_status'] = status === 2 ? 1 : 0 // 2=待发货(已支付), 1=待支付(未支付)
orders[foundIndex]['updated_at'] = new Date().toISOString()
// 确保更新的是 'orders' key
uni.setStorageSync('orders', JSON.stringify(orders))
console.log('订单状态已更新到Storage (orders):', targetOrderId, status)
} else {
console.warn('在Storage (orders)中未找到订单:', targetOrderId)
}
} catch (e) {
console.error('更新订单状态失败', e)
}
}
// 取消支付,更新订单状态(必须在 goBack 之前定义)
const cancelPayment = async () => {
try {
// 这里应该调用API更新订单状态为待支付status: 1
// 模拟更新订单状态
// 更新本地存储
updateOrderInStorage(orderId.value, 1) // 1: 待支付
// 发布订单更新事件让profile页面可以刷新数据
uni.$emit('orderUpdated', { orderId: orderId.value, status: 1 })
uni.showToast({
title: '已保存到待支付订单',
icon: 'success'
})
// 延迟返回,让用户看到提示
setTimeout(() => {
uni.navigateBack()
}, 1500)
} catch (err) {
console.error('取消支付异常:', err)
uni.showToast({
title: '操作失败',
icon: 'none'
})
}
}
// 返回(必须在 onBackPress 之前定义)
const goBack = () => {
uni.showModal({
title: '取消支付',
content: '确定要取消支付吗?取消后订单将保存到待支付订单中',
confirmText: '取消支付',
cancelText: '继续支付',
success: (res) => {
if (res.confirm) {
// 用户确认取消支付,更新订单状态为待支付
cancelPayment()
}
}
})
}
// 加载订单信息(必须在 onMounted 之前定义)
const loadOrderInfo = async () => {
try {
if (orderId.value == '') return
const order = await supabaseService.getOrderDetail(orderId.value)
if (order != null) {
const orderObj = order as Record<string, any>
orderNo.value = orderObj['order_no'] as string
// Only update amount if not passed via options (options is priority for UI flow usually, but DB is source of truth)
// But checking consistency is good
const totalAmount = orderObj['total_amount']
const dbAmount = totalAmount != null ? parseFloat(totalAmount.toString()) : 0
if (dbAmount > 0) {
amount.value = dbAmount
}
const items = orderObj['items']
if (items != null && Array.isArray(items) && items.length > 0) {
// Could update product name etc if displayed
}
} else {
// Fallback or error
console.warn('Order not found in DB', orderId.value)
if (orderNo.value == '') orderNo.value = 'ORD_PENDING_' + Date.now()
}
} catch (err) {
console.error('加载订单信息异常:', err)
}
}
// 生命周期
onMounted(() => {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const options = currentPage.options as Record<string, any>
const orderIdValue = options['orderId']
if (orderIdValue != null) {
orderId.value = orderIdValue as string
loadOrderInfo()
}
const amountValue = options['amount']
if (amountValue != null) {
amount.value = parseFloat(amountValue.toString())
}
// 获取传递的价格详情
const productAmountValue = options['productAmount']
if (productAmountValue != null) {
productAmount.value = parseFloat(productAmountValue.toString())
}
const deliveryFeeValue = options['deliveryFee']
if (deliveryFeeValue != null) {
deliveryFee.value = parseFloat(deliveryFeeValue.toString())
}
const discountAmountValue = options['discountAmount']
if (discountAmountValue != null) {
discountAmount.value = parseFloat(discountAmountValue.toString())
}
// 如果没有传详情,尝试根据总价估算(兼容旧逻辑,但优先使用传参)
if (productAmountValue == null && amount.value > 0) {
calculatePriceDetails(amount.value)
}
loadPaymentMethods()
loadUserBalance()
})
// 监听返回操作(包含系统返回键和导航栏返回按钮)
onBackPress((options) => {
// 如果是通过代码主动调用 navigateBack 返回,则允许
if (options.from === 'navigateBack') {
return false
}
// 否则拦截返回,显示确认弹窗
goBack()
return true
})
// 获取当前用户ID // 获取当前用户ID
const getCurrentUserId = (): string => { const getCurrentUserId = (): string => {
const userStore = uni.getStorageSync('userInfo') const userStore = uni.getStorageSync('userInfo')
return userStore?.id || '' if (userStore != null) {
const userObj = userStore as Record<string, any>
const id = userObj['id']
if (id != null) {
return id as string
}
}
return ''
} }
// 获取支付方式图标 // 获取支付方式图标
const getMethodIcon = (methodId: string): string => { const getMethodIcon = (methodId: string): string => {
const icons: Record<string, string> = { if (methodId === 'wechat') {
wechat: '💳', return '💳'
alipay: '💳', } else if (methodId === 'alipay') {
balance: '💰', return '💳'
bankcard: '💳' } else if (methodId === 'balance') {
return '💰'
} else if (methodId === 'bankcard') {
return '💳'
} }
return icons[methodId] || '💳' return '💳'
} }
// 选择支付方式 // 选择支付方式
@@ -319,14 +414,16 @@ const getPayButtonText = (): string => {
return '余额不足' return '余额不足'
} }
const texts: Record<string, string> = { if (selectedMethod.value === 'wechat') {
wechat: '微信支付', return '微信支付'
alipay: '支付宝支付', } else if (selectedMethod.value === 'alipay') {
balance: '余额支付', return '支付宝支付'
bankcard: '银行卡支付' } else if (selectedMethod.value === 'balance') {
return '余额支付'
} else if (selectedMethod.value === 'bankcard') {
return '银行卡支付'
} }
return '确认支付'
return texts[selectedMethod.value] || '确认支付'
} }
// 减少商品库存 // 减少商品库存
@@ -363,29 +460,37 @@ const confirmPayment = async () => {
} }
isPaying.value = true isPaying.value = true
uni.showLoading({ title: '支付中...' })
try { try {
// Call Supabase Service to handle payment console.log('[confirmPayment] 开始支付, orderId:', orderId.value, 'method:', selectedMethod.value)
const success = await supabaseService.payOrder(orderId.value, selectedMethod.value, amount.value) const success = await supabaseService.payOrder(orderId.value, selectedMethod.value, amount.value)
console.log('[confirmPayment] 支付结果:', success)
if (!success) { if (!success) {
throw new Error('Payment processing failed') console.error('[confirmPayment] payOrder 返回 false')
uni.hideLoading()
uni.showToast({
title: '支付处理失败',
icon: 'none'
})
isPaying.value = false
return
} }
// 更新订单状态 uni.hideLoading()
updateOrderInStorage(2) // 2: 待发货(已支付)
updateOrderInStorage(orderId.value, 2)
// 支付成功
uni.showToast({ uni.showToast({
title: '支付成功', title: '支付成功',
icon: 'success', icon: 'success',
duration: 2000 duration: 2000
}) })
// 发布订单更新事件让profile页面可以刷新数据 uni.$emit('orderUpdated', { orderId: orderId.value, status: 2 })
uni.$emit('orderUpdated', { orderId: orderId.value, status: 2 }) // 2: 待发货
// 跳转到支付成功页面
setTimeout(() => { setTimeout(() => {
uni.redirectTo({ uni.redirectTo({
url: `/pages/mall/consumer/payment-success?orderId=${orderId.value}` url: `/pages/mall/consumer/payment-success?orderId=${orderId.value}`
@@ -393,56 +498,42 @@ const confirmPayment = async () => {
}, 1500) }, 1500)
} catch (err) { } catch (err) {
console.error('支付失败:', err) console.error('[confirmPayment] 支付异常:', err)
uni.hideLoading()
uni.showToast({ uni.showToast({
title: '支付失败', title: '支付失败',
icon: 'none' icon: 'none'
}) })
} finally {
isPaying.value = false isPaying.value = false
} }
} }
// 获取支付方式代码 // 获取支付方式代码
const getPaymentMethodCode = (methodId: string): number => { const getPaymentMethodCode = (methodId: string): number => {
const codes: Record<string, number> = { if (methodId === 'wechat') {
wechat: 1, return 1
alipay: 2, } else if (methodId === 'alipay') {
balance: 3, return 2
bankcard: 4 } else if (methodId === 'balance') {
return 3
} else if (methodId === 'bankcard') {
return 4
} }
return codes[methodId] || 0 return 0
} }
// 输入密码 // 验证密码(必须在 watch 之前定义)
const inputPassword = (num: string) => {
if (password.value.length >= 6) return
password.value += num
}
// 删除密码
const deletePassword = () => {
if (password.value.length > 0) {
password.value = password.value.slice(0, -1)
}
}
// 监听密码输入
watch(password, (newPassword) => {
if (newPassword.length === 6) {
// 自动验证密码
verifyPassword()
}
})
// 验证密码
const verifyPassword = async () => { const verifyPassword = async () => {
// 这里应该验证支付密码,这里简单模拟 // 这里应该验证支付密码,这里简单模拟
const userId = getCurrentUserId() const userId = getCurrentUserId()
try { try {
// 模拟验证 // 模拟验证
await new Promise(resolve => setTimeout(resolve, 500)) await new Promise<void>((resolve: (value: void) => void) => {
setTimeout(() => {
resolve()
}, 500)
})
// 假设密码正确 // 假设密码正确
const isCorrect = true const isCorrect = true
@@ -463,6 +554,27 @@ const verifyPassword = async () => {
} }
} }
// 输入密码
const inputPassword = (num: string) => {
if (password.value.length >= 6) return
password.value += num
}
// 删除密码
const deletePassword = () => {
if (password.value.length > 0) {
password.value = password.value.slice(0, -1)
}
}
// 监听密码输入
watch(password, (newPassword: string) => {
if (newPassword.length === 6) {
// 自动验证密码
verifyPassword()
}
})
// 忘记密码 // 忘记密码
const forgotPassword = () => { const forgotPassword = () => {
uni.navigateTo({ uni.navigateTo({
@@ -470,99 +582,17 @@ const forgotPassword = () => {
}) })
} }
// 计算价格明细
const calculatePriceDetails = (totalAmount: number) => {
// 模拟计算各项费用
// 假设商品总价占总金额的80%运费占10%优惠减免占10%
productAmount.value = totalAmount * 0.8
deliveryFee.value = totalAmount * 0.1
discountAmount.value = totalAmount * 0.1
// 确保总和等于应付金额
const calculatedTotal = productAmount.value + deliveryFee.value - discountAmount.value
if (Math.abs(calculatedTotal - totalAmount) > 0.01) {
// 调整商品总价以匹配应付金额
productAmount.value = totalAmount + discountAmount.value - deliveryFee.value
}
}
// 在组件卸载时移除返回键监听 // 在组件卸载时移除返回键监听
onUnmounted(() => { onUnmounted(() => {
// uni.offBackPress() 在uni-app中不需要手动移除 // uni.offBackPress() 在uni-app中不需要手动移除
}) })
// 返回
const goBack = () => {
uni.showModal({
title: '取消支付',
content: '确定要取消支付吗?取消后订单将保存到待支付订单中',
confirmText: '取消支付',
cancelText: '继续支付',
success: async (res) => {
if (res.confirm) {
// 用户确认取消支付,更新订单状态为待支付
await cancelPayment()
} else {
// 用户选择继续支付,留在当前页面
return
}
}
})
}
// 取消支付,更新订单状态
const cancelPayment = async () => {
try {
// 这里应该调用API更新订单状态为待支付status: 1
// 模拟更新订单状态
/* const { error } = await supa
.from('orders')
.update({
status: 1, // 待支付
updated_at: new Date().toISOString()
})
.eq('id', orderId.value)
if (error !== null) {
console.error('更新订单状态失败:', error)
uni.showToast({
title: '操作失败',
icon: 'none'
})
return
} */
// 更新本地存储
updateOrderInStorage(orderId.value, 1) // 1: 待支付
// 发布订单更新事件让profile页面可以刷新数据
uni.$emit('orderUpdated', { orderId: orderId.value, status: 1 })
uni.showToast({
title: '已保存到待支付订单',
icon: 'success'
})
// 延迟返回,让用户看到提示
setTimeout(() => {
uni.navigateBack()
}, 1500)
} catch (err) {
console.error('取消支付异常:', err)
uni.showToast({
title: '操作失败',
icon: 'none'
})
}
}
</script> </script>
<style scoped> <style scoped>
.payment-page { .payment-page {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100vh; flex: 1;
background-color: #f5f5f5; background-color: #f5f5f5;
} }
@@ -589,7 +619,7 @@ const cancelPayment = async () => {
.payment-content { .payment-content {
flex: 1; flex: 1;
overflow-y: auto; /* overflow-y: auto; */
} }
/* 价格明细部分 */ /* 价格明细部分 */
@@ -640,7 +670,7 @@ const cancelPayment = async () => {
} }
.order-no { .order-no {
display: block; /* display: block; */
font-size: 12px; font-size: 12px;
color: #999999; color: #999999;
text-align: center; text-align: center;
@@ -653,7 +683,7 @@ const cancelPayment = async () => {
} }
.section-title { .section-title {
display: block; /* display: block; */
font-size: 16px; font-size: 16px;
font-weight: bold; font-weight: bold;
color: #333333; color: #333333;
@@ -663,7 +693,7 @@ const cancelPayment = async () => {
.method-list { .method-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; /* gap: 10px; */
} }
.method-item { .method-item {
@@ -673,6 +703,7 @@ const cancelPayment = async () => {
padding: 15px; padding: 15px;
border: 1px solid #e5e5e5; border: 1px solid #e5e5e5;
border-radius: 8px; border-radius: 8px;
margin-bottom: 10px;
} }
.method-item.selected { .method-item.selected {
@@ -765,7 +796,7 @@ const cancelPayment = async () => {
} }
.password-title { .password-title {
display: block; /* display: block; */
font-size: 16px; font-size: 16px;
color: #333333; color: #333333;
margin-bottom: 30px; margin-bottom: 30px;
@@ -774,7 +805,7 @@ const cancelPayment = async () => {
.password-input { .password-input {
display: flex; display: flex;
justify-content: center; justify-content: center;
gap: 15px; /* gap: 15px; */
margin-bottom: 20px; margin-bottom: 20px;
} }
@@ -786,9 +817,10 @@ const cancelPayment = async () => {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin: 0 7.5px;
} }
.password-dot text { .password-dot-text {
color: #ffffff; color: #ffffff;
font-size: 8px; font-size: 8px;
} }
@@ -809,7 +841,7 @@ const cancelPayment = async () => {
.price-summary { .price-summary {
display: flex; display: flex;
align-items: baseline; align-items: flex-end;
} }
.summary-label { .summary-label {
@@ -847,18 +879,22 @@ const cancelPayment = async () => {
} }
.keyboard-grid { .keyboard-grid {
display: grid; display: flex;
grid-template-columns: repeat(3, 1fr); flex-wrap: wrap;
grid-gap: 1px; /* grid-template-columns: repeat(3, 1fr); uvue unsupported */
/* grid-gap: 1px; uvue unsupported */
background-color: #e5e5e5; background-color: #e5e5e5;
} }
.keyboard-key { .keyboard-key {
width: 33.33%;
background-color: #ffffff; background-color: #ffffff;
height: 60px; height: 60px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border: 1px solid #f5f5f5; /* mimic grid gap */
box-sizing: border-box;
} }
.key-text { .key-text {

View File

@@ -24,7 +24,7 @@
<view v-else class="record-list"> <view v-else class="record-list">
<view v-for="item in records" :key="item.id" class="record-item"> <view v-for="item in records" :key="item.id" class="record-item">
<view class="record-left"> <view class="record-left">
<text class="record-title">{{ item.description || getTypeText(item.type) }}</text> <text class="record-title">{{ item.description ?? getTypeText(item.type) }}</text>
<text class="record-time">{{ formatTime(item.created_at) }}</text> <text class="record-time">{{ formatTime(item.created_at) }}</text>
</view> </view>
<view class="record-right"> <view class="record-right">
@@ -105,11 +105,11 @@ const getTypeText = (type: string): string => {
'admin': '系统调整', 'admin': '系统调整',
'register': '注册赠送' 'register': '注册赠送'
} }
return map[type] || '积分变动' return map[type] ?? '积分变动'
} }
const formatTime = (timeStr: string): string => { const formatTime = (timeStr: string): string => {
if (!timeStr) return '' if (timeStr == '') return ''
const date = new Date(timeStr) const date = new Date(timeStr)
const y = date.getFullYear() const y = date.getFullYear()
const m = (date.getMonth() + 1).toString().padStart(2, '0') const m = (date.getMonth() + 1).toString().padStart(2, '0')
@@ -122,8 +122,7 @@ const formatTime = (timeStr: string): string => {
<style> <style>
.points-page { .points-page {
min-height: 100vh; flex: 1;
background-color: #f5f7fa;
} }
.points-header { .points-header {
@@ -174,7 +173,7 @@ const formatTime = (timeStr: string): string => {
font-weight: bold; font-weight: bold;
padding: 16px 0; padding: 16px 0;
border-bottom: 1px solid #f0f0f0; border-bottom: 1px solid #f0f0f0;
display: block; display: flex;
} }
.record-item { .record-item {
@@ -188,10 +187,7 @@ const formatTime = (timeStr: string): string => {
.record-left { .record-left {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 4px; margin-bottom: 4px;
}
.record-title {
font-size: 15px; font-size: 15px;
color: #333; color: #333;
} }

View File

@@ -1,6 +1,7 @@
<!-- 消费者端 - 商品详情页 --> <!-- 消费者端 - 商品详情页 -->
<template> <template>
<view class="product-detail-page"> <view class="product-detail-page">
<scroll-view class="page-scroll" scroll-y="true">
<!-- 商品图片轮播 --> <!-- 商品图片轮播 -->
<view class="product-images"> <view class="product-images">
<swiper class="image-swiper" :indicator-dots="true" :autoplay="false" @change="onSwiperChange"> <swiper class="image-swiper" :indicator-dots="true" :autoplay="false" @change="onSwiperChange">
@@ -23,7 +24,7 @@
<!-- 店铺信息 --> <!-- 店铺信息 -->
<view class="shop-info" @click="goToShop"> <view class="shop-info" @click="goToShop">
<image :src="merchant.shop_logo || '/static/default-shop.png'" class="shop-logo" /> <image :src="merchant.shop_logo ?? '/static/default-shop.png'" class="shop-logo" />
<view class="shop-details"> <view class="shop-details">
<text class="shop-name" @click.stop="goToShop">{{ merchant.shop_name }}</text> <text class="shop-name" @click.stop="goToShop">{{ merchant.shop_name }}</text>
<view class="shop-stats-row"> <view class="shop-stats-row">
@@ -67,7 +68,7 @@
<!-- 规格选择 --> <!-- 规格选择 -->
<view class="spec-section" @click="showSpecModal" v-if="productSkus.length > 0"> <view class="spec-section" @click="showSpecModal" v-if="productSkus.length > 0">
<text class="spec-title">规格</text> <text class="spec-title">规格</text>
<text class="spec-selected">{{ selectedSpec || '请选择规格' }}</text> <text class="spec-selected">{{ selectedSpec ?? '请选择规格' }}</text>
<text class="spec-arrow">></text> <text class="spec-arrow">></text>
</view> </view>
@@ -94,7 +95,7 @@
<!-- 商品详情 --> <!-- 商品详情 -->
<view class="product-description"> <view class="product-description">
<view class="section-title">商品详情</view> <view class="section-title">商品详情</view>
<text class="description-text">{{ product.description || '暂无详细描述' }}</text> <text class="description-text">{{ product.description ?? '暂无详细描述' }}</text>
<!-- 商品详情图片 --> <!-- 商品详情图片 -->
<view class="detail-images" v-if="product.images && product.images.length > 0"> <view class="detail-images" v-if="product.images && product.images.length > 0">
<image v-for="(img, index) in product.images" <image v-for="(img, index) in product.images"
@@ -105,6 +106,7 @@
@click="previewImage(index)" /> @click="previewImage(index)" />
</view> </view>
</view> </view>
</scroll-view>
<!-- 底部操作栏 --> <!-- 底部操作栏 -->
<view class="bottom-actions"> <view class="bottom-actions">
@@ -136,7 +138,7 @@
<text class="spec-title">选择规格</text> <text class="spec-title">选择规格</text>
<text class="close-btn" @click="hideSpecModal">×</text> <text class="close-btn" @click="hideSpecModal">×</text>
</view> </view>
<view class="spec-list"> <scroll-view class="spec-list" direction="vertical">
<view v-for="sku in productSkus" :key="sku.id" <view v-for="sku in productSkus" :key="sku.id"
class="spec-item" class="spec-item"
:class="{ active: selectedSkuId === sku.id }" :class="{ active: selectedSkuId === sku.id }"
@@ -145,7 +147,7 @@
<text class="spec-price">¥{{ sku.price }}</text> <text class="spec-price">¥{{ sku.price }}</text>
<text class="spec-stock">库存{{ sku.stock }}</text> <text class="spec-stock">库存{{ sku.stock }}</text>
</view> </view>
</view> </scroll-view>
</view> </view>
</view> </view>
@@ -156,7 +158,7 @@
<text class="params-title">商品参数</text> <text class="params-title">商品参数</text>
<text class="close-btn" @click="hideParamsModal">×</text> <text class="close-btn" @click="hideParamsModal">×</text>
</view> </view>
<view class="params-list"> <scroll-view class="params-list" direction="vertical">
<view class="params-item" v-if="product.specification"> <view class="params-item" v-if="product.specification">
<text class="params-label">规格</text> <text class="params-label">规格</text>
<text class="params-value">{{ product.specification }}</text> <text class="params-value">{{ product.specification }}</text>
@@ -189,7 +191,7 @@
<text class="params-label">标签</text> <text class="params-label">标签</text>
<text class="params-value">{{ product.tags.join(', ') }}</text> <text class="params-value">{{ product.tags.join(', ') }}</text>
</view> </view>
</view> </scroll-view>
</view> </view>
</view> </view>
@@ -272,7 +274,7 @@ export default {
} }
}, },
onLoad(options: any) { onLoad(options: any) {
const productId = options.productId as string || options.id as string const productId = (options['productId'] ?? options['id']) as string
const productPrice = options.price ? parseFloat(options.price) : null const productPrice = options.price ? parseFloat(options.price) : null
const productOriginalPrice = options.originalPrice ? parseFloat(options.originalPrice) : null const productOriginalPrice = options.originalPrice ? parseFloat(options.originalPrice) : null
@@ -370,7 +372,7 @@ export default {
uni.showLoading({ title: '加载中...' }) uni.showLoading({ title: '加载中...' })
try { try {
const dbProductResponse = await supabaseService.getProductById(productId) const dbProductResponse = await supabaseService.getProductById(productId)
let dbProduct: any = null let dbProduct: any | null = null
if (Array.isArray(dbProductResponse) && dbProductResponse.length > 0) { if (Array.isArray(dbProductResponse) && dbProductResponse.length > 0) {
dbProduct = dbProductResponse[0] dbProduct = dbProductResponse[0]
} else if (dbProductResponse && !Array.isArray(dbProductResponse)) { } else if (dbProductResponse && !Array.isArray(dbProductResponse)) {
@@ -381,25 +383,25 @@ export default {
// Map DB product to local product // Map DB product to local product
this.product = { this.product = {
id: dbProduct.id, id: dbProduct.id,
merchant_id: dbProduct.merchant_id || dbProduct.shop_id || '', merchant_id: dbProduct.merchant_id ?? dbProduct.shop_id ?? '',
category_id: dbProduct.category_id || '', category_id: dbProduct.category_id ?? '',
name: dbProduct.name, name: dbProduct.name,
description: dbProduct.description || '', description: dbProduct.description ?? '',
images: [] as string[], images: [] as string[],
price: dbProduct.base_price || dbProduct.price || 0, price: dbProduct.base_price ?? dbProduct.price ?? 0,
original_price: dbProduct.market_price || dbProduct.original_price || 0, original_price: dbProduct.market_price ?? dbProduct.original_price ?? 0,
stock: dbProduct.available_stock || dbProduct.total_stock || dbProduct.stock || 0, stock: dbProduct.available_stock ?? dbProduct.total_stock ?? dbProduct.stock ?? 0,
sales: dbProduct.sale_count || dbProduct.sales || 0, sales: dbProduct.sale_count ?? dbProduct.sales ?? 0,
status: dbProduct.status !== undefined ? dbProduct.status : 1, status: dbProduct.status !== undefined ? dbProduct.status : 1,
created_at: dbProduct.created_at || new Date().toISOString(), created_at: dbProduct.created_at ?? new Date().toISOString(),
// Attributes // Attributes
specification: dbProduct.specification || null, specification: dbProduct.specification ?? null,
usage: dbProduct.usage || null, usage: dbProduct.usage ?? null,
side_effects: dbProduct.side_effects || null, side_effects: dbProduct.side_effects ?? null,
precautions: dbProduct.precautions || null, precautions: dbProduct.precautions ?? null,
expiry_date: dbProduct.expiry_date || null, expiry_date: dbProduct.expiry_date ?? null,
storage_conditions: dbProduct.storage_conditions || null, storage_conditions: dbProduct.storage_conditions ?? null,
approval_number: dbProduct.approval_number || null, approval_number: dbProduct.approval_number ?? null,
tags: [] as string[] tags: [] as string[]
} as ProductType } as ProductType
@@ -482,15 +484,15 @@ export default {
id: shop.id, id: shop.id,
user_id: shop.merchant_id, user_id: shop.merchant_id,
shop_name: shop.shop_name, shop_name: shop.shop_name,
shop_logo: shop.shop_logo || '/static/default-shop.png', shop_logo: shop.shop_logo ?? '/static/default-shop.png',
shop_banner: shop.shop_banner || '/static/default-banner.png', shop_banner: shop.shop_banner ?? '/static/default-banner.png',
shop_description: shop.description || '', shop_description: shop.description ?? '',
contact_name: shop.contact_name || '店主', contact_name: shop.contact_name ?? '店主',
contact_phone: shop.contact_phone || '', contact_phone: shop.contact_phone ?? '',
shop_status: 1, shop_status: 1,
rating: shop.rating_avg || 5.0, rating: shop.rating_avg ?? 5.0,
total_sales: shop.total_sales || 0, total_sales: shop.total_sales ?? 0,
created_at: shop.created_at || new Date().toISOString() created_at: shop.created_at ?? new Date().toISOString()
} as MerchantType } as MerchantType
realMerchantLoaded = true realMerchantLoaded = true
} }
@@ -547,7 +549,7 @@ export default {
specifications: specs, specifications: specs,
price: sku.price, price: sku.price,
stock: sku.stock !== undefined ? sku.stock : 0, stock: sku.stock !== undefined ? sku.stock : 0,
image_url: sku.image_url || '', image_url: sku.image_url != null ? sku.image_url : '',
status: sku.status !== undefined ? sku.status : 1 status: sku.status !== undefined ? sku.status : 1
} as ProductSkuType } as ProductSkuType
}) })
@@ -560,7 +562,7 @@ export default {
// 新增:加载优惠券 // 新增:加载优惠券
async loadCoupons() { async loadCoupons() {
if (!this.product.merchant_id) return if (this.product.merchant_id == '') return
// Safety check for cached service definition // Safety check for cached service definition
// @ts-ignore // @ts-ignore
if (typeof supabaseService.fetchShopCoupons === 'function') { if (typeof supabaseService.fetchShopCoupons === 'function') {
@@ -574,12 +576,12 @@ export default {
// 新增:联系客服(商家) // 新增:联系客服(商家)
contactMerchant() { contactMerchant() {
if (!supabaseService.getCurrentUserId()) { if (supabaseService.getCurrentUserId() == '') {
uni.navigateTo({ url: '/pages/auth/login' }) uni.navigateTo({ url: '/pages/auth/login' })
return return
} }
// Navigate to chat // Navigate to chat
const merchId = this.merchant.user_id || this.merchant.id || this.product.merchant_id; const merchId = this.merchant.user_id ?? this.merchant.id ?? this.product.merchant_id;
uni.navigateTo({ uni.navigateTo({
url: `/pages/mall/consumer/chat?merchantId=${merchId}&merchantName=${this.merchant.shop_name}` url: `/pages/mall/consumer/chat?merchantId=${merchId}&merchantName=${this.merchant.shop_name}`
}) })
@@ -596,7 +598,7 @@ export default {
// 新增:领取优惠券 // 新增:领取优惠券
async claimCoupon(coupon: any) { async claimCoupon(coupon: any) {
const userId = supabaseService.getCurrentUserId() const userId = supabaseService.getCurrentUserId()
if (!userId) { if (userId == '') {
uni.navigateTo({ url: '/pages/auth/login' }) uni.navigateTo({ url: '/pages/auth/login' })
return return
} }
@@ -621,7 +623,7 @@ export default {
}, },
formatDate(dateStr: string): string { formatDate(dateStr: string): string {
if (!dateStr) return '' if (dateStr == '') return ''
const date = new Date(dateStr) const date = new Date(dateStr)
return `${date.getFullYear()}.${date.getMonth()+1}.${date.getDate()}` return `${date.getFullYear()}.${date.getMonth()+1}.${date.getDate()}`
}, },
@@ -729,7 +731,7 @@ export default {
}, },
async toggleFavorite() { async toggleFavorite() {
if (!this.product.id) return if (this.product.id == '') return
uni.showLoading({ title: '处理中' }) uni.showLoading({ title: '处理中' })
try { try {
@@ -830,8 +832,15 @@ export default {
<style> <style>
.product-detail-page { .product-detail-page {
background-color: #f5f5f5; background-color: #f5f5f5;
min-height: 100vh; flex: 1;
padding-bottom: 120rpx; display: flex;
flex-direction: column;
}
.page-scroll {
flex: 1;
height: 0;
width: 100%;
} }
.product-images { .product-images {
@@ -881,7 +890,7 @@ export default {
.original-price { .original-price {
font-size: 28rpx; font-size: 28rpx;
color: #999; color: #999;
text-decoration: line-through; text-decoration-line: line-through;
} }
.product-name { .product-name {
@@ -991,15 +1000,18 @@ export default {
bottom: 0; bottom: 0;
background-color: rgba(0, 0, 0, 0.5); background-color: rgba(0, 0, 0, 0.5);
display: flex; display: flex;
align-items: flex-end; justify-content: flex-end;
flex-direction: column;
z-index: 1000; z-index: 1000;
} }
.popup-content { .popup-content {
background-color: #fff; background-color: #fff;
width: 100%; width: 100%;
max-height: 80vh;
border-radius: 20rpx 20rpx 0 0; border-radius: 20rpx 20rpx 0 0;
padding: 30rpx; padding: 30rpx;
display: flex;
flex-direction: column;
max-height: 1000rpx;
} }
.popup-header { .popup-header {
display: flex; display: flex;
@@ -1019,7 +1031,7 @@ export default {
color: #999; color: #999;
} }
.coupon-list-scroll { .coupon-list-scroll {
max-height: 60vh; flex: 1;
} }
.coupon-item { .coupon-item {
display: flex; display: flex;
@@ -1168,6 +1180,7 @@ export default {
.product-description { .product-description {
background-color: #fff; background-color: #fff;
padding: 30rpx; padding: 30rpx;
padding-bottom: 140rpx;
margin-bottom: 20rpx; margin-bottom: 20rpx;
} }
@@ -1259,16 +1272,19 @@ export default {
bottom: 0; bottom: 0;
background-color: rgba(0, 0, 0, 0.5); background-color: rgba(0, 0, 0, 0.5);
display: flex; display: flex;
align-items: flex-end; justify-content: flex-end; /* UVUE 推荐用 flex 布局对齐 */
flex-direction: column;
z-index: 999; z-index: 999;
} }
.spec-content { .spec-content {
background-color: #fff; background-color: #fff;
width: 100%; width: 100%;
max-height: 80vh;
border-radius: 20rpx 20rpx 0 0; border-radius: 20rpx 20rpx 0 0;
padding: 30rpx; padding: 30rpx;
display: flex;
flex-direction: column;
max-height: 1000rpx;
} }
.spec-header { .spec-header {
@@ -1287,8 +1303,7 @@ export default {
} }
.spec-list { .spec-list {
max-height: 60vh; flex: 1;
overflow-y: auto;
} }
.spec-item { .spec-item {
@@ -1333,7 +1348,6 @@ export default {
color: #333; color: #333;
font-weight: bold; font-weight: bold;
margin-bottom: 15rpx; margin-bottom: 15rpx;
display: block;
} }
.function-content { .function-content {
@@ -1391,16 +1405,19 @@ export default {
bottom: 0; bottom: 0;
background-color: rgba(0, 0, 0, 0.5); background-color: rgba(0, 0, 0, 0.5);
display: flex; display: flex;
align-items: flex-end; justify-content: flex-end; /* UVUE 推荐用 flex 布局对齐 */
flex-direction: column;
z-index: 1000; z-index: 1000;
} }
.params-content { .params-content {
background-color: #fff; background-color: #fff;
width: 100%; width: 100%;
max-height: 80vh;
border-radius: 20rpx 20rpx 0 0; border-radius: 20rpx 20rpx 0 0;
padding: 30rpx; padding: 30rpx;
display: flex;
flex-direction: column;
max-height: 1000rpx;
} }
.params-header { .params-header {
@@ -1420,8 +1437,7 @@ export default {
} }
.params-list { .params-list {
max-height: 60vh; flex: 1;
overflow-y: auto;
} }
.params-item { .params-item {
@@ -1473,8 +1489,6 @@ export default {
flex: 1; flex: 1;
margin-right: 0; margin-right: 0;
text-align: center; text-align: center;
white-space: normal;
word-break: break-word;
padding: 0 10rpx; padding: 0 10rpx;
} }
@@ -1482,4 +1496,4 @@ export default {
margin-left: 20rpx; margin-left: 20rpx;
} }
} }
</style> </style>

View File

@@ -6,14 +6,14 @@
<view class="nav-container"> <view class="nav-container">
<!-- 头像 --> <!-- 头像 -->
<image <image
:src="userInfo.avatar_url || '/static/default-avatar.png'" :src="userInfo.avatar_url != '' ? userInfo.avatar_url : '/static/default-avatar.png'"
class="nav-avatar" class="nav-avatar"
@click="editProfile" @click="editProfile"
/> />
<!-- 用户信息横向排列 (名字、积分、余额、优惠券) --> <!-- 用户信息横向排列 (名字、积分、余额、优惠券) -->
<view class="nav-user-stats"> <view class="nav-user-stats">
<text class="nav-user-name">{{ userInfo.nickname || userInfo.phone }}</text> <text class="nav-user-name">{{ userInfo.nickname != '' ? userInfo.nickname : userInfo.phone }}</text>
<view class="nav-stat-item" @click="goToPoints"> <view class="nav-stat-item" @click="goToPoints">
<text class="nav-stat-label">积分</text> <text class="nav-stat-label">积分</text>
@@ -40,6 +40,7 @@
</view> </view>
</view> </view>
<scroll-view class="profile-scroll-content" direction="vertical" style="flex:1; height: 0; width: 100%;">
<!-- 导航栏占位符 - 恢复 --> <!-- 导航栏占位符 - 恢复 -->
<view :style="{ height: (statusBarHeight + 10) + 'px' }"></view> <view :style="{ height: (statusBarHeight + 10) + 'px' }"></view>
@@ -243,6 +244,7 @@
</view> </view>
</view> </view>
</view> --> </view> -->
</scroll-view>
</view> </view>
</template> </template>
@@ -381,8 +383,10 @@ export default {
// 按时间倒序 (created_at) // 按时间倒序 (created_at)
this.allOrders.sort((a: any, b: any) => { this.allOrders.sort((a: any, b: any) => {
const timeA = new Date(a.created_at || 0).getTime() const dateA = a['created_at']
const timeB = new Date(b.created_at || 0).getTime() const dateB = b['created_at']
const timeA = new Date(dateA != null ? dateA : 0).getTime()
const timeB = new Date(dateB != null ? dateB : 0).getTime()
return timeB - timeA return timeB - timeA
}) })
@@ -415,12 +419,13 @@ export default {
'shipped': '待收货订单', 'shipped': '待收货订单',
'review': '待评价订单' 'review': '待评价订单'
} }
return titles[this.currentOrderTab] || '我的订单' const title = titles[this.currentOrderTab]
return title != null ? title : '我的订单'
}, },
initPage() { initPage() {
const systemInfo = uni.getSystemInfoSync() const systemInfo = uni.getSystemInfoSync()
this.statusBarHeight = systemInfo.statusBarHeight || 0 this.statusBarHeight = systemInfo.statusBarHeight ?? 0
}, },
async loadUserProfile() { async loadUserProfile() {
try { try {
@@ -436,19 +441,19 @@ export default {
let uGender = 0 let uGender = 0
if (profile instanceof UTSJSONObject) { if (profile instanceof UTSJSONObject) {
uId = profile.getString('user_id') || '' uId = profile.getString('user_id') ?? ''
uPhone = profile.getString('phone') || '' uPhone = profile.getString('phone') ?? ''
uEmail = profile.getString('email') || '' uEmail = profile.getString('email') ?? ''
uNickname = profile.getString('nickname') || '' uNickname = profile.getString('nickname') ?? ''
uAvatar = profile.getString('avatar_url') || '' uAvatar = profile.getString('avatar_url') ?? ''
uGender = profile.getNumber('gender') || 0 uGender = profile.getNumber('gender') ?? 0
} else { } else {
uId = (profile['user_id'] as string) || '' uId = (profile['user_id'] as string) ?? ''
uPhone = (profile['phone'] as string) || '' uPhone = (profile['phone'] as string) ?? ''
uEmail = (profile['email'] as string) || '' uEmail = (profile['email'] as string) ?? ''
uNickname = (profile['nickname'] as string) || '' uNickname = (profile['nickname'] as string) ?? ''
uAvatar = (profile['avatar_url'] as string) || '' uAvatar = (profile['avatar_url'] as string) ?? ''
uGender = (profile['gender'] as number) || 0 uGender = (profile['gender'] as number) ?? 0
} }
// 如果昵称为空,使用手机号脱敏显示 // 如果昵称为空,使用手机号脱敏显示
@@ -460,8 +465,8 @@ export default {
id: uId, id: uId,
phone: uPhone, phone: uPhone,
email: uEmail, email: uEmail,
nickname: uNickname || '微信用户', nickname: uNickname != '' ? uNickname : '微信用户',
avatar_url: uAvatar || '/static/default-avatar.png', avatar_url: uAvatar != '' ? uAvatar : '/static/default-avatar.png',
gender: uGender, gender: uGender,
user_type: 1, user_type: 1,
status: 1, status: 1,
@@ -556,36 +561,50 @@ export default {
getUserLevel(): string { getUserLevel(): string {
const levels = ['新手', '铜牌会员', '银牌会员', '金牌会员', '钻石会员'] const levels = ['新手', '铜牌会员', '银牌会员', '金牌会员', '钻石会员']
return levels[this.userStats.level] || '新手' if (this.userStats.level >= 0 && this.userStats.level < levels.length) {
return levels[this.userStats.level]
}
return '新手'
}, },
getOrderStatusText(status: number): string { getOrderStatusText(status: number): string {
const statusTexts = ['异常', '待支付', '待发货', '待收货', '已完成', '已取消'] const statusTexts = ['异常', '待支付', '待发货', '待收货', '已完成', '已取消']
return statusTexts[status] || '未知' if (status >= 0 && status < statusTexts.length) {
return statusTexts[status]
}
return '未知'
}, },
getOrderStatusClass(status: number): string { getOrderStatusClass(status: number): string {
const statusClasses = ['error', 'pending', 'processing', 'shipping', 'completed', 'cancelled'] const statusClasses = ['error', 'pending', 'processing', 'shipping', 'completed', 'cancelled']
return statusClasses[status] || 'error' if (status >= 0 && status < statusClasses.length) {
return statusClasses[status]
}
return 'error'
}, },
getOrderMainImage(order: any): string { getOrderMainImage(order: any): string {
// 尝试从 ml_order_items 获取第一张图 // 尝试从 ml_order_items 获取第一张图
const items = order['ml_order_items'] as any[] const items = order['ml_order_items'] as any[]
if (items && items.length > 0) { if (items != null && items.length > 0) {
const firstItem = items[0] const firstItem = items[0]
// 数据库字段通常是 image_url // 数据库字段通常是 image_url
const img = firstItem['image_url'] || firstItem['product_image'] const imgUrl = firstItem['image_url'] as string
if (img) return img as string const prodImg = firstItem['product_image'] as string
const img = (imgUrl != null && imgUrl != '') ? imgUrl : prodImg
if (img != null && img != '') return img
} }
return '/static/product1.jpg' return '/static/product1.jpg'
}, },
getOrderTitle(order: any): string { getOrderTitle(order: any): string {
const items = order['ml_order_items'] as any[] const items = order['ml_order_items'] as any[]
if (items && items.length > 0) { if (items != null && items.length > 0) {
const firstItem = items[0] const firstItem = items[0]
const name = (firstItem['product_name'] || '商品') as string const pName = firstItem['product_name'] as string
const name = (pName != null && pName != '') ? pName : '商品'
if (items.length > 1) { if (items.length > 1) {
return `${name} 等${items.length}件商品` return `${name} 等${items.length}件商品`
} }
@@ -787,9 +806,10 @@ export default {
<style> <style>
.consumer-profile { .consumer-profile {
background-color: #f5f5f5; background-color: #f5f5f5;
min-height: 100vh; flex: 1;
display: flex;
flex-direction: column;
} }
/* 智能顶部导航栏 */ /* 智能顶部导航栏 */
.smart-navbar { .smart-navbar {
position: fixed; position: fixed;
@@ -833,7 +853,8 @@ export default {
font-weight: bold; font-weight: bold;
color: white; color: white;
margin-right: 12px; margin-right: 12px;
max-width: 30%; /* 限制名字宽度 */ /* max-width: 30%; REMOVED */
width: 100px; /* Use fixed width approx */
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
@@ -886,7 +907,7 @@ export default {
height: 32px; height: 32px;
background: rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.2);
border-radius: 16px; border-radius: 16px;
cursor: pointer; /* cursor: pointer; REMOVED */
} }
.action-icon { .action-icon {
@@ -1071,7 +1092,11 @@ export default {
.order-actions { .order-actions {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
gap: 15rpx; /* gap: 15rpx; REMOVED */
}
.order-actions .action-btn {
margin-left: 15px; /* Replace gap */
} }
.action-btn { .action-btn {
@@ -1100,7 +1125,7 @@ export default {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: wrap; /* 允许换行 */ flex-wrap: wrap; /* 允许换行 */
gap: 16px 0; /* 行间距16px列间距由 flex 控制 */ /* gap: 16px 0; REMOVED */
justify-content: flex-start; /* 从左开始排列 */ justify-content: flex-start; /* 从左开始排列 */
} }
@@ -1111,6 +1136,7 @@ export default {
align-items: center; align-items: center;
position: relative; position: relative;
box-sizing: border-box; /* 确保 padding 不影响宽度 */ box-sizing: border-box; /* 确保 padding 不影响宽度 */
margin-bottom: 16px; /* Replace gap row */
} }
.service-icon { .service-icon {
@@ -1138,7 +1164,7 @@ export default {
.stats-period { .stats-period {
display: flex; display: flex;
gap: 30rpx; /* gap: 30rpx; REMOVED */
margin-bottom: 30rpx; margin-bottom: 30rpx;
} }
@@ -1147,6 +1173,7 @@ export default {
color: #666; color: #666;
padding: 12rpx 24rpx; padding: 12rpx 24rpx;
border-radius: 20rpx; border-radius: 20rpx;
margin-right: 30rpx; /* Replace gap */
background-color: #f0f0f0; background-color: #f0f0f0;
} }
@@ -1157,7 +1184,7 @@ export default {
.stats-content { .stats-content {
display: flex; display: flex;
gap: 20rpx; /* gap: 20rpx; REMOVED */
} }
.stat-card { .stat-card {
@@ -1166,6 +1193,11 @@ export default {
padding: 30rpx 0; padding: 30rpx 0;
background-color: #f8f9fa; background-color: #f8f9fa;
border-radius: 10rpx; border-radius: 10rpx;
margin-right: 20rpx; /* Replace gap */
}
.stat-card:last-child {
margin-right: 0;
} }
.stat-value { .stat-value {

View File

@@ -1,6 +1,6 @@
<template> <template>
<view class="red-packets-page"> <view class="red-packets-page">
<view class="tab-header"> <view class="tab-header" style="position: fixed; top: 0; left: 0; right: 0; z-index: 10;">
<text <text
class="tab-item" class="tab-item"
:class="{ active: currentTab === 0 }" :class="{ active: currentTab === 0 }"
@@ -82,19 +82,19 @@ const loadData = async () => {
let createdAt = '' let createdAt = ''
if (item instanceof UTSJSONObject) { if (item instanceof UTSJSONObject) {
id = item.getString('id') || '' id = item.getString('id') ?? ''
amount = item.getNumber('amount') || 0 amount = item.getNumber('amount') ?? 0
name = item.getString('name') || '' name = item.getString('name') ?? ''
status = item.getNumber('status') || 0 status = item.getNumber('status') ?? 0
expireAt = item.getString('expire_at') || '' expireAt = item.getString('expire_at') ?? ''
createdAt = item.getString('created_at') || '' createdAt = item.getString('created_at') ?? ''
} else { } else {
id = (item['id'] as string) || '' id = (item['id'] as string) ?? ''
amount = (item['amount'] as number) || 0 amount = (item['amount'] as number) ?? 0
name = (item['name'] as string) || '' name = (item['name'] as string) ?? ''
status = (item['status'] as number) || 0 status = (item['status'] as number) ?? 0
expireAt = (item['expire_at'] as string) || '' expireAt = (item['expire_at'] as string) ?? ''
createdAt = (item['created_at'] as string) || '' createdAt = (item['created_at'] as string) ?? ''
} }
return { return {
@@ -127,7 +127,7 @@ const getStatusText = (status: number): string => {
} }
const formatTime = (timeStr: string): string => { const formatTime = (timeStr: string): string => {
if (!timeStr) return '永久有效' if (timeStr == '') return '永久有效'
const date = new Date(timeStr) const date = new Date(timeStr)
return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}` return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`
} }
@@ -136,18 +136,14 @@ const formatTime = (timeStr: string): string => {
<style> <style>
.red-packets-page { .red-packets-page {
background-color: #f5f5f5; background-color: #f5f5f5;
min-height: 100vh; flex: 1;
display: flex;
flex-direction: column;
} }
.tab-header { .tab-header {
display: flex; display: flex;
background-color: #fff; background-color: #fff;
padding: 10px 0; padding: 10px 0;
position: sticky; /* position: sticky is removed in flavor of inline fixed style */
top: 0;
z-index: 10;
} }
.tab-item { .tab-item {

View File

@@ -71,7 +71,7 @@ const submitReview = () => {
<style scoped> <style scoped>
.review-page { .review-page {
min-height: 100vh; flex: 1;
background-color: #ffffff; background-color: #ffffff;
padding: 20px; padding: 20px;
} }
@@ -85,7 +85,6 @@ const submitReview = () => {
font-size: 20px; font-size: 20px;
font-weight: bold; font-weight: bold;
color: #333; color: #333;
display: block;
margin-bottom: 10px; margin-bottom: 10px;
} }
@@ -109,11 +108,11 @@ const submitReview = () => {
.stars { .stars {
display: flex; display: flex;
gap: 10px;
margin-bottom: 10px; margin-bottom: 10px;
} }
.star { .star {
margin-right: 10px;
font-size: 32px; font-size: 32px;
color: #ddd; color: #ddd;
transition: color 0.2s; transition: color 0.2s;

View File

@@ -103,6 +103,32 @@
import { ref, onMounted, watch } from 'vue' import { ref, onMounted, watch } from 'vue'
import { supabaseService } from '@/utils/supabaseService.uts' import { supabaseService } from '@/utils/supabaseService.uts'
type RefundStatusHistoryItem = {
status: number
remark: string
created_at: string
}
type RefundProductInfo = {
images: string[]
}
type RefundOrderItem = {
id: string
product_name: string
sku_specifications: any
price: number
quantity: number
product?: RefundProductInfo
}
type RefundOrderInfo = {
id: string
order_no: string
created_at: string
order_items: RefundOrderItem[]
}
type RefundType = { type RefundType = {
id: string id: string
user_id: string user_id: string
@@ -112,27 +138,9 @@ type RefundType = {
refund_reason: string refund_reason: string
refund_amount: number refund_amount: number
status: number // 1:待处理 2:处理中 3:已完成 4:已取消 5:已拒绝 status: number // 1:待处理 2:处理中 3:已完成 4:已取消 5:已拒绝
status_history: Array<{ status_history: RefundStatusHistoryItem[] | null
status: number
remark: string
created_at: string
}> | null
created_at: string created_at: string
order?: { order?: RefundOrderInfo
id: string
order_no: string
created_at: string
order_items: Array<{
id: string
product_name: string
sku_specifications: any
price: number
quantity: number
product?: {
images: string[]
}
}>
}
} }
type TabCountsType = { type TabCountsType = {
@@ -178,7 +186,7 @@ const loadRefunds = async (loadMore: boolean = false) => {
try { try {
const userId = getCurrentUserId() const userId = getCurrentUserId()
if (!userId) { if (userId == '') {
uni.navigateTo({ uni.navigateTo({
url: '/pages/user/login' url: '/pages/user/login'
}) })
@@ -198,28 +206,28 @@ const loadRefunds = async (loadMore: boolean = false) => {
// Map data to UI structure (RefundType) // Map data to UI structure (RefundType)
const newRefunds = rawData.map((item: any): RefundType => { const newRefunds = rawData.map((item: any): RefundType => {
const orderObj = item.order || {} const orderObj: any = item['order'] ?? {}
const dbItems = orderObj.ml_order_items || [] const dbItems: any[] = (orderObj['ml_order_items'] as any[]) ?? []
const uiItems = dbItems.map((di: any) : any => ({ const uiItems = dbItems.map((di: any) : RefundOrderItem => ({
id: di.id || '', id: di['id'] ?? '',
product_name: di.product_name, product_name: di['product_name'] ?? '',
sku_specifications: di.specifications, sku_specifications: di['specifications'],
price: 0, price: 0,
quantity: di.quantity || 1, quantity: di['quantity'] ?? 1,
product: { images: [di.image_url || '/static/default-product.png'] } product: { images: [di['image_url'] ?? '/static/default-product.png'] }
})) }))
return { return {
id: item.id, id: item['id'],
user_id: item.user_id, user_id: item['user_id'],
order_id: item.order_id, order_id: item['order_id'],
refund_no: item.refund_no, refund_no: item['refund_no'],
refund_type: item.refund_type, refund_type: item['refund_type'],
refund_reason: item.refund_reason, refund_reason: item['refund_reason'],
refund_amount: Number(item.refund_amount), refund_amount: Number(item['refund_amount']),
status: item.status, status: item['status'],
// Handle missing timeline by defaulting or leaving empty // Handle missing timeline by defaulting or leaving empty
status_history: item.status_history || [], status_history: (item['status_history'] as RefundStatusHistoryItem[]) ?? [],
created_at: item.created_at, created_at: item.created_at,
order: { order: {
id: item.order_id, id: item.order_id,
@@ -249,7 +257,7 @@ const loadRefunds = async (loadMore: boolean = false) => {
// 加载标签页计数 // 加载标签页计数
const loadTabCounts = async () => { const loadTabCounts = async () => {
const userId = getCurrentUserId() const userId = getCurrentUserId()
if (!userId) return if (userId == '') return
try { try {
const { count, error } = await supa const { count, error } = await supa
@@ -263,7 +271,7 @@ const loadTabCounts = async () => {
return return
} }
tabCounts.value.processing = count || 0 tabCounts.value.processing = count ?? 0
} catch (err) { } catch (err) {
console.error('加载计数异常:', err) console.error('加载计数异常:', err)
} }
@@ -272,7 +280,7 @@ const loadTabCounts = async () => {
// 获取当前用户ID // 获取当前用户ID
const getCurrentUserId = (): string => { const getCurrentUserId = (): string => {
const userStore = uni.getStorageSync('userInfo') const userStore = uni.getStorageSync('userInfo')
return userStore?.id || '' return userStore['id'] ?? ''
} }
// 获取状态文本 // 获取状态文本
@@ -284,7 +292,7 @@ const getStatusText = (status: number): string => {
4: '已取消', 4: '已取消',
5: '已拒绝' 5: '已拒绝'
} }
return statusMap[status] || '未知状态' return statusMap[status] ?? '未知状态'
} }
// 获取状态样式类 // 获取状态样式类
@@ -296,21 +304,21 @@ const getStatusClass = (status: number): string => {
4: 'status-cancelled', 4: 'status-cancelled',
5: 'status-rejected' 5: 'status-rejected'
} }
return classMap[status] || 'status-unknown' return classMap[status] ?? 'status-unknown'
} }
// 获取商品图片 // 获取商品图片
const getProductImage = (refund: RefundType): string => { const getProductImage = (refund: RefundType): string => {
const firstItem = refund.order?.order_items?.[0] const firstItem = refund.order?.order_items?.[0]
if (!firstItem?.product?.images?.[0]) { if (firstItem?.product?.images == null || firstItem?.product?.images.length == 0) {
return '/static/default-product.png' return '/static/default-product.png'
} }
return firstItem.product.images[0] return firstItem.product!.images[0]
} }
// 获取商品名称 // 获取商品名称
const getProductName = (refund: RefundType): string => { const getProductName = (refund: RefundType): string => {
const items = refund.order?.order_items || [] const items = refund.order?.order_items ?? []
if (items.length === 0) return '未知商品' if (items.length === 0) return '未知商品'
if (items.length === 1) { if (items.length === 1) {
@@ -322,7 +330,7 @@ const getProductName = (refund: RefundType): string => {
// 格式化时间 // 格式化时间
const formatTime = (timeStr?: string): string => { const formatTime = (timeStr?: string): string => {
if (!timeStr) return '' if (timeStr == null || timeStr == '') return ''
const date = new Date(timeStr) const date = new Date(timeStr)
const month = (date.getMonth() + 1).toString().padStart(2, '0') const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0') const day = date.getDate().toString().padStart(2, '0')
@@ -401,7 +409,7 @@ const cancelRefund = (refund: RefundType) => {
.from('refunds') .from('refunds')
.update({ .update({
status: 4, // 已取消 status: 4, // 已取消
status_history: [...(refund.status_history || []), { status_history: [...(refund.status_history ?? []), {
status: 4, status: 4,
remark: '用户取消申请', remark: '用户取消申请',
created_at: new Date().toISOString() created_at: new Date().toISOString()
@@ -511,7 +519,7 @@ const goBack = () => {
.refund-page { .refund-page {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100vh; flex: 1;
background-color: #f5f5f5; background-color: #f5f5f5;
} }
@@ -551,16 +559,7 @@ const goBack = () => {
.refund-tab.active { .refund-tab.active {
color: #007aff; color: #007aff;
} border-bottom: 2px solid #007aff;
.refund-tab.active::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background-color: #007aff;
} }
.tab-text { .tab-text {
@@ -725,7 +724,7 @@ const goBack = () => {
.refund-amount { .refund-amount {
display: flex; display: flex;
align-items: baseline; align-items: flex-end;
} }
.amount-label { .amount-label {
@@ -783,32 +782,30 @@ const goBack = () => {
color: #333333; color: #333333;
font-weight: bold; font-weight: bold;
margin-bottom: 3px; margin-bottom: 3px;
display: block;
} }
.step-time { .step-time {
font-size: 12px; font-size: 12px;
color: #999999; color: #999999;
margin-bottom: 3px; margin-bottom: 3px;
display: block;
} }
.step-desc { .step-desc {
font-size: 12px; font-size: 12px;
color: #666666; color: #666666;
display: block;
} }
.refund-actions { .refund-actions {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
gap: 10px; /* gap: 10px; removed for uni-app-x */
padding-top: 15px; padding-top: 15px;
border-top: 1px solid #f5f5f5; border-top: 1px solid #f5f5f5;
} }
.action-btn { .action-btn {
padding: 8px 15px; margin-left: 10px;
padding: 6px 15px;
border-radius: 15px; border-radius: 15px;
font-size: 12px; font-size: 12px;
border: 1px solid; border: 1px solid;

View File

@@ -18,7 +18,7 @@
<view class="products-section"> <view class="products-section">
<view v-for="(item, index) in orderItems" :key="item.id" class="product-review"> <view v-for="(item, index) in orderItems" :key="item.id" class="product-review">
<view class="product-header"> <view class="product-header">
<image class="product-image" :src="item.product_image || '/static/default-product.png'" /> <image class="product-image" :src="item.product_image ?? '/static/default-product.png'" />
<view class="product-info"> <view class="product-info">
<text class="product-name">{{ item.product_name }}</text> <text class="product-name">{{ item.product_name }}</text>
<text v-if="item.sku_specifications" class="product-spec">{{ getSpecText(item.sku_specifications) }}</text> <text v-if="item.sku_specifications" class="product-spec">{{ getSpecText(item.sku_specifications) }}</text>
@@ -46,7 +46,7 @@
v-model="contents[index]" v-model="contents[index]"
placeholder="请写下您的使用感受,分享给其他小伙伴吧" placeholder="请写下您的使用感受,分享给其他小伙伴吧"
maxlength="500" /> maxlength="500" />
<text class="word-count">{{ contents[index]?.length || 0 }}/500</text> <text class="word-count">{{ contents[index]?.length ?? 0 }}/500</text>
</view> </view>
<!-- 图片上传 --> <!-- 图片上传 -->
@@ -143,7 +143,8 @@
</template> </template>
<script setup lang="uts"> <script setup lang="uts">
import { ref, onMounted, computed } from 'vue' import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import supa from '@/components/supadb/aksupainstance.uts' import supa from '@/components/supadb/aksupainstance.uts'
type OrderItemType = { type OrderItemType = {
@@ -163,7 +164,7 @@ type MerchantType = {
} }
const orderId = ref<string>('') const orderId = ref<string>('')
const order = ref<any>(null) const order = ref<any>({})
const orderItems = ref<Array<OrderItemType>>([]) const orderItems = ref<Array<OrderItemType>>([])
const merchant = ref<MerchantType | null>(null) const merchant = ref<MerchantType | null>(null)
const ratings = ref<Array<number>>([]) const ratings = ref<Array<number>>([])
@@ -185,15 +186,10 @@ const canSubmit = computed(() => {
}) })
// 生命周期 // 生命周期
onMounted(() => { onLoad((options: any) => {
const pages = getCurrentPages() const optObj = (options instanceof UTSJSONObject) ? (options as UTSJSONObject) : (JSON.parse(JSON.stringify(options ?? {})) as UTSJSONObject)
const currentPage = pages[pages.length - 1] orderId.value = optObj.getString('orderId') ?? ''
const options = currentPage.options as any if (orderId.value != '') loadOrderData()
if (options.orderId) {
orderId.value = options.orderId
loadOrderData()
}
}) })
// 加载订单数据 // 加载订单数据
@@ -226,9 +222,9 @@ const loadOrderData = async () => {
return return
} }
orderItems.value = (itemsData || []).map((item: any) => ({ orderItems.value = (itemsData ?? []).map((item: any) => ({
...item, ...item,
product_image: item.product?.images?.[0] || '/static/default-product.png' product_image: item.product?.images?.[0] ?? '/static/default-product.png'
})) }))
// 初始化评分和内容数组 // 初始化评分和内容数组
@@ -245,7 +241,7 @@ const loadOrderData = async () => {
.eq('id', order.value.merchant_id) .eq('id', order.value.merchant_id)
.single() .single()
if (!merchantError) { if (merchantError == null) {
merchant.value = merchantData merchant.value = merchantData
} }
} }
@@ -257,7 +253,7 @@ const loadOrderData = async () => {
// 格式化时间 // 格式化时间
const formatTime = (timeStr?: string): string => { const formatTime = (timeStr?: string): string => {
if (!timeStr) return '' if (timeStr == null) return ''
const date = new Date(timeStr) const date = new Date(timeStr)
const year = date.getFullYear() const year = date.getFullYear()
const month = (date.getMonth() + 1).toString().padStart(2, '0') const month = (date.getMonth() + 1).toString().padStart(2, '0')
@@ -267,7 +263,7 @@ const formatTime = (timeStr?: string): string => {
// 获取规格文本 // 获取规格文本
const getSpecText = (specs: any): string => { const getSpecText = (specs: any): string => {
if (!specs) return '' if (specs == null) return ''
if (typeof specs === 'object') { if (typeof specs === 'object') {
return Object.keys(specs) return Object.keys(specs)
.map(key => `${key}: ${specs[key]}`) .map(key => `${key}: ${specs[key]}`)
@@ -279,7 +275,7 @@ const getSpecText = (specs: any): string => {
// 获取评分文本 // 获取评分文本
const getRatingText = (rating: number): string => { const getRatingText = (rating: number): string => {
const texts = ['非常差', '差', '一般', '好', '非常好'] const texts = ['非常差', '差', '一般', '好', '非常好']
return texts[rating - 1] || '未评价' return texts[rating - 1] ?? '未评价'
} }
// 设置商品评分 // 设置商品评分
@@ -352,7 +348,7 @@ const submitReview = async () => {
try { try {
const userId = getCurrentUserId() const userId = getCurrentUserId()
if (!userId) { if (userId == '') {
uni.showToast({ uni.showToast({
title: '用户信息错误', title: '用户信息错误',
icon: 'none' icon: 'none'
@@ -366,7 +362,7 @@ const submitReview = async () => {
product_id: item.product_id, product_id: item.product_id,
order_id: orderId.value, order_id: orderId.value,
rating: ratings.value[index], rating: ratings.value[index],
content: contents.value[index] || '', content: contents.value[index] != '' ? contents.value[index] : '',
images: images.value[index], images: images.value[index],
is_anonymous: anonymous.value is_anonymous: anonymous.value
})) }))
@@ -436,7 +432,7 @@ const submitReview = async () => {
// 获取当前用户ID // 获取当前用户ID
const getCurrentUserId = (): string => { const getCurrentUserId = (): string => {
const userStore = uni.getStorageSync('userInfo') const userStore = uni.getStorageSync('userInfo')
return userStore?.id || '' return userStore?.getString('id') ?? ''
} }
// 返回 // 返回
@@ -449,7 +445,7 @@ const goBack = () => {
.review-page { .review-page {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100vh; flex: 1;
background-color: #f5f5f5; background-color: #f5f5f5;
} }
@@ -532,13 +528,13 @@ const goBack = () => {
color: #333333; color: #333333;
line-height: 1.4; line-height: 1.4;
margin-bottom: 5px; margin-bottom: 5px;
display: block; /* display: block; removed */
} }
.product-spec { .product-spec {
font-size: 12px; font-size: 12px;
color: #999999; color: #999999;
display: block; /* display: block; removed */
} }
.rating-section { .rating-section {
@@ -555,16 +551,17 @@ const goBack = () => {
.rating-stars { .rating-stars {
display: flex; display: flex;
gap: 10px; /* gap: 10px; removed */
} }
.rating-stars.small { .rating-stars.small {
gap: 5px; /* gap: 5px; removed */
} }
.star-icon { .star-icon {
font-size: 24px; font-size: 24px;
color: #cccccc; color: #cccccc;
margin-right: 10px;
} }
.star-icon.active { .star-icon.active {
@@ -593,7 +590,7 @@ const goBack = () => {
} }
.word-count { .word-count {
display: block; /* display: block; removed */
text-align: right; text-align: right;
font-size: 12px; font-size: 12px;
color: #999999; color: #999999;
@@ -605,7 +602,7 @@ const goBack = () => {
} }
.images-label { .images-label {
display: block; /* display: block; removed */
font-size: 14px; font-size: 14px;
color: #333333; color: #333333;
margin-bottom: 10px; margin-bottom: 10px;
@@ -614,10 +611,12 @@ const goBack = () => {
.images-grid { .images-grid {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 10px; /* gap: 10px; removed */
} }
.image-item { .image-item {
margin-right: 10px;
margin-bottom: 10px;
width: 70px; width: 70px;
height: 70px; height: 70px;
border-radius: 5px; border-radius: 5px;
@@ -644,7 +643,9 @@ const goBack = () => {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
margin-right: 10px;
margin-bottom: 10px;
.upload-btn { .upload-btn {
width: 70px; width: 70px;
height: 70px; height: 70px;
@@ -680,11 +681,11 @@ const goBack = () => {
.switch-label { .switch-label {
font-size: 14px; font-size: 14px;
color: #333333; /* display: block; removed */
} }
.anonymous-tip { .anonymous-tip {
display: block; /* display: block; removed */
font-size: 12px; font-size: 12px;
color: #999999; color: #999999;
} }
@@ -696,7 +697,7 @@ const goBack = () => {
} }
.section-title { .section-title {
display: block; /* display: block; removed */
font-size: 16px; font-size: 16px;
font-weight: bold; font-weight: bold;
color: #333333; color: #333333;
@@ -722,7 +723,7 @@ const goBack = () => {
} }
.tip-title { .tip-title {
display: block; /* display: block; removed */
font-size: 16px; font-size: 16px;
font-weight: bold; font-weight: bold;
color: #333333; color: #333333;
@@ -730,7 +731,7 @@ const goBack = () => {
} }
.tip-item { .tip-item {
display: block; /* display: block; removed */
font-size: 12px; font-size: 12px;
color: #666666; color: #666666;
line-height: 1.6; line-height: 1.6;
@@ -761,4 +762,4 @@ const goBack = () => {
background-color: #cccccc; background-color: #cccccc;
opacity: 0.6; opacity: 0.6;
} }
</style> </style>

View File

@@ -285,7 +285,7 @@ onMounted(() => {
const initPage = () => { const initPage = () => {
try { try {
const systemInfo = uni.getSystemInfoSync() const systemInfo = uni.getSystemInfoSync()
statusBarHeight.value = systemInfo.statusBarHeight || 0 statusBarHeight.value = systemInfo.statusBarHeight ?? 0
const windowHeight = systemInfo.windowHeight const windowHeight = systemInfo.windowHeight
// 减去头部高度 (约60px + statusBarHeight) // 减去头部高度 (约60px + statusBarHeight)
scrollHeight.value = windowHeight - (60 + statusBarHeight.value) scrollHeight.value = windowHeight - (60 + statusBarHeight.value)
@@ -343,7 +343,7 @@ const loadData = async () => {
id: p.id, id: p.id,
name: p.name, name: p.name,
price: p.base_price, price: p.base_price,
image: p.main_image_url || '/static/default.jpg', image: p.main_image_url ?? '/static/default.jpg',
sales: typeof p.sale_count === 'number' ? p.sale_count : 0 sales: typeof p.sale_count === 'number' ? p.sale_count : 0
})) }))
@@ -386,7 +386,7 @@ const saveSearchHistory = () => {
} }
const addToHistory = (keyword: string) => { const addToHistory = (keyword: string) => {
if (!keyword) return if (keyword == '') return
const index = searchHistory.value.indexOf(keyword) const index = searchHistory.value.indexOf(keyword)
if (index > -1) { if (index > -1) {
searchHistory.value.splice(index, 1) searchHistory.value.splice(index, 1)
@@ -418,30 +418,11 @@ const deleteHistoryItem = (index: number) => {
const searchSuggestions = ref<string[]>([]) const searchSuggestions = ref<string[]>([])
let suggestTimer = 0 let suggestTimer = 0
const fetchSuggestions = async (kw: string) => {
if (!kw || showResults.value) return
// 简单搜索前5个相关商品作为建议
try {
const res = await supabaseService.searchProducts(kw.trim(), 1, 5)
if (res.data.length > 0) {
// 去重
const names = res.data.map((p:any) => p.name as string)
// @ts-ignore
searchSuggestions.value = [...new Set(names)]
} else {
searchSuggestions.value = []
}
} catch(e) {
searchSuggestions.value = []
}
}
// 搜索逻辑 // 搜索逻辑
const onInput = (e: any) => { const onInput = (e: any) => {
const val = e.detail.value const val = e.detail.value
searchKeyword.value = val searchKeyword.value = val
if (!val) { if (val == '') {
showResults.value = false showResults.value = false
searchSuggestions.value = [] searchSuggestions.value = []
return return
@@ -454,13 +435,37 @@ const onInput = (e: any) => {
}, 300) }, 300)
} }
const fetchSuggestions = async (kw: string) => {
if (kw == '' || showResults.value) return
// 简单搜索前5个相关商品作为建议
try {
const res = await supabaseService.searchProducts(kw.trim(), 1, 5)
if (Array.isArray(res.data) && res.data.length > 0) {
// 去重
const names = res.data.map((p:any) :string => {
if(p instanceof UTSJSONObject){
return p.getString('name') ?? ''
}
return p['name'] as string
})
// @ts-ignore
searchSuggestions.value = Array.from(new Set(names))
} else {
searchSuggestions.value = []
}
} catch(e) {
searchSuggestions.value = []
}
}
const clearSearch = () => { const clearSearch = () => {
searchKeyword.value = '' searchKeyword.value = ''
showResults.value = false showResults.value = false
} }
const onSearch = () => { const onSearch = () => {
if (!searchKeyword.value.trim()) return if (searchKeyword.value.trim() == '') return
addToHistory(searchKeyword.value.trim()) addToHistory(searchKeyword.value.trim())
performSearch() performSearch()
} }
@@ -493,7 +498,7 @@ const performSearch = async () => {
// 使用 Supabase 搜索真实数据 // 使用 Supabase 搜索真实数据
const keyword = searchKeyword.value.trim() const keyword = searchKeyword.value.trim()
if (!keyword) { if (keyword == '') {
loading.value = false loading.value = false
return return
} }
@@ -523,8 +528,8 @@ const performSearch = async () => {
searchShopResults.value = shopResp.data.map((s: any) => ({ searchShopResults.value = shopResp.data.map((s: any) => ({
id: s.id, id: s.id,
name: s.shop_name, name: s.shop_name,
logo: s.shop_logo || '/static/shop_logo_default.png', logo: s.shop_logo ?? '/static/shop_logo_default.png',
productCount: s.product_count || 0 productCount: s.product_count ?? 0
})) }))
} else { } else {
searchShopResults.value = [] searchShopResults.value = []
@@ -543,11 +548,11 @@ const performSearch = async () => {
return { return {
id: p.id, id: p.id,
name: p.name, name: p.name,
image: p.main_image_url || '/static/default.jpg', image: p.main_image_url ?? '/static/default.jpg',
price: p.base_price, price: p.base_price,
specification: p.specification || '标准规格', specification: p.specification ?? '标准规格',
tag: tag, tag: tag,
sales: p.sale_count || 0 sales: p.sale_count ?? 0
} }
}) })
@@ -576,7 +581,7 @@ const switchSort = (type: string) => {
} }
const loadMore = async () => { const loadMore = async () => {
if (loading.value || !hasMore.value || !searchKeyword.value.trim()) return if (loading.value || !hasMore.value || searchKeyword.value.trim() == '') return
loading.value = true loading.value = true
// 增加页码 // 增加页码
@@ -607,11 +612,11 @@ const loadMore = async () => {
return { return {
id: p.id, id: p.id,
name: p.name, name: p.name,
image: p.main_image_url || '/static/default.jpg', image: p.main_image_url ?? '/static/default.jpg',
price: p.base_price, price: p.base_price,
specification: p.specification || '标准规格', specification: p.specification ?? '标准规格',
tag: tag, tag: tag,
sales: p.sale_count || 0 sales: p.sale_count ?? 0
} }
}) })
searchResults.value.push(...newItems) searchResults.value.push(...newItems)
@@ -698,7 +703,7 @@ const goBack = () => {
<style> <style>
.search-page { .search-page {
width: 100%; width: 100%;
height: 100vh; flex: 1; /* Fixed 100vh */
background-color: #f5f5f5; background-color: #f5f5f5;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -764,7 +769,7 @@ const goBack = () => {
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
display: block; /* display: block; REMOVED */
margin-bottom: 2px; margin-bottom: 2px;
} }
@@ -787,7 +792,6 @@ const goBack = () => {
flex-direction: row; /* UVUE 必须显式设置 row */ flex-direction: row; /* UVUE 必须显式设置 row */
align-items: center; align-items: center;
padding: 10px 16px; padding: 10px 16px;
gap: 12px;
width: 100%; /* 确保占满宽度 */ width: 100%; /* 确保占满宽度 */
} }
@@ -799,6 +803,7 @@ const goBack = () => {
justify-content: center; justify-content: center;
width: 32px; /* 固定宽度防止压缩 */ width: 32px; /* 固定宽度防止压缩 */
height: 32px; height: 32px;
margin-right: 12px;
} }
.back-icon { .back-icon {
@@ -875,7 +880,7 @@ const goBack = () => {
.inner-search-text { .inner-search-text {
font-size: 13px; font-size: 13px;
color: #ffffff; color: #ffffff;
font-weight: 500; font-weight: bold;
} }
/* 内容区域 */ /* 内容区域 */
@@ -907,11 +912,12 @@ const goBack = () => {
display: flex; display: flex;
flex-direction: row; /* UVUE 显式设置 row */ flex-direction: row; /* UVUE 显式设置 row */
align-items: center; align-items: center;
gap: 4px; /* gap: 4px; REMOVED */
flex-shrink: 0; /* 防止被压缩 */ flex-shrink: 0; /* 防止被压缩 */
} }
.clear-text { .clear-text {
margin-right: 4px; /* REPLACED gap */
font-size: 12px; font-size: 12px;
color: #999; color: #999;
} }
@@ -929,7 +935,7 @@ const goBack = () => {
.history-tags { .history-tags {
display: flex; display: flex;
flex-direction: row; /* UVUE 显式设置 row */ flex-direction: row; /* UVUE 显式设置 row */
gap: 10px; /* gap: 10px; REMOVED */
flex-wrap: wrap; /* 允许换行 */ flex-wrap: wrap; /* 允许换行 */
padding: 0 4px; padding: 0 4px;
align-items: center; align-items: center;
@@ -941,8 +947,10 @@ const goBack = () => {
border-radius: 16px; border-radius: 16px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; /* gap: 6px; REMOVED */
flex-shrink: 0; /* 防止被压缩 */ flex-shrink: 0; /* 防止被压缩 */
margin-right: 10px; /* REPLACED gap */
margin-bottom: 10px; /* REPLACED gap */
} }
.history-text { .history-text {
@@ -951,6 +959,7 @@ const goBack = () => {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
margin-right: 6px; /* REPLACED gap */
} }
.delete-tag-btn { .delete-tag-btn {
@@ -959,7 +968,7 @@ const goBack = () => {
justify-content: center; justify-content: center;
width: 16px; width: 16px;
height: 16px; height: 16px;
border-radius: 50%; border-radius: 8px;
background-color: #f0f0f0; background-color: #f0f0f0;
} }
@@ -978,10 +987,16 @@ const goBack = () => {
display: flex; display: flex;
flex-direction: row; /* UVUE 显式设置 row */ flex-direction: row; /* UVUE 显式设置 row */
flex-wrap: wrap; /* 允许换行 */ flex-wrap: wrap; /* 允许换行 */
gap: 10px; /* gap: 10px; REMOVED */
padding: 0 4px; padding: 0 4px;
} }
.hot-tag {
/* ... existing styles ... */
margin-right: 10px; /* REPLACED gap */
margin-bottom: 10px; /* REPLACED gap */
}
.hot-tag { .hot-tag {
background-color: #fff; background-color: #fff;
padding: 6px 12px; padding: 6px 12px;
@@ -989,8 +1004,9 @@ const goBack = () => {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
gap: 4px;
flex-shrink: 0; /* 防止被压缩 */ flex-shrink: 0; /* 防止被压缩 */
margin-right: 10px; /* REPLACED gap */
margin-bottom: 10px; /* REPLACED gap */
} }
.hot-tag.hot { .hot-tag.hot {
@@ -1001,7 +1017,7 @@ const goBack = () => {
font-size: 12px; font-size: 12px;
color: #999; color: #999;
font-weight: bold; font-weight: bold;
margin-right: 2px; margin-right: 6px;
} }
.hot-rank.top-three { .hot-rank.top-three {
@@ -1015,6 +1031,7 @@ const goBack = () => {
.hot-icon { .hot-icon {
font-size: 12px; font-size: 12px;
margin-left: 4px;
} }
/* 猜你需要 */ /* 猜你需要 */
@@ -1025,11 +1042,11 @@ const goBack = () => {
.title-with-icon { .title-with-icon {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px;
} }
.section-icon { .section-icon {
font-size: 16px; font-size: 16px;
margin-right: 6px;
} }
.refresh-btn { .refresh-btn {
@@ -1038,9 +1055,11 @@ const goBack = () => {
} }
.guess-grid { .guess-grid {
display: grid; display: flex;
grid-template-columns: repeat(2, 1fr); flex-direction: row;
gap: 10px; flex-wrap: wrap;
justify-content: space-between;
padding: 0 4px;
} }
.guess-item { .guess-item {
@@ -1048,6 +1067,8 @@ const goBack = () => {
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
padding-bottom: 8px; padding-bottom: 8px;
width: 48%;
margin-bottom: 10px;
} }
.guess-img-box { .guess-img-box {
@@ -1074,15 +1095,15 @@ const goBack = () => {
font-size: 13px; font-size: 13px;
color: #333; color: #333;
margin-bottom: 6px; margin-bottom: 6px;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis;
line-height: 1.3;
height: 34px; /* 限制2行高度 */
} }
.guess-price-row { .guess-price-row {
display: flex; display: flex;
align-items: baseline; align-items: flex-end; /* REPLACED baseline */
} }
.price-symbol { .price-symbol {
@@ -1139,19 +1160,18 @@ const goBack = () => {
align-items: center; align-items: center;
margin-bottom: 12px; margin-bottom: 12px;
flex-wrap: wrap; /* 允许换行以适应小屏 */ flex-wrap: wrap; /* 允许换行以适应小屏 */
gap: 8px;
} }
.results-title { .results-title {
font-size: 15px; font-size: 15px;
font-weight: bold; font-weight: bold;
color: #333; color: #333;
margin-right: 8px;
} }
.filter-tabs { .filter-tabs {
display: flex; display: flex;
flex-direction: row; /* UVUE 显式设置 row */ flex-direction: row; /* UVUE 显式设置 row */
gap: 16px;
flex: 1; /* 自适应填充剩余空间 */ flex: 1; /* 自适应填充剩余空间 */
justify-content: flex-end; /* 靠右对齐 */ justify-content: flex-end; /* 靠右对齐 */
} }
@@ -1160,17 +1180,19 @@ const goBack = () => {
font-size: 13px; font-size: 13px;
color: #666; color: #666;
padding: 4px 8px; /* 增加点击区域 */ padding: 4px 8px; /* 增加点击区域 */
margin-left: 16px;
} }
.filter-tab.active { .filter-tab.active {
color: #4CAF50; color: #4CAF50;
font-weight: 500; font-weight: bold; /* REPLACED 500 */
} }
.results-list { .results-list {
display: grid; display: flex;
grid-template-columns: repeat(2, 1fr); /* 默认移动端双列 */ flex-direction: row;
gap: 10px; flex-wrap: wrap;
justify-content: space-between;
padding: 0 4px; padding: 0 4px;
} }
@@ -1178,26 +1200,30 @@ const goBack = () => {
/* 平板设备 (768px以上) */ /* 平板设备 (768px以上) */
@media screen and (min-width: 768px) { @media screen and (min-width: 768px) {
.results-list { .results-list {
grid-template-columns: repeat(3, 1fr); /* 平板显示3列 */
gap: 16px;
padding: 0 16px; padding: 0 16px;
} }
.guess-grid { .result-item {
grid-template-columns: repeat(4, 1fr); /* 猜你喜欢在平板上显示4列 */ width: 32%;
}
.guess-item {
width: 24%; /* 猜你喜欢在平板上显示4列 */
} }
} }
/* 桌面设备 (1024px以上) */ /* 桌面设备 (1024px以上) */
@media screen and (min-width: 1024px) { @media screen and (min-width: 1024px) {
.results-list { .results-list {
grid-template-columns: repeat(4, 1fr); /* 桌面显示4列 */
gap: 20px;
padding: 0 24px; padding: 0 24px;
} }
.result-item {
width: 24%;
}
.guess-grid { .guess-item {
grid-template-columns: repeat(6, 1fr); /* 猜你喜欢在桌面上显示6列 */ width: 16%; /* 猜你喜欢在桌面上显示6列 */
} }
/* 桌面端调整图片高度 */ /* 桌面端调整图片高度 */
@@ -1212,7 +1238,8 @@ const goBack = () => {
padding: 8px; padding: 8px;
display: flex; display: flex;
flex-direction: column; /* 垂直排列 */ flex-direction: column; /* 垂直排列 */
gap: 8px; width: 48%;
margin-bottom: 10px;
} }
.product-image { .product-image {
@@ -1227,19 +1254,17 @@ const goBack = () => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
margin-top: 8px;
} }
.product-name { .product-name {
font-size: 13px; /* 减小字号 */ font-size: 13px; /* 减小字号 */
color: #333; color: #333;
font-weight: 500; font-weight: bold;
line-height: 1.3; line-height: 1.3;
height: 34px; /* 限制高度 */ height: 34px; /* 限制高度 */
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
} }
.product-tags-row { .product-tags-row {
@@ -1261,7 +1286,7 @@ const goBack = () => {
.price-box { .price-box {
color: #ff5000; color: #ff5000;
display: flex; display: flex;
align-items: baseline; align-items: flex-end;
} }
.price-symbol { .price-symbol {
@@ -1270,7 +1295,7 @@ const goBack = () => {
.price-value { .price-value {
font-size: 16px; /* 减小价格字号 */ font-size: 16px; /* 减小价格字号 */
font-weight: 600; font-weight: 700;
} }
.add-cart-btn { .add-cart-btn {
@@ -1302,17 +1327,18 @@ const goBack = () => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 12px;
} }
.error-icon { .error-icon {
font-size: 48px; font-size: 48px;
margin-bottom: 12px;
} }
.error-title { .error-title {
font-size: 16px; font-size: 16px;
font-weight: bold; font-weight: bold;
color: #333; color: #333;
margin-bottom: 12px;
} }
.error-desc { .error-desc {
@@ -1333,16 +1359,10 @@ const goBack = () => {
height: 24px; height: 24px;
border: 2px solid #f0f0f0; border: 2px solid #f0f0f0;
border-top-color: #4CAF50; border-top-color: #4CAF50;
border-radius: 50%; border-radius: 12px;
animation: spin 1s linear infinite;
margin-bottom: 8px; margin-bottom: 8px;
} }
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text { .loading-text {
font-size: 12px; font-size: 12px;
color: #999; color: #999;

View File

@@ -57,17 +57,17 @@
<view class="list-item"> <view class="list-item">
<text class="item-icon">🔔</text> <text class="item-icon">🔔</text>
<text class="item-text">订单消息</text> <text class="item-text">订单消息</text>
<switch :checked="notifications.order" @change="toggleNotification('order')" /> <switch class="settings-switch" :checked="notifications.order" @change="toggleNotification('order')" />
</view> </view>
<view class="list-item"> <view class="list-item">
<text class="item-icon">🎁</text> <text class="item-icon">🎁</text>
<text class="item-text">促销活动</text> <text class="item-text">促销活动</text>
<switch :checked="notifications.promotion" @change="toggleNotification('promotion')" /> <switch class="settings-switch" :checked="notifications.promotion" @change="toggleNotification('promotion')" />
</view> </view>
<view class="list-item"> <view class="list-item">
<text class="item-icon">⭐</text> <text class="item-icon">⭐</text>
<text class="item-text">评价提醒</text> <text class="item-text">评价提醒</text>
<switch :checked="notifications.review" @change="toggleNotification('review')" /> <switch class="settings-switch" :checked="notifications.review" @change="toggleNotification('review')" />
</view> </view>
</view> </view>
</view> </view>
@@ -79,17 +79,17 @@
<view class="list-item"> <view class="list-item">
<text class="item-icon">👁️</text> <text class="item-icon">👁️</text>
<text class="item-text">隐藏购物记录</text> <text class="item-text">隐藏购物记录</text>
<switch :checked="privacy.hidePurchase" @change="togglePrivacy('hidePurchase')" /> <switch class="settings-switch" :checked="privacy.hidePurchase" @change="togglePrivacy('hidePurchase')" />
</view> </view>
<view class="list-item"> <view class="list-item">
<text class="item-icon">🔍</text> <text class="item-icon">🔍</text>
<text class="item-text">允许通过手机号找到我</text> <text class="item-text">允许通过手机号找到我</text>
<switch :checked="privacy.allowSearchByPhone" @change="togglePrivacy('allowSearchByPhone')" /> <switch class="settings-switch" :checked="privacy.allowSearchByPhone" @change="togglePrivacy('allowSearchByPhone')" />
</view> </view>
<view class="list-item"> <view class="list-item">
<text class="item-icon">💬</text> <text class="item-icon">💬</text>
<text class="item-text">接收商家消息</text> <text class="item-text">接收商家消息</text>
<switch :checked="privacy.receiveMerchantMsg" @change="togglePrivacy('receiveMerchantMsg')" /> <switch class="settings-switch" :checked="privacy.receiveMerchantMsg" @change="togglePrivacy('receiveMerchantMsg')" />
</view> </view>
</view> </view>
</view> </view>
@@ -265,7 +265,7 @@ const statusBarHeight = ref(0)
// 生命周期 // 生命周期
onMounted(() => { onMounted(() => {
const systemInfo = uni.getSystemInfoSync() const systemInfo = uni.getSystemInfoSync()
statusBarHeight.value = systemInfo.statusBarHeight || 0 statusBarHeight.value = systemInfo.statusBarHeight
loadUserInfo() loadUserInfo()
loadSettings() loadSettings()
}) })
@@ -565,9 +565,13 @@ const deleteAccount = () => {
title: '注销中...' title: '注销中...'
}) })
const userId = userInfo.value.id || uni.getStorageSync('user_id') let userId = userInfo.value.getString('id')
if (userId == null) {
const storageId = uni.getStorageSync('user_id')
userId = (storageId != null) ? storageId as string : null
}
if (userId) { if (userId != null) {
try { try {
// 标记用户状态为注销 (status=3) // 标记用户状态为注销 (status=3)
await supa await supa
@@ -661,7 +665,7 @@ const deleteAccount = () => {
} }
/* 针对 switch 组件的特殊处理,如果需要显示开关,可能需要调整布局 */ /* 针对 switch 组件的特殊处理,如果需要显示开关,可能需要调整布局 */
.list-item switch { .settings-switch {
transform: scale(0.7); transform: scale(0.7);
margin-top: 5px; margin-top: 5px;
} }
@@ -669,7 +673,7 @@ const deleteAccount = () => {
/* 屏幕宽度大于 480px (大屏手机/平板/PC) 时,启用更宽的网格布局或列表布局 */ /* 屏幕宽度大于 480px (大屏手机/平板/PC) 时,启用更宽的网格布局或列表布局 */
@media screen and (min-width: 480px) { @media screen and (min-width: 480px) {
.list-item { .list-item {
width: calc(50% - 10px); /* 每行两个,留出间隙 */ width: 47%; /* width: calc(50% - 10px); REPLACED */
margin: 5px; margin: 5px;
border: 1px solid #f0f0f0; border: 1px solid #f0f0f0;
border-radius: 8px; border-radius: 8px;
@@ -697,7 +701,7 @@ const deleteAccount = () => {
/* 增加针对手机横屏的媒体查询 */ /* 增加针对手机横屏的媒体查询 */
@media screen and (orientation: landscape) and (max-height: 500px) { @media screen and (orientation: landscape) and (max-height: 500px) {
.list-item { .list-item {
width: calc(25% - 10px); /* 横屏也保持4个一行或者根据需要调整 */ width: 22%; /* width: calc(25% - 10px); REPLACED */
margin: 5px; margin: 5px;
border: 1px solid #f0f0f0; border: 1px solid #f0f0f0;
border-radius: 8px; border-radius: 8px;
@@ -723,7 +727,7 @@ const deleteAccount = () => {
} }
.list-item { .list-item {
width: calc(33.33% - 10px); /* 每行三个 */ width: 31%; /* width: calc(33.33% - 10px); REPLACED */
flex-direction: row; /* PC端保持水平排列 */ flex-direction: row; /* PC端保持水平排列 */
justify-content: flex-start; justify-content: flex-start;
text-align: left; text-align: left;
@@ -743,7 +747,7 @@ const deleteAccount = () => {
.settings-page { .settings-page {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100vh; height: 100%;
background-color: #f5f5f5; background-color: #f5f5f5;
} }
@@ -779,7 +783,6 @@ const deleteAccount = () => {
} }
.section-title { .section-title {
display: block;
font-size: 16px; font-size: 16px;
font-weight: bold; font-weight: bold;
color: #333333; color: #333333;
@@ -853,6 +856,5 @@ const deleteAccount = () => {
.delete-account { .delete-account {
color: #999999; color: #999999;
font-size: 14px; font-size: 14px;
text-decoration: underline; }/* text-decoration: underline; REMOVED */
}
</style> </style>

View File

@@ -1,10 +1,11 @@
<template> <template>
<view class="shop-detail-page"> <view class="shop-detail-page">
<scroll-view class="page-scroll" scroll-y="true" @scrolltolower="onScrollToLower" refresher-enabled="true" @refresherrefresh="onRefresherRefresh" :refresher-triggered="isRefresherTriggered">
<!-- 店铺头部信息 --> <!-- 店铺头部信息 -->
<view class="shop-header"> <view class="shop-header">
<image :src="merchant.shop_banner || '/static/default-banner.png'" class="shop-banner" mode="aspectFill" /> <image :src="merchant.shop_banner != '' ? merchant.shop_banner : '/static/default-banner.png'" class="shop-banner" mode="aspectFill" />
<view class="shop-info-card"> <view class="shop-info-card">
<image :src="merchant.shop_logo || '/static/default-shop.png'" class="shop-logo" /> <image :src="merchant.shop_logo != '' ? merchant.shop_logo : '/static/default-shop.png'" class="shop-logo" />
<view class="shop-basic-info"> <view class="shop-basic-info">
<text class="shop-name">{{ merchant.shop_name }}</text> <text class="shop-name">{{ merchant.shop_name }}</text>
<view class="shop-stats"> <view class="shop-stats">
@@ -21,7 +22,7 @@
</view> </view>
</view> </view>
</view> </view>
<text class="shop-desc">{{ merchant.shop_description || '这家店很懒,什么都没写~' }}</text> <text class="shop-desc">{{ merchant.shop_description != '' ? merchant.shop_description : '这家店很懒,什么都没写~' }}</text>
<!-- 优惠券列表 (新增) --> <!-- 优惠券列表 (新增) -->
<view class="shop-coupons" v-if="coupons.length > 0"> <view class="shop-coupons" v-if="coupons.length > 0">
@@ -63,12 +64,12 @@
</view> </view>
</view> </view>
</view> </view>
</scroll-view>
</view> </view>
</template> </template>
<script setup lang="uts"> <script setup lang="uts">
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { onReachBottom, onPullDownRefresh } from '@dcloudio/uni-app'
import { MerchantType, ProductType } from '@/types/mall-types.uts' import { MerchantType, ProductType } from '@/types/mall-types.uts'
import { supabaseService } from '@/utils/supabaseService.uts' import { supabaseService } from '@/utils/supabaseService.uts'
@@ -97,12 +98,15 @@ const merchant = ref<MerchantType>({
const products = ref<ProductType[]>([]) const products = ref<ProductType[]>([])
const isFollowed = ref(false) const isFollowed = ref(false)
const coupons = ref<any[]>([]) // 新增优惠券 const coupons = ref<any[]>([]) // 新增优惠券
const isRefresherTriggered = ref(false)
onMounted(() => { onMounted(() => {
const pages = getCurrentPages() const pages = getCurrentPages()
const options = pages[pages.length - 1].options as any const options = pages[pages.length - 1].options as any
// Search传递的是 id (shop_id), 其他地方可能传递 merchantId // Search传递的是 id (shop_id), 其他地方可能传递 merchantId
const paramId = (options['merchantId'] || options['id']) as string const mId = options['merchantId']
const pId = options['id']
const paramId = (mId != null ? mId : pId) as string
if (paramId) { if (paramId) {
console.log('Page mounted with params:', paramId) console.log('Page mounted with params:', paramId)
@@ -129,31 +133,41 @@ onMounted(() => {
} }
}) })
onPullDownRefresh(() => { const onRefresherRefresh = () => {
// 下拉刷新 isRefresherTriggered.value = true
currentPage.value = 1 currentPage.value = 1
hasMore.value = true hasMore.value = true
isLoading.value = false isLoading.value = false
if (currentMerchantId.value != '') { if (currentMerchantId.value != '') {
const id = currentMerchantId.value const id = currentMerchantId.value
// 重新加载所有数据 Promise.all([
loadShopData(id) loadShopData(id),
loadCoupons(id) loadCoupons(id),
loadShopProducts(id) loadShopProducts(id)
]).then(() => {
isRefresherTriggered.value = false
})
} else { } else {
setTimeout(() => { setTimeout(() => {
uni.stopPullDownRefresh() isRefresherTriggered.value = false
}, 500) }, 500)
} }
}
const onScrollToLower = () => {
if (hasMore.value && !isLoading.value && currentMerchantId.value != '') {
console.log('Scroll to lower, loading more...')
loadShopProducts(currentMerchantId.value)
}
}
onPullDownRefresh(() => {
onRefresherRefresh()
}) })
onReachBottom(() => { onReachBottom(() => {
// 触底加载更多 onScrollToLower()
if (hasMore.value && !isLoading.value && currentMerchantId.value != '') {
console.log('Reach bottom, loading more...')
loadShopProducts(currentMerchantId.value)
}
}) })
const loadShopData = async (id: string) => { const loadShopData = async (id: string) => {
@@ -166,15 +180,15 @@ const loadShopData = async (id: string) => {
id: shop.id, id: shop.id,
user_id: shop.merchant_id, // 映射关系 user_id: shop.merchant_id, // 映射关系
shop_name: shop.shop_name, shop_name: shop.shop_name,
shop_logo: shop.shop_logo || '/static/default-shop.png', shop_logo: shop.shop_logo != null ? shop.shop_logo : '/static/default-shop.png',
shop_banner: shop.shop_banner || '/static/default-banner.png', shop_banner: shop.shop_banner != null ? shop.shop_banner : '/static/default-banner.png',
shop_description: shop.description || '', shop_description: shop.description != null ? shop.description : '',
contact_name: shop.contact_name || '', contact_name: shop.contact_name != null ? shop.contact_name : '',
contact_phone: shop.contact_phone || '', contact_phone: shop.contact_phone != null ? shop.contact_phone : '',
shop_status: 1, // 默认正常 shop_status: 1, // 默认正常
rating: shop.rating_avg || 5.0, rating: shop.rating_avg != null ? shop.rating_avg : 5.0,
total_sales: shop.total_sales || 0, total_sales: shop.total_sales != null ? shop.total_sales : 0,
created_at: shop.created_at || '' created_at: shop.created_at != null ? shop.created_at : ''
} }
// 检查关注状态 // 检查关注状态
@@ -204,7 +218,7 @@ const loadCoupons = async (id: string) => {
const claimCoupon = async (coupon: any) => { const claimCoupon = async (coupon: any) => {
const userId = supabaseService.getCurrentUserId() const userId = supabaseService.getCurrentUserId()
if (!userId) { if (userId == null) {
uni.navigateTo({ url: '/pages/auth/login' }) uni.navigateTo({ url: '/pages/auth/login' })
return return
} }
@@ -297,24 +311,49 @@ const loadShopProducts = async (id: string) => {
// 安全获取属性的方式,处理字段名称不一样的问题 // 安全获取属性的方式,处理字段名称不一样的问题
const safeItem = item as any const safeItem = item as any
const safePrice = (safeItem['base_price'] || safeItem['price'] || 0) as number let safePrice = safeItem['base_price'] as number
const safeMarketPrice = (safeItem['market_price'] || safeItem['original_price'] || safePrice) as number if (safePrice == null) {
const safeStock = (safeItem['total_stock'] || safeItem['available_stock'] || safeItem['stock'] || 0) as number const p = safeItem['price'] as number
const safeSales = (safeItem['sale_count'] || safeItem['sales'] || 0) as number safePrice = p != null ? p : 0
}
let safeMarketPrice = safeItem['market_price'] as number
if (safeMarketPrice == null) {
const mp = safeItem['original_price'] as number
safeMarketPrice = mp != null ? mp : safePrice
}
let safeStock = safeItem['total_stock'] as number
if (safeStock == null) {
let as_ = safeItem['available_stock'] as number
if (as_ == null) {
const s = safeItem['stock'] as number
safeStock = s != null ? s : 0
} else {
safeStock = as_
}
}
let safeSales = safeItem['sale_count'] as number
if (safeSales == null) {
const s = safeItem['sales'] as number
safeSales = s != null ? s : 0
}
return { return {
id: item.id, id: item.id,
merchant_id: item.merchant_id, merchant_id: item.merchant_id,
category_id: item.category_id, category_id: item.category_id,
name: item.name, name: item.name,
description: item.description || '', description: item.description != null ? item.description : '',
images: images, images: images,
price: safePrice, price: safePrice,
original_price: safeMarketPrice, original_price: safeMarketPrice,
stock: safeStock, stock: safeStock,
sales: safeSales, sales: safeSales,
status: 1, status: 1,
created_at: item.created_at || '' created_at: item.created_at != null ? item.created_at : '',
updated_at: item.updated_at != null ? item.updated_at : ''
} }
}) })
@@ -355,14 +394,14 @@ const checkFollowStatus = async (shopId: string) => {
const toggleFollow = async () => { const toggleFollow = async () => {
const userId = supabaseService.getCurrentUserId() const userId = supabaseService.getCurrentUserId()
if (!userId) { if (userId == null) {
uni.navigateTo({ url: '/pages/auth/login' }) uni.navigateTo({ url: '/pages/auth/login' })
return return
} }
// 这里的 merchant.value.id 假如是 ML_SHOPS.id // 这里的 merchant.value.id 假如是 ML_SHOPS.id
const shopId = merchant.value.id const shopId = merchant.value.id
if (!shopId) return if (shopId == null || shopId == '') return
uni.showLoading({ title: '处理中' }) uni.showLoading({ title: '处理中' })
@@ -393,7 +432,7 @@ const toggleFollow = async () => {
const contactService = () => { const contactService = () => {
const currentUser = supabaseService.getCurrentUserId() const currentUser = supabaseService.getCurrentUserId()
if (!currentUser) { if (currentUser == null) {
uni.navigateTo({ url: '/pages/user/login' }) uni.navigateTo({ url: '/pages/user/login' })
return return
} }
@@ -410,7 +449,7 @@ const contactService = () => {
const addToCart = async (product: ProductType) => { const addToCart = async (product: ProductType) => {
uni.showLoading({ title: '添加中...' }) uni.showLoading({ title: '添加中...' })
const success = await supabaseService.addToCart(product.id, 1) const success = await supabaseService.addToCart(product.id, 1, '')
uni.hideLoading() uni.hideLoading()
@@ -437,7 +476,15 @@ const goToProduct = (id: string) => {
<style> <style>
.shop-detail-page { .shop-detail-page {
background-color: #f5f5f5; background-color: #f5f5f5;
min-height: 100vh; flex: 1;
display: flex;
flex-direction: column;
}
.page-scroll {
flex: 1;
height: 0;
width: 100%;
} }
.shop-header { .shop-header {
@@ -544,8 +591,7 @@ const goToProduct = (id: string) => {
} }
.shop-desc { .shop-desc {
display: block;
font-size: 14px;
color: #666; color: #666;
padding: 10px 15px 0; padding: 10px 15px 0;
line-height: 1.4; line-height: 1.4;
@@ -603,13 +649,15 @@ const goToProduct = (id: string) => {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
background-color: #ff4444; background-color: #ff4444;
writing-mode: vertical-rl; /* Note: writing-mode may not work in all environments, used flex direction in product detail instead, but let's try or use flex col */ flex-direction: column;
} }
.coupon-btn-label { .coupon-btn-label {
color: #fff; color: #fff;
font-size: 12px; font-size: 12px;
writing-mode: vertical-rl; width: 14px; /* Force vertical flow by width constraint if needed, or just let it stack naturally if char by char */
} text-align: center;
line-height: 1.2;
}
.product-section { .product-section {
padding: 15px; padding: 15px;
@@ -628,12 +676,12 @@ const goToProduct = (id: string) => {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
gap: 10px;
width: 100%; width: 100%;
justify-content: space-between;
} }
.product-item { .product-item {
width: calc(50% - 5px); width: 48%; /* Fallback for calc(50% - 5px) */
background-color: white; background-color: white;
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
@@ -659,9 +707,8 @@ const goToProduct = (id: string) => {
font-size: 14px; font-size: 14px;
color: #333; color: #333;
margin-bottom: 8px; margin-bottom: 8px;
display: -webkit-box; text-overflow: ellipsis;
-webkit-line-clamp: 2; lines: 2;
-webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
height: 40px; height: 40px;
line-height: 20px; line-height: 20px;
@@ -677,17 +724,14 @@ const goToProduct = (id: string) => {
.price-left { .price-left {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: baseline; align-items: flex-end;
} }
.cart-btn { .cart-btn {
width: 24px; width: 24px;
height: 24px; height: 24px;
background-color: #ff4444; background-color: #ff4444;
border-radius: 50%; border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
} }
.cart-icon { .cart-icon {
@@ -713,13 +757,13 @@ const goToProduct = (id: string) => {
*/ */
@media (min-width: 768px) { @media (min-width: 768px) {
.product-item { .product-item {
width: calc(33.33% - 7px) !important; /* Tablet: 3 items */ width: 32% !important; /* Tablet: 3 items */
} }
} }
@media (min-width: 1024px) { @media (min-width: 1024px) {
.product-item { .product-item {
width: calc(16.66% - 9px) !important; /* PC: 6 items */ width: 16% !important; /* PC: 6 items */
} }
.shop-info-card, .shop-header, .product-section { .shop-info-card, .shop-header, .product-section {
@@ -729,4 +773,4 @@ const goToProduct = (id: string) => {
margin-right: auto; margin-right: auto;
} }
} }
</style> </style>

View File

@@ -6,13 +6,13 @@
<view class="shop-list" v-if="shops.length > 0"> <view class="shop-list" v-if="shops.length > 0">
<view class="shop-item" v-for="shop in shops" :key="shop.id" @click="goToShop(shop)"> <view class="shop-item" v-for="shop in shops" :key="shop.id" @click="goToShop(shop)">
<image :src="shop.shop_logo || '/static/default-shop.png'" class="shop-logo" mode="aspectFill" /> <image :src="shop.shop_logo != null ? shop.shop_logo : '/static/default-shop.png'" class="shop-logo" mode="aspectFill" />
<view class="shop-info"> <view class="shop-info">
<text class="shop-name">{{ shop.shop_name }}</text> <text class="shop-name">{{ shop.shop_name }}</text>
<text class="shop-desc">{{ shop.description || '暂无介绍' }}</text> <text class="shop-desc">{{ shop.description != null ? shop.description : '暂无介绍' }}</text>
<view class="shop-meta"> <view class="shop-meta">
<text class="rating">⭐ {{ shop.rating_avg || 5.0 }}</text> <text class="rating shop-meta-text">⭐ {{ shop.rating_avg }}</text>
<text class="sales">销量: {{ shop.total_sales || 0 }}</text> <text class="sales shop-meta-text">销量: {{ shop.total_sales }}</text>
</view> </view>
</view> </view>
<button class="unfollow-btn" @click.stop="unfollow(shop)">已关注</button> <button class="unfollow-btn" @click.stop="unfollow(shop)">已关注</button>
@@ -72,8 +72,8 @@ const loadFollowedShops = async () => {
shop_name: shopData['shop_name'] as string, shop_name: shopData['shop_name'] as string,
shop_logo: shopData['shop_logo'] as string | null, shop_logo: shopData['shop_logo'] as string | null,
description: shopData['description'] as string | null, description: shopData['description'] as string | null,
rating_avg: (shopData['rating_avg'] || 5.0) as number, rating_avg: (shopData['rating_avg'] != null) ? (shopData['rating_avg'] as number) : 5.0,
total_sales: (shopData['total_sales'] || 0) as number total_sales: (shopData['total_sales'] != null) ? (shopData['total_sales'] as number) : 0
}) })
} }
}) })
@@ -111,8 +111,9 @@ const goToShop = (shop: FollowedShop) => {
// Since shop-detail handles both, passing shop.id (which is ml_shops.id) is fine? // Since shop-detail handles both, passing shop.id (which is ml_shops.id) is fine?
// Wait, shop-detail logic: 1. getShopByMerchantId(id) [tries merchant_id then id]. // Wait, shop-detail logic: 1. getShopByMerchantId(id) [tries merchant_id then id].
// So passing shop.id is safer if merchant_id is not unique or confusing. // So passing shop.id is safer if merchant_id is not unique or confusing.
const targetId = shop.merchant_id != '' ? shop.merchant_id : shop.id
uni.navigateTo({ uni.navigateTo({
url: `/pages/mall/consumer/shop-detail?merchantId=${shop.merchant_id || shop.id}` url: `/pages/mall/consumer/shop-detail?merchantId=${targetId}`
}) })
} }
@@ -125,7 +126,7 @@ const goHome = () => {
.followed-shops-page { .followed-shops-page {
padding: 15px; padding: 15px;
background-color: #f5f5f5; background-color: #f5f5f5;
min-height: 100vh; flex: 1;
} }
.header { .header {
margin-bottom: 15px; margin-bottom: 15px;
@@ -137,7 +138,6 @@ const goHome = () => {
.shop-list { .shop-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px;
} }
.shop-item { .shop-item {
background-color: #fff; background-color: #fff;
@@ -146,6 +146,10 @@ const goHome = () => {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
margin-bottom: 10px;
}
.shop-item:last-child {
margin-bottom: 0;
} }
.shop-logo { .shop-logo {
width: 50px; width: 50px;
@@ -178,7 +182,9 @@ const goHome = () => {
color: #999; color: #999;
margin-top: 4px; margin-top: 4px;
display: flex; display: flex;
gap: 8px; }
.shop-meta-text {
margin-right: 8px;
} }
.unfollow-btn { .unfollow-btn {
font-size: 12px; font-size: 12px;

View File

@@ -11,8 +11,8 @@
<view v-else class="list"> <view v-else class="list">
<view class="card" v-for="s in items" :key="s['id']"> <view class="card" v-for="s in items" :key="s['id']">
<view class="row between"> <view class="row between">
<text class="name">{{ s['plan']?.['name'] || '订阅' }}</text> <text class="name">{{ s['plan']?.['name'] != null ? s['plan']?.['name'] : '订阅' }}</text>
<text class="status" :class="'st-' + (s['status'] || 'active')">{{ statusText(s['status'] as string) }}</text> <text class="status" :class="'st-' + (s['status'] != null ? s['status'] : 'active')">{{ statusText(s['status'] as string) }}</text>
</view> </view>
<view class="row"> <view class="row">
<text class="label">周期</text> <text class="label">周期</text>
@@ -33,7 +33,7 @@
<view class="actions"> <view class="actions">
<label class="toggle"> <label class="toggle">
<switch :checked="!!s['auto_renew']" @change="e => toggleAutoRenew(s, e.detail.value as boolean)" /> <switch :checked="!!s['auto_renew']" @change="e => toggleAutoRenew(s, e.detail.value as boolean)" />
<text>自动续费</text> <text class="toggle-text">自动续费</text>
</label> </label>
<button class="danger" @click="cancelAtPeriodEnd(s)" :disabled="(s['status'] as string) !== 'active'">到期取消</button> <button class="danger" @click="cancelAtPeriodEnd(s)" :disabled="(s['status'] as string) !== 'active'">到期取消</button>
</view> </view>
@@ -59,7 +59,8 @@ const fmt = (s: string | null): string => {
const statusText = (st: string): string => { const statusText = (st: string): string => {
const map: UTSJSONObject = { trial: '试用', active: '生效', past_due: '逾期', canceled: '已取消', expired: '已过期' } as UTSJSONObject const map: UTSJSONObject = { trial: '试用', active: '生效', past_due: '逾期', canceled: '已取消', expired: '已过期' } as UTSJSONObject
return (map[st] as string) || st const val = map[st] as string | null
return val != null ? val : st
} }
const loadSubs = async () => { const loadSubs = async () => {
@@ -133,14 +134,16 @@ onMounted(loadSubs)
<style scoped> <style scoped>
.my-subs { padding: 12px; } .my-subs { padding: 12px; }
.header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; } .header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.title { font-size: 18px; font-weight: 600; } .title { font-size: 18px; font-weight: 700; }
.ghost { background: #fff; border: 1px solid #ddd; color: #333; border-radius: 6px; padding: 6px 10px; } .ghost { background: #fff; border: 1px solid #ddd; color: #333; border-radius: 6px; padding: 6px 10px; }
.loading, .empty { padding: 24px; text-align: center; color: #888; } .loading, .empty { padding: 24px; text-align: center; color: #888; }
.list { display: flex; flex-direction: column; gap: 12px; } .list { display: flex; flex-direction: column; }
.card { background: #fff; border-radius: 10px; padding: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.05); } .card { background: #fff; border-radius: 10px; padding: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.05); margin-bottom: 12px; }
.row { display: flex; gap: 8px; padding: 4px 0; } .card:last-child { margin-bottom: 0; }
.row { display: flex; padding: 4px 0; }
.label { margin-right: 8px; }
.between { justify-content: space-between; align-items: center; } .between { justify-content: space-between; align-items: center; }
.name { font-size: 16px; font-weight: 600; } .name { font-size: 16px; font-weight: 700; }
.status { font-size: 12px; padding: 2px 8px; border-radius: 999px; background: #eee; color: #333; } .status { font-size: 12px; padding: 2px 8px; border-radius: 999px; background: #eee; color: #333; }
.st-trial { background: #e6f7ff; color: #1677ff; } .st-trial { background: #e6f7ff; color: #1677ff; }
.st-active { background: #f6ffed; color: #52c41a; } .st-active { background: #f6ffed; color: #52c41a; }
@@ -149,6 +152,7 @@ onMounted(loadSubs)
.label { color: #666; width: 80px; } .label { color: #666; width: 80px; }
.value { color: #111; flex: 1; } .value { color: #111; flex: 1; }
.actions { display: flex; align-items: center; justify-content: space-between; margin-top: 8px; } .actions { display: flex; align-items: center; justify-content: space-between; margin-top: 8px; }
.toggle { display: flex; align-items: center; gap: 6px; } .toggle { display: flex; align-items: center; }
.toggle-text { margin-right: 6px; }
.danger { background: #f5222d; color: #fff; border-radius: 6px; padding: 6px 10px; } .danger { background: #f5222d; color: #fff; border-radius: 6px; padding: 6px 10px; }
</style> </style>

View File

@@ -96,16 +96,17 @@ onMounted(loadPlan)
<style scoped> <style scoped>
.plan-detail { padding: 12px; } .plan-detail { padding: 12px; }
.header { margin-bottom: 8px; } .header { margin-bottom: 8px; }
.title { font-size: 18px; font-weight: 600; } .title { font-size: 18px; font-weight: 700; }
.card { background: #fff; border-radius: 10px; padding: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.05); } .card { background: #fff; border-radius: 10px; padding: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.05); }
.name { font-size: 16px; font-weight: 600; } .name { font-size: 16px; font-weight: 700; }
.desc { color: #666; margin: 6px 0; } .desc { color: #666; margin: 6px 0; }
.price-row { display: flex; align-items: baseline; gap: 4px; margin: 8px 0; } .price-row { display: flex; align-items: flex-end; margin: 8px 0; }
.price { font-size: 22px; color: #ff4d4f; font-weight: 700; } .price { font-size: 22px; color: #ff4d4f; font-weight: 700; margin-right: 4px; }
.period { color: #999; } .period { color: #999; }
.features { margin-top: 8px; } .features { margin-top: 8px; }
.f-title { font-weight: 600; margin-bottom: 4px; } .f-title { font-weight: 700; margin-bottom: 4px; }
.f-list { display: flex; flex-direction: column; gap: 2px; color: #444; } .f-list { display: flex; flex-direction: column; color: #444; }
.f-item { margin-bottom: 2px; }
.actions { display: flex; justify-content: flex-end; margin-top: 12px; } .actions { display: flex; justify-content: flex-end; margin-top: 12px; }
.primary { background: #3cc51f; color: #fff; border-radius: 6px; padding: 8px 12px; } .primary { background: #3cc51f; color: #fff; border-radius: 6px; padding: 8px 12px; }
.loading, .empty { padding: 24px; text-align: center; color: #888; } .loading, .empty { padding: 24px; text-align: center; color: #888; }

View File

@@ -92,18 +92,19 @@ onMounted(loadPlans)
<style scoped> <style scoped>
.sub-plan-list { padding: 12px; } .sub-plan-list { padding: 12px; }
.header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; } .header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.title { font-size: 18px; font-weight: 600; } .title { font-size: 18px; font-weight: 700; }
.plan-container { display: flex; flex-direction: column; gap: 12px; } .plan-container { display: flex; flex-direction: column; }
.plan-card { background: #fff; border-radius: 10px; padding: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.05); } .plan-card { background: #fff; border-radius: 10px; padding: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.05); margin-bottom: 12px; }
.plan-header { display: flex; align-items: center; justify-content: space-between; } .plan-card:last-child { margin-bottom: 0; }
.plan-name { font-size: 16px; font-weight: 600; } .plan-header { display: flex; align-items: center; justify-content: space-between; }
.badge { font-size: 12px; color: #fff; background: #3cc51f; border-radius: 999px; padding: 2px 8px; } .plan-name { font-size: 16px; font-weight: 700; color: #333; }
.plan-desc { color: #666; margin: 6px 0; line-height: 1.5; } .badge { font-size: 12px; color: #fff; background: #3cc51f; border-radius: 999px; padding: 2px 8px; }
.price-row { display: flex; align-items: baseline; gap: 4px; margin: 6px 0; } .plan-desc { color: #666; margin: 6px 0; line-height: 1.5; }
.price { font-size: 22px; color: #ff4d4f; font-weight: 700; } .price-row { display: flex; align-items: flex-end; margin: 6px 0; }
.period { color: #999; } .price { font-size: 22px; color: #ff4d4f; font-weight: 700; margin-right: 4px; }
.feature-list { color: #444; display: flex; flex-direction: column; gap: 2px; margin: 6px 0; } .period { color: #999; }
.feature-item { font-size: 12px; color: #555; } .feature-list { color: #444; display: flex; flex-direction: column; margin: 6px 0; }
.feature-item { font-size: 12px; color: #555; margin-bottom: 2px; }
.actions { display: flex; justify-content: flex-end; margin-top: 8px; } .actions { display: flex; justify-content: flex-end; margin-top: 8px; }
.primary { background: #3cc51f; color: #fff; border-radius: 6px; padding: 8px 12px; } .primary { background: #3cc51f; color: #fff; border-radius: 6px; padding: 8px 12px; }
.loading, .empty { padding: 24px; text-align: center; color: #888; } .loading, .empty { padding: 24px; text-align: center; color: #888; }

View File

@@ -91,7 +91,10 @@ const selPay = (v: number) => { payMethod.value = v }
// 获取当前用户ID按现有store实现替换 // 获取当前用户ID按现有store实现替换
const getCurrentUserId = (): string => { const getCurrentUserId = (): string => {
try { return (uni.getStorageSync('current_user_id') as string) || '' } catch { return '' } try {
const u = uni.getStorageSync('current_user_id')
return (u != null) ? (u as string) : ''
} catch { return '' }
} }
const confirmSubscribe = async () => { const confirmSubscribe = async () => {
@@ -135,7 +138,7 @@ const confirmSubscribe = async () => {
uni.redirectTo({ url: '/pages/mall/consumer/profile' }) uni.redirectTo({ url: '/pages/mall/consumer/profile' })
}, 600) }, 600)
} else { } else {
uni.showToast({ title: ins?.error?.message || '订阅失败', icon: 'none' }) uni.showToast({ title: ins?.error?.message ?? '订阅失败', icon: 'none' })
} }
} catch (e) { } catch (e) {
console.error('订阅失败:', e) console.error('订阅失败:', e)
@@ -149,15 +152,17 @@ const confirmSubscribe = async () => {
<style scoped> <style scoped>
.subscribe-checkout { padding: 12px; } .subscribe-checkout { padding: 12px; }
.header { margin-bottom: 8px; } .header { margin-bottom: 8px; }
.title { font-size: 18px; font-weight: 600; } .title { font-size: 18px; font-weight: 700; }
.card { background: #fff; border-radius: 10px; padding: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.05); } .card { background: #fff; border-radius: 10px; padding: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.05); }
.row { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #f0f0f0; } .row { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #f0f0f0; }
.row:last-child { border-bottom: none; } .row:last-child { border-bottom: none; }
.label { color: #666; } .label { color: #666; }
.value { color: #111; font-weight: 600; } .value { color: #111; font-weight: 700; }
.section-title { margin-top: 12px; font-weight: 600; } .section-title { margin-top: 12px; font-weight: 700; }
.pay-methods { display: flex; flex-direction: column; gap: 8px; padding: 8px 0; } .pay-methods { display: flex; flex-direction: column; padding: 8px 0; }
.pay-item { display: flex; align-items: center; gap: 8px; } .pay-item { display: flex; align-items: center; margin-bottom: 8px; }
.pay-item:last-child { margin-bottom: 0; }
.pay-icon { margin-right: 8px; }
.actions { display: flex; justify-content: flex-end; margin-top: 12px; } .actions { display: flex; justify-content: flex-end; margin-top: 12px; }
.primary { background: #3cc51f; color: #fff; border-radius: 6px; padding: 8px 12px; } .primary { background: #3cc51f; color: #fff; border-radius: 6px; padding: 8px 12px; }
.loading, .empty { padding: 24px; text-align: center; color: #888; } .loading, .empty { padding: 24px; text-align: center; color: #888; }

View File

@@ -241,7 +241,7 @@ const resetTransactions = () => {
// 加载钱包数据 // 加载钱包数据
const loadWalletData = async () => { const loadWalletData = async () => {
const userId = getCurrentUserId() const userId = getCurrentUserId()
if (userId == null) { if (userId == '') {
// uni.navigateTo({ // uni.navigateTo({
// url: '/pages/user/login' // url: '/pages/user/login'
// }) // })
@@ -282,7 +282,7 @@ const loadTransactions = async (loadMore: boolean = false) => {
try { try {
const userId = getCurrentUserId() const userId = getCurrentUserId()
if (!userId) { if (userId == '') {
isLoading.value = false isLoading.value = false
return return
} }
@@ -305,19 +305,19 @@ const loadTransactions = async (loadMore: boolean = false) => {
let createdAt = '' let createdAt = ''
if (item instanceof UTSJSONObject) { if (item instanceof UTSJSONObject) {
id = item.getString('id') || '' id = item.getString('id') ?? ''
amount = item.getNumber('amount') || 0 amount = item.getNumber('amount') ?? 0
balance = item.getNumber('balance_after') || 0 balance = item.getNumber('balance_after') ?? 0
type = item.getString('type') || 'consume' type = item.getString('type') ?? 'consume'
remark = item.getString('description') || '' remark = item.getString('description') ?? ''
createdAt = item.getString('created_at') || '' createdAt = item.getString('created_at') ?? ''
} else { } else {
id = (item['id'] as string) || '' id = (item['id'] as string) ?? ''
amount = (item['amount'] as number) || 0 amount = (item['amount'] as number) ?? 0
balance = (item['balance_after'] as number) || 0 balance = (item['balance_after'] as number) ?? 0
type = (item['type'] as string) || 'consume' type = (item['type'] as string) ?? 'consume'
remark = (item['description'] as string) || '' remark = (item['description'] as string) ?? ''
createdAt = (item['created_at'] as string) || '' createdAt = (item['created_at'] as string) ?? ''
} }
mappedData.push({ mappedData.push({
@@ -355,7 +355,7 @@ const loadTransactions = async (loadMore: boolean = false) => {
// 获取当前用户ID // 获取当前用户ID
const getCurrentUserId = (): string => { const getCurrentUserId = (): string => {
const userStore = uni.getStorageSync('userInfo') const userStore = uni.getStorageSync('userInfo')
return userStore?.id || '' return userStore?.getString('id') ?? ''
} }
// 获取交易图标 // 获取交易图标
@@ -369,7 +369,8 @@ const getTransactionIcon = (type: string): string => {
income: '💰', income: '💰',
expense: '📤' expense: '📤'
} }
return icons[type] || '💰' const icon = icons[type]
return icon != null ? icon : '💰'
} }
// 获取交易标题 // 获取交易标题
@@ -383,7 +384,8 @@ const getTransactionTitle = (type: string): string => {
income: '收入', income: '收入',
expense: '支出' expense: '支出'
} }
return titles[type] || '交易' const title = titles[type]
return title != null ? title : '交易'
} }
// 格式化时间 // 格式化时间
@@ -531,7 +533,7 @@ const goBack = () => {
.wallet-page { .wallet-page {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100vh; flex: 1; /* Fixed 100vh */
background-color: #f5f5f5; background-color: #f5f5f5;
} }
@@ -595,13 +597,14 @@ const goBack = () => {
.dashboard-container { .dashboard-container {
flex-direction: row; /* 横向排列 */ flex-direction: row; /* 横向排列 */
align-items: flex-start; align-items: flex-start;
gap: 20px; /* gap: 20px; REMOVED */
max-width: 100%; /* max-width: 100%; REMOVED */
} }
.dashboard-main { .dashboard-main {
width: 400px; /* 左侧固定宽度 */ width: 400px; /* 左侧固定宽度 */
flex-shrink: 0; flex-shrink: 0;
margin-right: 20px; /* REPLACED gap */
} }
.dashboard-side { .dashboard-side {
@@ -632,7 +635,7 @@ const goBack = () => {
} }
.balance-label { .balance-label {
display: block; /* display: block; REMOVED */
font-size: 14px; font-size: 14px;
opacity: 0.9; opacity: 0.9;
margin-bottom: 10px; margin-bottom: 10px;
@@ -640,7 +643,7 @@ const goBack = () => {
} }
.balance-value { .balance-value {
display: block; /* display: block; REMOVED */
font-size: 36px; font-size: 36px;
font-weight: bold; font-weight: bold;
margin-bottom: 20px; margin-bottom: 20px;
@@ -650,7 +653,7 @@ const goBack = () => {
.balance-actions { .balance-actions {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 20px; /* gap: 20px; REMOVED */
} }
.action-btn { .action-btn {
@@ -665,6 +668,7 @@ const goBack = () => {
.action-btn.recharge { .action-btn.recharge {
background-color: #ffffff; background-color: #ffffff;
color: #667eea; color: #667eea;
margin-right: 20px; /* REPLACED gap */
} }
.action-btn.withdraw { .action-btn.withdraw {
@@ -688,14 +692,14 @@ const goBack = () => {
} }
.stat-label { .stat-label {
display: block; /* display: block; REMOVED */
font-size: 12px; font-size: 12px;
color: #666666; color: #666666;
margin-bottom: 8px; margin-bottom: 8px;
} }
.stat-value { .stat-value {
display: block; /* display: block; REMOVED */
font-size: 16px; font-size: 16px;
font-weight: bold; font-weight: bold;
color: #333333; color: #333333;
@@ -752,9 +756,7 @@ const goBack = () => {
} }
.filter-tabs { .filter-tabs {
display: flex; /* gap: 15px; REMOVED */
flex-direction: row;
gap: 15px;
} }
.filter-tab { .filter-tab {
@@ -762,22 +764,17 @@ const goBack = () => {
color: #666666; color: #666666;
padding: 5px 0; padding: 5px 0;
position: relative; position: relative;
margin-right: 15px; /* REPLACED gap */
border-bottom: 2px solid transparent; /* Prepare for active state */
} }
.filter-tab.active { .filter-tab.active {
color: #007aff; color: #007aff;
font-weight: bold; font-weight: bold;
border-bottom: 2px solid #007aff; /* REPLACED ::after */
} }
.filter-tab.active::after { /* ::after removed */
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background-color: #007aff;
}
.empty-transactions { .empty-transactions {
display: flex; display: flex;
@@ -899,7 +896,10 @@ const goBack = () => {
} }
.tip-title { .tip-title {
display: block; /* display: block; REMOVED */
font-size: 16px;
font-weight: bold;
/* display: block; REMOVED */
font-size: 16px; font-size: 16px;
font-weight: bold; font-weight: bold;
color: #333333; color: #333333;
@@ -907,11 +907,11 @@ const goBack = () => {
} }
.tip-item { .tip-item {
display: block; /* display: block; REMOVED */
margin-bottom: 8px;
font-size: 12px; font-size: 12px;
color: #666666; color: #666666;
line-height: 1.6; line-height: 1.6;
margin-bottom: 8px;
} }
.tip-item:last-child { .tip-item:last-child {
@@ -974,7 +974,7 @@ const goBack = () => {
} }
.amount-label { .amount-label {
display: block; /* display: block; REMOVED */
font-size: 14px; font-size: 14px;
color: #333333; color: #333333;
margin-bottom: 10px; margin-bottom: 10px;
@@ -1007,7 +1007,7 @@ const goBack = () => {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
gap: 10px; /* gap: 10px; REMOVED */
margin-bottom: 15px; margin-bottom: 15px;
} }
@@ -1017,6 +1017,8 @@ const goBack = () => {
border-radius: 15px; border-radius: 15px;
font-size: 14px; font-size: 14px;
color: #333333; color: #333333;
margin-right: 10px; /* REPLACED gap */
margin-bottom: 10px; /* REPLACED gap */
} }
.quick-amount.active { .quick-amount.active {
@@ -1026,7 +1028,7 @@ const goBack = () => {
} }
.recharge-tip { .recharge-tip {
display: block; /* display: block; REMOVED */
font-size: 12px; font-size: 12px;
color: #999999; color: #999999;
} }
@@ -1034,7 +1036,7 @@ const goBack = () => {
.popup-footer { .popup-footer {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 15px; /* gap: 15px; REMOVED */
} }
.cancel-btn, .cancel-btn,
@@ -1050,6 +1052,7 @@ const goBack = () => {
.cancel-btn { .cancel-btn {
background-color: #f5f5f5; background-color: #f5f5f5;
color: #666666; color: #666666;
margin-right: 15px; /* REPLACED gap */
} }
.confirm-btn { .confirm-btn {

View File

@@ -117,14 +117,19 @@ const loadData = async () => {
let cardNum = '' let cardNum = ''
if (item instanceof UTSJSONObject) { if (item instanceof UTSJSONObject) {
id = item.getString('id') || '' id = item.getString('id') ?? ''
bankName = item.getString('bank_name') || '' bankName = item.getString('bank_name') ?? ''
cardNum = item.getString('card_number') || '' cardNum = item.getString('card_number') ?? ''
} else { } else {
const m = item as Map<string, any> const m = item as Map<string, any>
id = (m.get('id') as string) || '' const idVal = m.get('id')
bankName = (m.get('bank_name') as string) || '' id = idVal != null ? (idVal as string) : ''
cardNum = (m.get('card_number') as string) || ''
const nameVal = m.get('bank_name')
bankName = nameVal != null ? (nameVal as string) : ''
const numVal = m.get('card_number')
cardNum = numVal != null ? (numVal as string) : ''
} }
if (id != '') { if (id != '') {
@@ -207,7 +212,7 @@ const submitWithdraw = async () => {
<style scoped> <style scoped>
.page-container { .page-container {
background-color: #f5f5f5; background-color: #f5f5f5;
min-height: 100vh; flex: 1; /* Fixed 100vh issue */
padding: 20px; padding: 20px;
} }
.card { .card {
@@ -230,7 +235,14 @@ const submitWithdraw = async () => {
.bank-info { .bank-info {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; /* gap removed */
}
.bank-name {
margin-right: 10px;
font-weight: bold;
}
.card-type {
margin-right: 10px;
} }
.placeholder { .placeholder {
color: #999; color: #999;
@@ -242,7 +254,7 @@ const submitWithdraw = async () => {
font-size: 14px; font-size: 14px;
color: #666; color: #666;
margin-bottom: 10px; margin-bottom: 10px;
display: block; /* display: block removed */
} }
.input-wrapper { .input-wrapper {
display: flex; display: flex;
@@ -329,6 +341,6 @@ const submitWithdraw = async () => {
padding: 15px 0; padding: 15px 0;
text-align: center; text-align: center;
color: #5785e5; color: #5785e5;
font-weight: 500; font-weight: bold;
} }
</style> </style>

View File

@@ -5,12 +5,10 @@ export type DeviceInfo = {
status?: string // 'online' | 'offline' | 其他状态 status?: string // 'online' | 'offline' | 其他状态
user_id?: string user_id?: string
// 可根据实际需求添加更多字段 // 可根据实际需求添加更多字段
[key: string]: any
} }
// 设备查询参数类型 // 设备查询参数类型
export type DeviceParams = { export type DeviceParams = {
user_id: string user_id: string
// 可根据实际需求添加更多查询参数 // 可根据实际需求添加更多查询参数
[key: string]: any
} }

View File

@@ -132,10 +132,7 @@ const handleSubmit = async () => {
.page-container { .page-container {
padding: 20px; padding: 20px;
background-color: #f5f5f5; background-color: #f5f5f5;
min-height: 100vh; flex: 1;
}
.form-group {
background-color: #fff; background-color: #fff;
border-radius: 8px; border-radius: 8px;
padding: 0 15px; padding: 0 15px;

View File

@@ -133,10 +133,7 @@ const handleSubmit = async () => {
.page-container { .page-container {
padding: 20px; padding: 20px;
background-color: #f5f5f5; background-color: #f5f5f5;
min-height: 100vh; flex: 1;
}
.form-group {
background-color: #fff; background-color: #fff;
border-radius: 8px; border-radius: 8px;
padding: 0 15px; padding: 0 15px;

View File

@@ -139,14 +139,15 @@
.spinner { .spinner {
width: 88rpx; width: 88rpx;
height: 88rpx; height: 88rpx;
border-radius: 50%; /* 如果不支持 50%,可以用 44rpx */ border-radius: 44rpx; /* 如果不支持 50%,可以用 44rpx */
border-width: 8rpx; border-width: 8rpx;
border-style: solid; border-style: solid;
border-color: #f3f4f6; border-color: #f3f4f6;
border-top-color: #ff6b6b; border-top-color: #ff6b6b;
/* animation: spin 1s linear infinite; UVUE CSS动画需要特定写法或 transform */ /* animation: spin 1s linear infinite; UVUE CSS动画需要特定写法或 transform */
transform: rotate(360deg); /* transform: rotate(360deg); */
transition-duration: 1000ms; /* transition-duration: 1000ms; */
/* 简单的无限旋转在原生 CSS 中可能需要写关键帧但 App-UVUE 支持有限, /* 简单的无限旋转在原生 CSS 中可能需要写关键帧但 App-UVUE 支持有限,
这里暂时保留样式但不指望它自动动起来,或者应该用 loading 组件 */ 这里暂时保留样式但不指望它自动动起来,或者应该用 loading 组件 */
} }
@@ -178,7 +179,7 @@
padding: 24rpx; padding: 24rpx;
border-radius: 16rpx; border-radius: 16rpx;
font-size: 28rpx; font-size: 28rpx;
font-weight: 600; font-weight: 700;
} }
.action.primary { .action.primary {
@@ -192,6 +193,7 @@
background: #ffffff; background: #ffffff;
} }
/*
@keyframes spin { @keyframes spin {
from { from {
transform: rotate(0deg); transform: rotate(0deg);
@@ -200,4 +202,5 @@
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
*/
</style> </style>

View File

@@ -87,10 +87,7 @@ const handleSubmit = async () => {
.page-container { .page-container {
padding: 20px; padding: 20px;
background-color: #f5f5f5; background-color: #f5f5f5;
min-height: 100vh; flex: 1;
}
.form-group {
background-color: #fff; background-color: #fff;
border-radius: 8px; border-radius: 8px;
padding: 0 15px; padding: 0 15px;

View File

@@ -316,7 +316,7 @@ const handleLogin = async () => {
errorCode === 'invalid_credentials') { errorCode === 'invalid_credentials') {
throw new Error('邮箱或密码错误') throw new Error('邮箱或密码错误')
} else { } else {
throw new Error(errorMsg || '登录失败,请重试') throw new Error(errorMsg != '' ? errorMsg : '登录失败,请重试')
} }
} }
} else { } else {

View File

@@ -69,6 +69,7 @@
<view class="protocol"> <view class="protocol">
<checkbox-group @change="handleProtocolChange"> <checkbox-group @change="handleProtocolChange">
<checkbox <checkbox
class="protocol-checkbox"
:checked="protocol" :checked="protocol"
:class="{ 'trembling': inAnimation }" :class="{ 'trembling': inAnimation }"
@animationend="inAnimation = false" @animationend="inAnimation = false"
@@ -241,7 +242,8 @@
user = result user = result
console.log('✅ result 本身就是 user 对象:', id) console.log('✅ result 本身就是 user 对象:', id)
} else { } else {
console.warn('⚠️ 未找到 user 信息,检查所有字段:', Object.keys(result.toMap() || {})) // console.warn('⚠️ 未找到 user 信息,检查所有字段:', Object.keys(result.toMap() || {}))
console.warn('⚠️ 未找到 user 信息,检查所有字段')
} }
} }
@@ -263,7 +265,7 @@
if (code === 500 && errorMsg.includes('confirmation email')) { if (code === 500 && errorMsg.includes('confirmation email')) {
throw new Error('注册失败:邮件服务配置错误,请联系管理员或修改 Supabase 配置(设置 ENABLE_EMAIL_AUTOCONFIRM=true') throw new Error('注册失败:邮件服务配置错误,请联系管理员或修改 Supabase 配置(设置 ENABLE_EMAIL_AUTOCONFIRM=true')
} else { } else {
throw new Error(errorMsg || '注册失败,请重试') throw new Error(errorMsg != '' ? errorMsg : '注册失败,请重试')
} }
} }
@@ -355,12 +357,12 @@
</script> </script>
<style> <style>
page { /* page {
background: #F5F5F5; background: #F5F5F5;
} } */
.register-wrapper { .register-wrapper {
min-height: 100vh; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background: #F5F5F5; background: #F5F5F5;
@@ -389,7 +391,7 @@ page {
.title { .title {
font-size: 40rpx; font-size: 40rpx;
font-weight: 600; font-weight: 700;
color: #333333; color: #333333;
text-align: center; text-align: center;
margin-bottom: 50rpx; margin-bottom: 50rpx;
@@ -466,7 +468,7 @@ page {
border-radius: 4rpx; border-radius: 4rpx;
color: #FFFFFF; color: #FFFFFF;
font-size: 32rpx; font-size: 32rpx;
font-weight: 500; font-weight: 700;
box-shadow: 0 4rpx 12rpx rgba(255, 77, 79, 0.3); box-shadow: 0 4rpx 12rpx rgba(255, 77, 79, 0.3);
} }
@@ -501,7 +503,7 @@ page {
justify-content: center; justify-content: center;
} }
.protocol checkbox { .protocol-checkbox {
margin-right: 10rpx; margin-right: 10rpx;
} }
@@ -514,6 +516,7 @@ page {
color: var(--view-theme, #FF4D4F); color: var(--view-theme, #FF4D4F);
} }
/*
.trembling { .trembling {
animation: shake 0.6s; animation: shake 0.6s;
} }
@@ -523,6 +526,7 @@ page {
10%, 30%, 50%, 70%, 90% { transform: translateX(-10rpx); } 10%, 30%, 50%, 70%, 90% { transform: translateX(-10rpx); }
20%, 40%, 60%, 80% { transform: translateX(10rpx); } 20%, 40%, 60%, 80% { transform: translateX(10rpx); }
} }
*/
/* 底部版权 */ /* 底部版权 */
.footer { .footer {

View File

@@ -46,7 +46,7 @@
<style> <style>
.page { .page {
min-height: 100vh; flex: 1;
background: #f7f8fa; background: #f7f8fa;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -97,7 +97,6 @@
font-weight: 700; font-weight: 700;
color: #111; color: #111;
margin-bottom: 12rpx; margin-bottom: 12rpx;
display: block;
} }
.p { .p {
@@ -105,7 +104,6 @@
color: rgba(0, 0, 0, 0.65); color: rgba(0, 0, 0, 0.65);
line-height: 44rpx; line-height: 44rpx;
margin-bottom: 12rpx; margin-bottom: 12rpx;
display: block;
} }
.divider { .divider {
@@ -125,7 +123,7 @@
background: linear-gradient(135deg, #ff4d4f 0%, #ff7a45 100%); background: linear-gradient(135deg, #ff4d4f 0%, #ff7a45 100%);
color: #fff; color: #fff;
font-size: 30rpx; font-size: 30rpx;
font-weight: 600; font-weight: 700;
box-shadow: 0 16rpx 32rpx rgba(255, 77, 79, 0.24); box-shadow: 0 16rpx 32rpx rgba(255, 77, 79, 0.24);
} }
</style> </style>

685
pages20260224.json Normal file
View File

@@ -0,0 +1,685 @@
{
"pages": [
{
"path": "pages/user/login",
"style": {
"navigationBarTitleText": "用户登录",
"navigationStyle": "custom"
}
},
// {
// "path": "pages/mall/admin/homePage/index",
// "style": {
// "navigationBarTitleText": "管理后台",
// "navigationStyle": "custom"
// }
// },
{
"path": "pages/user/boot",
"style": {
"navigationBarTitleText": ""
}
},
{
"path": "pages/user/register",
"style": {
"navigationBarTitleText": "注册"
}
},
{
"path": "pages/user/forgot-password",
"style": {
"navigationBarTitleText": "忘记密码"
}
},
{
"path": "pages/user/terms",
"style": {
"navigationBarTitleText": "用户协议与隐私政策"
}
},
{
"path": "pages/user/center",
"style": {
"navigationBarTitleText": "用户中心"
}
},
{
"path": "pages/user/profile",
"style": {
"navigationBarTitleText": "个人资料"
}
},
{
"path": "pages/user/change-password",
"style": {
"navigationBarTitleText": "修改密码"
}
},
{
"path": "pages/user/bind-phone",
"style": {
"navigationBarTitleText": "绑定手机"
}
},
{
"path": "pages/user/bind-email",
"style": {
"navigationBarTitleText": "绑定邮箱"
}
},
{
"path": "pages/mall/consumer/index",
"style": {
"navigationBarTitleText": "首页",
"navigationStyle": "custom",
"enablePullDownRefresh": false
}
},
{
"path": "pages/mall/consumer/category",
"style": {
"navigationBarTitleText": "分类",
"navigationStyle": "custom"
}
},
{
"path": "pages/mall/consumer/messages",
"style": {
"navigationBarTitleText": "消息",
"enablePullDownRefresh": true
}
},
{
"path": "pages/mall/consumer/cart",
"style": {
"navigationBarTitleText": "购物车"
}
},
{
"path": "pages/mall/consumer/profile",
"style": {
"navigationBarTitleText": "我的"
}
}
],
"subPackages": [
{
"root": "pages/mall/consumer",
"pages": [
{
"path": "settings",
"style": {
"navigationBarTitleText": "设置"
}
},
{
"path": "wallet",
"style": {
"navigationBarTitleText": "我的钱包"
}
},
{
"path": "withdraw",
"style": {
"navigationBarTitleText": "余额提现"
}
},
{
"path": "search",
"style": {
"navigationBarTitleText": "搜索",
"navigationStyle": "custom"
}
},
{
"path": "product-detail",
"style": {
"navigationBarTitleText": "商品详情"
}
},
{
"path": "shop-detail",
"style": {
"navigationBarTitleText": "店铺详情"
}
},
{
"path": "coupons",
"style": {
"navigationBarTitleText": "我的优惠券"
}
},
{
"path": "favorites",
"style": {
"navigationBarTitleText": "我的收藏"
}
},
{
"path": "footprint",
"style": {
"navigationBarTitleText": "我的足迹"
}
},
{
"path": "address-list",
"style": {
"navigationBarTitleText": "收货地址"
}
},
{
"path": "address-edit",
"style": {
"navigationBarTitleText": "编辑地址"
}
},
{
"path": "checkout",
"style": {
"navigationBarTitleText": "确认订单"
}
},
{
"path": "payment",
"style": {
"navigationBarTitleText": "收银台"
}
},
{
"path": "payment-success",
"style": {
"navigationBarTitleText": "支付成功",
"navigationStyle": "custom"
}
},
{
"path": "orders",
"style": {
"navigationBarTitleText": "我的订单",
"enablePullDownRefresh": true
}
},
{
"path": "order-detail",
"style": {
"navigationBarTitleText": "订单详情"
}
},
{
"path": "logistics",
"style": {
"navigationBarTitleText": "物流详情"
}
},
{
"path": "review",
"style": {
"navigationBarTitleText": "评价晒单"
}
},
{
"path": "refund",
"style": {
"navigationBarTitleText": "退款/售后"
}
},
{
"path": "apply-refund",
"style": {
"navigationBarTitleText": "申请售后"
}
},
{
"path": "refund-review",
"style": {
"navigationBarTitleText": "服务评价"
}
},
{
"path": "chat",
"style": {
"navigationBarTitleText": "客服聊天",
"navigationStyle": "custom"
}
},
{
"path": "subscription/plan-list",
"style": {
"navigationBarTitleText": "软件订阅"
}
},
{
"path": "subscription/plan-detail",
"style": {
"navigationBarTitleText": "订阅详情"
}
},
{
"path": "subscription/subscribe-checkout",
"style": {
"navigationBarTitleText": "确认订阅"
}
},
{
"path": "subscription/my-subscriptions",
"style": {
"navigationBarTitleText": "我的订阅"
}
},
{
"path": "subscription/followed-shops",
"style": {
"navigationBarTitleText": "关注店铺"
}
},
{
"path": "points/index",
"style": {
"navigationBarTitleText": "积分管理"
}
},
{
"path": "red-packets/index",
"style": {
"navigationBarTitleText": "我的红包"
}
},
{
"path": "bank-cards/index",
"style": {
"navigationBarTitleText": "银行卡管理"
}
},
{
"path": "bank-cards/add",
"style": {
"navigationBarTitleText": "添加银行卡"
}
}
]
}
// {
// "root": "pages/mall/delivery",
// "pages": [
// {
// "path": "index",
// "style": {
// "navigationBarTitleText": "配送中心",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "order-detail",
// "style": {
// "navigationBarTitleText": "订单详情页",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "profile",
// "style": {
// "navigationBarTitleText": "配送个人中心",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "order-history",
// "style": {
// "navigationBarTitleText": "历史记录",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "earnings",
// "style": {
// "navigationBarTitleText": "收入明细",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "tasks",
// "style": {
// "navigationBarTitleText": "全部任务",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "task-detail",
// "style": {
// "navigationBarTitleText": "任务详情",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "profile-edit",
// "style": {
// "navigationBarTitleText": "编辑个人资料",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "ratings",
// "style": {
// "navigationBarTitleText": "评价",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "vehicle",
// "style": {
// "navigationBarTitleText": "车辆管理",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "vehicle-add",
// "style": {
// "navigationBarTitleText": "添加车辆",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "vehicle-edit",
// "style": {
// "navigationBarTitleText": "编辑车辆",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "help-center",
// "style": {
// "navigationBarTitleText": "帮助中心",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "about",
// "style": {
// "navigationBarTitleText": "关于我们",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "feedback",
// "style": {
// "navigationBarTitleText": "意见反馈",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "test",
// "style": {
// "navigationBarTitleText": "test",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "settings",
// "style": {
// "navigationBarTitleText": "设置",
// "navigationStyle": "custom"
// }
// }
// ]
// },
// {
// "root": "pages/mall/analytics",
// "pages": [
// {
// "path": "index",
// "style": {
// "navigationBarTitleText": "数据分析",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "profile",
// "style": {
// "navigationBarTitleText": "数据分析个人中心"
// }
// },
// {
// "path": "sales-report",
// "style": {
// "navigationBarTitleText": "销售报表"
// }
// },
// {
// "path": "user-analysis",
// "style": {
// "navigationBarTitleText": "用户分析"
// }
// },
// {
// "path": "product-insights",
// "style": {
// "navigationBarTitleText": "商品洞察"
// }
// },
// {
// "path": "delivery-analysis",
// "style": {
// "navigationBarTitleText": "配送效率分析"
// }
// },
// {
// "path": "coupon-analysis",
// "style": {
// "navigationBarTitleText": "优惠券效果分析"
// }
// },
// {
// "path": "market-trends",
// "style": {
// "navigationBarTitleText": "市场趋势"
// }
// },
// {
// "path": "custom-report",
// "style": {
// "navigationBarTitleText": "自定义报表"
// }
// },
// {
// "path": "report-detail",
// "style": {
// "navigationBarTitleText": "报表详情",
// "enablePullDownRefresh": false
// }
// },
// {
// "path": "data-detail",
// "style": {
// "navigationBarTitleText": "数据分析详情",
// "enablePullDownRefresh": false
// }
// },
// {
// "path": "insight-detail",
// "style": {
// "navigationBarTitleText": "数据洞察详情",
// "enablePullDownRefresh": false
// }
// }
// ]
// },
// {
// "root": "pages/mall/admin",
// "pages": [
// {
// "path": "user-management",
// "style": {
// "navigationBarTitleText": "用户管理",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "product-management",
// "style": {
// "navigationBarTitleText": "商品管理",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "order-management",
// "style": {
// "navigationBarTitleText": "订单管理",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "finance/record",
// "style": {
// "navigationBarTitleText": "财务管理",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "user-statistics",
// "style": {
// "navigationBarTitleText": "用户统计",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "system-settings",
// "style": {
// "navigationBarTitleText": "系统设置",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "subscription/plan-management",
// "style": {
// "navigationBarTitleText": "订阅方案管理"
// }
// },
// {
// "path": "subscription/user-subscriptions",
// "style": {
// "navigationBarTitleText": "用户订阅管理"
// }
// },
// {
// "path": "marketing/coupon/list",
// "style": {
// "navigationBarTitleText": "优惠券列表"
// }
// },
// {
// "path": "marketing/coupon/receive",
// "style": {
// "navigationBarTitleText": "用户领取记录"
// }
// },
// {
// "path": "marketing/signin/rule",
// "style": {
// "navigationBarTitleText": "签到规则"
// }
// },
// {
// "path": "marketing/signin/record",
// "style": {
// "navigationBarTitleText": "签到记录"
// }
// }
// ]
// },
// {
// "root": "pages/mall/merchant",
// "pages": [
// {
// "path": "index",
// "style": {
// "navigationBarTitleText": "商家中心",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "product-detail",
// "style": {
// "navigationBarTitleText": "商品管理详情",
// "enablePullDownRefresh": false
// }
// },
// {
// "path": "profile",
// "style": {
// "navigationBarTitleText": "个人资料"
// }
// }
// ]
// },
// {
// "root": "pages/mall/service",
// "pages": [
// {
// "path": "index",
// "style": {
// "navigationBarTitleText": "客服工作台",
// "navigationStyle": "custom"
// }
// },
// {
// "path": "profile",
// "style": {
// "navigationBarTitleText": "客服个人中心"
// }
// },
// {
// "path": "ticket-detail",
// "style": {
// "navigationBarTitleText": "工单详情",
// "enablePullDownRefresh": false
// }
// }
// ]
// }
],
"tabBar": {
"color": "#999999",
"selectedColor": "#ff5000",
"backgroundColor": "#ffffff",
"borderStyle": "black",
"list": [
{
"pagePath": "pages/mall/consumer/index",
"text": "首页",
"iconPath": "static/tabbar/home.png",
"selectedIconPath": "static/tabbar/home-active.png"
},
{
"pagePath": "pages/mall/consumer/category",
"text": "分类",
"iconPath": "static/tabbar/category.png",
"selectedIconPath": "static/tabbar/category-active.png"
},
{
"pagePath": "pages/mall/consumer/messages",
"text": "消息",
"iconPath": "static/tabbar/messages.png",
"selectedIconPath": "static/tabbar/messages-active.png"
},
{
"pagePath": "pages/mall/consumer/cart",
"text": "购物车",
"iconPath": "static/tabbar/cart.png",
"selectedIconPath": "static/tabbar/cart-active.png"
},
{
"pagePath": "pages/mall/consumer/profile",
"text": "我的",
"iconPath": "static/tabbar/profile.png",
"selectedIconPath": "static/tabbar/profile-active.png"
}
]
},
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "mall",
"navigationBarBackgroundColor": "#FFFFFF",
"backgroundColor": "#F8F8F8"
}
}

View File

@@ -251,7 +251,7 @@ export const ORDER_STATUS = {
CANCELLED: 6, CANCELLED: 6,
REFUNDING: 7, REFUNDING: 7,
REFUNDED: 8 REFUNDED: 8
} as const }
// 优惠券类型枚举 // 优惠券类型枚举
export const COUPON_TYPE = { export const COUPON_TYPE = {
@@ -263,7 +263,7 @@ export const COUPON_TYPE = {
CATEGORY: 6, // 品类券 CATEGORY: 6, // 品类券
MERCHANT: 7, // 商家券 MERCHANT: 7, // 商家券
LIMITED_TIME: 8 // 限时券 LIMITED_TIME: 8 // 限时券
} as const }
// 支付方式枚举 // 支付方式枚举
export const PAYMENT_METHOD = { export const PAYMENT_METHOD = {
@@ -271,7 +271,7 @@ export const PAYMENT_METHOD = {
ALIPAY: 2, ALIPAY: 2,
UNIONPAY: 3, UNIONPAY: 3,
BALANCE: 4 BALANCE: 4
} as const }
// 配送状态枚举 // 配送状态枚举
export const DELIVERY_STATUS = { export const DELIVERY_STATUS = {
@@ -281,7 +281,7 @@ export const DELIVERY_STATUS = {
IN_TRANSIT: 4, IN_TRANSIT: 4,
DELIVERED: 5, DELIVERED: 5,
FAILED: 6 FAILED: 6
} as const }
// 用户类型枚举 // 用户类型枚举
export const MALL_USER_TYPE = { export const MALL_USER_TYPE = {
@@ -290,7 +290,7 @@ export const MALL_USER_TYPE = {
DELIVERY: 3, // 配送员 DELIVERY: 3, // 配送员
SERVICE: 4, // 客服 SERVICE: 4, // 客服
ADMIN: 5 // 管理员 ADMIN: 5 // 管理员
} as const }
// 用户状态枚举 // 用户状态枚举
export const USER_STATUS = { export const USER_STATUS = {
@@ -305,7 +305,7 @@ export const VERIFICATION_STATUS = {
UNVERIFIED: 0, // 未认证 UNVERIFIED: 0, // 未认证
VERIFIED: 1, // 已认证 VERIFIED: 1, // 已认证
FAILED: 2 // 认证失败 FAILED: 2 // 认证失败
} as const }
// 地址标签枚举 // 地址标签枚举
export const ADDRESS_LABEL = { export const ADDRESS_LABEL = {
@@ -313,13 +313,13 @@ export const ADDRESS_LABEL = {
OFFICE: 'office', // 公司 OFFICE: 'office', // 公司
SCHOOL: 'school', // 学校 SCHOOL: 'school', // 学校
OTHER: 'other' // 其他 OTHER: 'other' // 其他
} as const }
// 收藏类型枚举 // 收藏类型枚举
export const FAVORITE_TYPE = { export const FAVORITE_TYPE = {
PRODUCT: 'product', // 商品 PRODUCT: 'product', // 商品
SHOP: 'shop' // 店铺 SHOP: 'shop' // 店铺
} as const }
// ========================= // =========================
// 订阅相关类型与枚举 // 订阅相关类型与枚举
@@ -329,7 +329,7 @@ export const FAVORITE_TYPE = {
export const SUBSCRIPTION_PERIOD = { export const SUBSCRIPTION_PERIOD = {
MONTHLY: 'monthly', MONTHLY: 'monthly',
YEARLY: 'yearly' YEARLY: 'yearly'
} as const }
// 订阅状态枚举 // 订阅状态枚举
export const SUBSCRIPTION_STATUS = { export const SUBSCRIPTION_STATUS = {
@@ -338,7 +338,7 @@ export const SUBSCRIPTION_STATUS = {
PAST_DUE: 'past_due', PAST_DUE: 'past_due',
CANCELED: 'canceled', CANCELED: 'canceled',
EXPIRED: 'expired' EXPIRED: 'expired'
} as const }
// 软件订阅方案类型 // 软件订阅方案类型
export type SubscriptionPlanType = { export type SubscriptionPlanType = {
@@ -349,7 +349,7 @@ export type SubscriptionPlanType = {
features: UTSJSONObject | null // { featureKey: description } features: UTSJSONObject | null // { featureKey: description }
price: number // 单位:元(或分,取决于后端;前端以显示为准) price: number // 单位:元(或分,取决于后端;前端以显示为准)
currency: string | null // 'CNY' | 'USD' ... currency: string | null // 'CNY' | 'USD' ...
billing_period: keyof typeof SUBSCRIPTION_PERIOD | string // 'monthly' | 'yearly' billing_period: string // 'monthly' | 'yearly'
trial_days: number | null trial_days: number | null
is_active: boolean is_active: boolean
sort_order?: number | null sort_order?: number | null
@@ -362,7 +362,7 @@ export type UserSubscriptionType = {
id: string id: string
user_id: string user_id: string
plan_id: string plan_id: string
status: keyof typeof SUBSCRIPTION_STATUS | string status: string
start_date: string start_date: string
end_date: string | null end_date: string | null
next_billing_date: string | null next_billing_date: string | null
@@ -372,76 +372,6 @@ export type UserSubscriptionType = {
created_at?: string created_at?: string
updated_at?: string updated_at?: string
} }
export interface Product {
id: string
name: string
description: string | null // 允许null
price: number
original_price: number | null // 允许null
images: string[]
// ... 其他属性
}
// types/mall-types.uts
export interface ProductType {
id: string
name: string
description: string
price: number
original_price: number
images: string[]
category_id: string
status: number
stock: number
sales: number
tags: string[]
created_at: string
updated_at: string
}
export interface CategoryType {
id: string
name: string
icon_url: string
parent_id: string | null
is_active: boolean
sort_order: number
}
export interface BannerType {
id: string
title: string
image_url: string
link_url: string
sort_order: number
status: number
}
export interface CouponTemplateType {
id: string
name: string
discount_value: number
min_order_amount: number
start_time: string
end_time: string
status: number
per_user_limit: number
}
export interface CartItemType {
id: string
user_id: string
product_id: string
quantity: number
created_at: string
}
// Mock 数据类型
export interface MockData {
banners: BannerType[]
categories: CategoryType[]
coupons: CouponTemplateType[]
products: ProductType[]
}
// 用户基础信息类型 (兼容 pages/user/types.uts) // 用户基础信息类型 (兼容 pages/user/types.uts)
export type UserProfile = { export type UserProfile = {

View File

@@ -1,5 +1,5 @@
import { AkReqUploadOptions, AkReqOptions, AkReqResponse, AkReqError } from './interface.uts'; import { AkReqUploadOptions, AkReqOptions, AkReqResponse, AkReqError } from './interface.uts';
import { SUPA_URL } from '@/ak/config.uts'; import { SUPA_URL, IS_TEST_MODE } from '@/ak/config.uts';
// token 持久化 key // token 持久化 key
const ACCESS_TOKEN_KEY = 'akreq_access_token'; const ACCESS_TOKEN_KEY = 'akreq_access_token';
@@ -231,8 +231,9 @@ export class AkReq {
} catch (e) {} } catch (e) {}
try { try {
// 动态读取配置,避免 ak-req 模块与业务工程强耦合 // 动态读取配置,避免 ak-req 模块与业务工程强耦合
const cfg = require('@/ak/config.uts') as any // const cfg = require('@/ak/config.uts') as any
const isTest = cfg != null ? (cfg.IS_TEST_MODE === true) : false // const isTest = cfg != null ? (cfg.IS_TEST_MODE === true) : false
const isTest = IS_TEST_MODE
// if (!isTest) { // if (!isTest) {
// uni.reLaunch({ url: '/pages/user/login' }); // uni.reLaunch({ url: '/pages/user/login' });
// } // }

View File

@@ -19,21 +19,29 @@ function t(key: string, values: UTSJSONObject | null = null, locale: string | nu
} }
// 创建响应式 locale 对象 // 创建响应式 locale 对象
const localeObj = { class LocaleWrapper {
get value(): string { get value(): string {
return currentLocale return currentLocale
}, }
set value(newLocale: string) { set value(newLocale: string) {
currentLocale = newLocale currentLocale = newLocale
}
}
const localeObj = new LocaleWrapper()
// I18n Global Context
class I18nGlobal {
t(key: string, values: UTSJSONObject | null = null, locale: string | null = null): string {
return t(key, values, locale)
} }
locale: LocaleWrapper = localeObj
} }
// 导出 i18n 对象(兼容 Vue I18n 的 API // I18n Instance
const i18nInstance = { class I18nInstance {
global: { global: I18nGlobal = new I18nGlobal()
t: t,
locale: localeObj
}
} }
export default i18nInstance // 导出 i18n 对象
const i18n = new I18nInstance()
export default i18n

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"version":3,"sources":["pages/mall/consumer/refund-review.uvue","pages/user/terms.uvue","uni_modules/ak-req/ak-req.uts"],"sourcesContent":["<template>\r\n <view class=\"review-page\">\r\n <view class=\"header\">\r\n <text class=\"title\">服务评价</text>\r\n <text class=\"subtitle\">请对本次售后服务进行评价</text>\r\n </view>\r\n\r\n <view class=\"rating-section\">\r\n <text class=\"label\">服务评分</text>\r\n <view class=\"stars\">\r\n <text \r\n v-for=\"i in 5\" \r\n :key=\"i\" \r\n class=\"star\" \r\n :class=\"{ active: i <= rating }\"\r\n @click=\"setRating(i)\"\r\n >★</text>\r\n </view>\r\n <text class=\"rating-text\">{{ ratingText }}</text>\r\n </view>\r\n\r\n <view class=\"comment-section\">\r\n <textarea \r\n v-model=\"comment\" \r\n class=\"comment-input\" \r\n placeholder=\"请输入您的评价内容,您的建议是我们改进的动力\"\r\n maxlength=\"200\"\r\n />\r\n <text class=\"word-count\">{{ comment.length }}/200</text>\r\n </view>\r\n\r\n <button class=\"submit-btn\" @click=\"submitReview\" :loading=\"submitting\">提交评价</button>\r\n </view>\r\n</template>\r\n\r\n<script setup lang=\"uts\">\r\nimport { ref, computed } from 'vue'\r\n\r\nconst rating = ref(5)\r\nconst comment = ref('')\r\nconst submitting = ref(false)\r\n\r\nconst ratingText = computed(() => {\r\n const texts = ['非常不满意', '不满意', '一般', '满意', '非常满意']\r\n return texts[rating.value - 1]\r\n})\r\n\r\nconst setRating = (val: number) => {\r\n rating.value = val\r\n}\r\n\r\nconst submitReview = () => {\r\n if (submitting.value) return\r\n submitting.value = true\r\n \r\n // 模拟提交\r\n setTimeout(() => {\r\n uni.showToast({\r\n title: '评价成功',\r\n icon: 'success'\r\n })\r\n \r\n setTimeout(() => {\r\n uni.navigateBack()\r\n }, 1500)\r\n \r\n submitting.value = false\r\n }, 1000)\r\n}\r\n</script>\r\n\r\n<style scoped>\r\n.review-page {\r\n flex: 1;\r\n background-color: #ffffff;\r\n padding: 20px;\r\n}\r\n\r\n.header {\r\n margin-bottom: 30px;\r\n text-align: center;\r\n}\r\n\r\n.title {\r\n font-size: 20px;\r\n font-weight: bold;\r\n color: #333;\r\n margin-bottom: 10px;\r\n}\r\n\r\n.subtitle {\r\n font-size: 14px;\r\n color: #999;\r\n}\r\n\r\n.rating-section {\r\n display: flex;\r\n flex-direction: column;\r\n align-items: center;\r\n margin-bottom: 30px;\r\n}\r\n\r\n.label {\r\n font-size: 16px;\r\n color: #333;\r\n margin-bottom: 15px;\r\n}\r\n\r\n.stars {\r\n display: flex;\r\n margin-bottom: 10px;\r\n}\r\n\r\n.star {\r\n margin-right: 10px;\r\n font-size: 32px;\r\n color: #ddd;\r\n transition: color 0.2s;\r\n}\r\n\r\n.star.active {\r\n color: #ffca28;\r\n}\r\n\r\n.rating-text {\r\n font-size: 14px;\r\n color: #666;\r\n}\r\n\r\n.comment-section {\r\n position: relative;\r\n margin-bottom: 30px;\r\n}\r\n\r\n.comment-input {\r\n width: 100%;\r\n height: 120px;\r\n background-color: #f5f5f5;\r\n border-radius: 8px;\r\n padding: 15px;\r\n font-size: 14px;\r\n box-sizing: border-box;\r\n}\r\n\r\n.word-count {\r\n position: absolute;\r\n bottom: 10px;\r\n right: 10px;\r\n font-size: 12px;\r\n color: #999;\r\n}\r\n\r\n.submit-btn {\r\n background-color: #007aff;\r\n color: #ffffff;\r\n border-radius: 25px;\r\n font-size: 16px;\r\n font-weight: bold;\r\n height: 50px;\r\n line-height: 50px;\r\n}\r\n</style>\r\n",null,null],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;YAsCA,IAAM,SAAS,IAAI,CAAC;YACpB,IAAM,UAAU,IAAI;YACpB,IAAM,aAAa,IAAI,KAAK;YAE5B,IAAM,aAAa,SAAS,OAAA,MAAA,CAAK;gBAC/B,IAAM,QAAQ;oBAAC;oBAAS;oBAAO;oBAAM;oBAAM;iBAAO;gBAClD,OAAO,KAAK,CAAC,OAAO,KAAK,CAAA,CAAA,CAAG,CAAC,CAAC;YAChC;;YAEA,IAAM,YAAY,IAAC,MAAK,MAAM,CAAI;gBAChC,OAAO,KAAK,GADK;YAEnB;YAEA,IAAM,eAAe,KAAK;gBACxB,IAAI,WAAW,KAAK;oBAAE;;gBACtB,WAAW,KAAK,GAAG,IAAI;gBAGvB,WAAW,KAAK;mDAEZ,QAAO,QACP,OAAM;oBAGR,WAAW,KAAK;;oBAEhB;sBAAG,IAAI;oBAEP,WAAW,KAAK,GAAG,KAAK;gBAC1B;kBAAG,IAAI;YACT;;uBAnEE,IA+BO,QAAA,IA/BD,WAAM,gBAAa;oBACvB,IAGO,QAAA,IAHD,WAAM,WAAQ;wBAClB,IAA+B,QAAA,IAAzB,WAAM,UAAQ;wBACpB,IAA0C,QAAA,IAApC,WAAM,aAAW;;oBAGzB,IAYO,QAAA,IAZD,WAAM,mBAAgB;wBAC1B,IAA+B,QAAA,IAAzB,WAAM,UAAQ;wBACpB,IAQO,QAAA,IARD,WAAM,UAAO;4BACjB,IAMS,UAAA,IAAA,EAAA,cAAA,UAAA,CALK,CAAC,EAAA,IAAN,GAAA,OAAA,SAAC,UAAA,GAAA,CAAA;uCADV,IAMS,QAAA,IAJN,SAAK,GACN,WAAK,IAAA;oCAAC;oCACE,IAAA,aAAA,EAAA,EAAA,CAAA,OAAA,KAAA;iCAAuB,GAC9B,aAAK,KAAA;oCAAE,UAAU;gCAAC;mCACpB,KAAC,EAAA,EAAA;oCAAA;iCAAA;;;;wBAEJ,IAAiD,QAAA,IAA3C,WAAM,gBAAa,IAAI,WAAA,KAAU,GAAA,CAAA;;oBAGzC,IAQO,QAAA,IARD,WAAM,oBAAiB;wBAC3B,IAKE,YAAA,oBAJS,QAAA,KAAO;4BAAP,QAAO,KAAA,GAAA,SAAA,MAAA,CAAA,KAAA;wBAAA;0BAChB,WAAM,iBACN,iBAAY,0BACZ,eAAU;;;;wBAEZ,IAAwD,QAAA,IAAlD,WAAM,eAAY,IAAI,QAAA,KAAO,CAAC,MAAM,EAAA,CAAA,CAAG,QAAI,CAAA;;oBAGnD,IAAoF,UAAA,IAA5E,WAAM,cAAc,aAAO,cAAe,aAAS,WAAA,KAAU,GAAE,QAAI,CAAA,EAAA;wBAAA;qBAAA"}

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More