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
// IP: 192.168.1.62
// IP: 192.168.1.62
// Kong HTTP Port: 8000
//自己的配置自己解开即可
//export const SUPA_URL: string = 'http://192.168.1.61:18000'
//export const SUPA_KEY: string = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlLTEiLCJpYXQiOjE3Njk2NzY0OTgsImV4cCI6MTkyNzM1NjQ5OH0.ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'
export const SUPA_URL: string = 'http://192.168.1.61:18000'
export const SUPA_KEY: string = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlLTEiLCJpYXQiOjE3Njk2NzY0OTgsImV4cCI6MTkyNzM1NjQ5OH0.ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'
//export const SUPA_URL: string = 'http://192.168.1.62:18000'
//export const SUPA_KEY: string = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlLTEiLCJpYXQiOjE3Njk2NzY0OTgsImV4cCI6MTkyNzM1NjQ5OH0.ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'
//export const SUPA_URL: string = 'http://192.168.1.63:18000'
//export const SUPA_KEY: string = 'eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJyb2xlIjogImFub24iLCAiaXNzIjogInN1cGFiYXNlIiwgImlhdCI6IDE3Njk4NDczMzQsICJleHAiOiAyMDg1MjA3MzM0fQ.js-2CS5_cUmf4iVv8aCmmx9iyFsQvLNDbt8YYOngeLU'
// WebSocket 实时连接(内网使用 ws:// 而非 wss://
export const WS_URL: string = 'ws://192.168.1.61:18000/realtime/v1/websocket'
//export const WS_URL: string = 'ws://192.168.1.62:18000/realtime/v1/websocket'
//export const WS_URL: string = 'ws://192.168.1.63:18000/realtime/v1/websocket'
//export const WS_URL: string = 'ws://localhost:18000/realtime/v1/websocket'
// 备用配置(已注释,如需切换可取消注释)
// 开发环境 - 其他内网地址
@@ -37,4 +33,4 @@ export const HOME_REDIRECT: string = '/pages/mall/consumer/index'
export const TABORPAGE: string = '/pages/mall/consumer/index'
// 测试模式:放开任意跳转(禁用启动页/登录/401 的强制重定向)
export const IS_TEST_MODE: boolean = true
export const IS_TEST_MODE: boolean = true

View File

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

View File

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

View File

@@ -79,11 +79,11 @@ export class AkSupaQueryBuilder {
like(field : string, value : any) : AkSupaQueryBuilder { return this._addCond(field, 'like', value); }
ilike(field : string, value : any) : AkSupaQueryBuilder { return this._addCond(field, 'ilike', value); }
in(field : string, value : any[]) : AkSupaQueryBuilder { return this._addCond(field, 'in', value); }
is(field : string, value : any) : AkSupaQueryBuilder { return this._addCond(field, 'is', value); }
is(field : string, value : any | null) : AkSupaQueryBuilder { return this._addCond(field, 'is', value); }
contains(field : string, value : any) : AkSupaQueryBuilder { return this._addCond(field, 'cs', value); }
containedBy(field : string, value : any) : AkSupaQueryBuilder { return this._addCond(field, 'cd', value); }
not(field : string, opOrValue : any, value?: any) : AkSupaQueryBuilder {
if (value !== undefined) {
not(field : string, opOrValue : any, value: any | null = null) : AkSupaQueryBuilder {
if (value != null) {
// 三元形式field, operator, value
// 例如 not('badge', 'is', null) -> badge=not.is.null
const combinedOp = 'not.' + opOrValue;
@@ -113,7 +113,7 @@ export class AkSupaQueryBuilder {
return this;
}
private _addCond(afield : string, op : string, value : any) : AkSupaQueryBuilder {
private _addCond(afield : string, op : string, value : any | null) : AkSupaQueryBuilder {
//console.log('add cond:', op, afield, value)
const field = encodeURIComponent(afield)!!
// 将 null 转换为字符串 'null',避免构造对象时缺少 value 属性
@@ -266,7 +266,7 @@ export class AkSupaQueryBuilder {
return params.length > 0 ? params.join('&') : null;
}
select(columns ?: string, opt ?: UTSJSONObject) : AkSupaQueryBuilder {
select(columns : string = "*", opt : UTSJSONObject | null = null) : AkSupaQueryBuilder {
this._action = 'select';
if (columns != null) {
this._options.columns = columns;
@@ -692,7 +692,7 @@ export class AkSupa {
token_type: 'bearer',
expires_in: 0,
raw: user
} as any;
} as AkSupaSignInResult;
}
return true;
} catch (e) {
@@ -999,6 +999,22 @@ async delete(table : string, filter : string | null) : Promise<AkReqResponse<any
from(tableName : string) : AkSupaQueryBuilder {
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
async refreshSession() : Promise<boolean> {
if (this.session == null || this.session?.refresh_token == null) return false;
@@ -1133,4 +1149,122 @@ export function createClient(url : string, key : string) : AkSupa {
return new AkSupa(url, key);
}
// 模拟 Realtime Channel 类 (Polling Fallback)
export class AkSupaRealtimeChannel {
private _supa: AkSupa;
private _topic: string;
private _timer: number = 0;
private _callback: ((payload: any) => void) | null = null;
private _table: string = '';
private _lastTime: string = new Date().toISOString();
private _isSubscribed: boolean = false;
constructor(supa: AkSupa, topic: string) {
this._supa = supa;
this._topic = topic;
}
// 绑定事件 (仅支持 postgres_changes INSERT)
on(type: string, filter: UTSJSONObject, callback: (payload: any) => void): AkSupaRealtimeChannel {
// 解析 table
const table = filter.getString('table');
if (table != null) {
this._table = table;
}
this._callback = callback;
return this;
}
// 开始订阅
subscribe(callback?: (status: string, err: any | null) => void): AkSupaRealtimeChannel {
if (this._isSubscribed) return this;
this._isSubscribed = true;
// 初始回调
if (callback != null) {
callback('SUBSCRIBED', null);
}
// 如果没有指定 table无法轮询
if (this._table == '') {
console.warn('Realtime check: No table specified for polling.');
return this;
}
// 开始轮询 (每3秒)
this._timer = setInterval(() => {
this._checkUpdates();
}, 3000);
return this;
}
// 停止订阅
unsubscribe() {
if (this._timer > 0) {
clearInterval(this._timer);
this._timer = 0;
}
this._isSubscribed = false;
}
// 检查更新
private async _checkUpdates() {
if (!this._isSubscribed || this._table == '') return;
try {
const now = new Date().toISOString();
const res = await this._supa
.from(this._table)
.select('*')
.gt('created_at', this._lastTime)
.order('created_at', { ascending: true })
.execute();
if (res.error == null && res.data != null) {
let list: any[] = [];
if (Array.isArray(res.data)) {
list = res.data as any[];
}
if (list.length > 0) {
// 更新最后时间
const lastItem = list[list.length - 1];
let lastTimeStr: string | null = null;
if (lastItem instanceof UTSJSONObject) {
lastTimeStr = lastItem.getString('created_at');
} else {
// 尝试转 json
const j = JSON.parse(JSON.stringify(lastItem)) as UTSJSONObject;
lastTimeStr = j.getString('created_at');
}
if (lastTimeStr != null) {
this._lastTime = lastTimeStr;
} else {
this._lastTime = now;
}
// 触发回调
if (this._callback != null) {
// 模拟 Realtime payload
list.forEach(item => {
const payload = {
new: item,
eventType: 'INSERT',
old: null
};
this._callback?.(payload);
});
}
}
}
} catch (e) {
console.error('Realtime polling error:', e);
}
}
}
export default AkSupa;

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 {
font-size: 16px;
font-weight: 600;
font-weight: 700;
color: #333;
}
}

View File

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

View File

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

View File

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

View File

@@ -1,74 +1,93 @@
{
"name": "mall",
"appid": "__UNI__YOUR_APP_ID__",
"description": "A multi-role e-commerce application.",
"versionName": "1.0.0",
"versionCode": "100",
"transformPx": false,
"app-plus": {
"usingComponents": true,
"nvueStyleCompiler": "uni-app",
"compilerVersion": 3,
"splashscreen": {
"alwaysShowBeforeRender": true,
"waiting": true,
"autoclose": true,
"delay": 0
},
"modules": {},
"distribute": {
"android": {
"permissions": [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>",
"<uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\"/>",
"<uses-permission android:name=\"android.permission.INTERNET\"/>"
]
},
"ios": {},
"sdkConfigs": {}
}
},
"quickapp": {},
"mp-weixin": {
"appid": "",
"setting": {
"urlCheck": false
},
"usingComponents": true
},
"mp-alipay": {
"usingComponents": true
},
"mp-baidu": {
"usingComponents": true
},
"mp-toutiao": {
"usingComponents": true
},
"uniStatistics": {
"enable": false
},
"vueVersion": "3",
"uni-app-x": {},
"h5": {
"title": "mall",
"router": {
"mode": "hash",
"base": "./"
}
}
"name": "mall",
"appid": "__UNI__YOUR_APP_ID__",
"description": "A multi-role e-commerce application.",
"versionName": "1.0.0",
"versionCode": "100",
"transformPx": false,
"app-plus": {
"usingComponents": true,
"nvueStyleCompiler": "uni-app",
"compilerVersion": 3,
"splashscreen": {
"alwaysShowBeforeRender": true,
"waiting": true,
"autoclose": true,
"delay": 0
},
"modules": {},
"distribute": {
"android": {
"permissions": [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>",
"<uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\"/>",
"<uses-permission android:name=\"android.permission.INTERNET\"/>"
]
},
"ios": {},
"sdkConfigs": {}
}
},
"quickapp": {},
"mp-weixin": {
"appid": "",
"setting": {
"urlCheck": false
},
"usingComponents": true
},
"mp-alipay": {
"usingComponents": true
},
"mp-baidu": {
"usingComponents": true
},
"mp-toutiao": {
"usingComponents": true
},
"uniStatistics": {
"enable": false
},
"vueVersion": "3",
"uni-app-x": {},
"h5": {
"title": "mall",
"router": {
"mode": "hash",
"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"
}
},
{
"path": "pages/mall/admin/homePage/index",
"style": {
"navigationBarTitleText": "管理后台",
"navigationStyle": "custom"
}
},
// {
// "path": "pages/mall/admin/homePage/index",
// "style": {
// "navigationBarTitleText": "管理后台",
// "navigationStyle": "custom"
// }
// },
{
"path": "pages/user/boot",
"style": {
@@ -50,6 +50,24 @@
"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": {
@@ -61,7 +79,8 @@
{
"path": "pages/mall/consumer/category",
"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": {
"color": "#999999",

View File

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

View File

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

View File

@@ -34,8 +34,8 @@
</template>
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { ref, onMounted, getCurrentInstance } from 'vue'
import { onShow, onLoad } from '@dcloudio/uni-app'
import { supabaseService, type UserAddress as SupabaseUserAddress } from '@/utils/supabaseService.uts'
type Address = {
@@ -52,25 +52,6 @@ type Address = {
const addresses = ref<Address[]>([])
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 () => {
try {
@@ -78,17 +59,19 @@ const loadAddresses = async () => {
const supabaseAddresses = await supabaseService.getAddresses()
// 转换数据格式以匹配前端界面
const transformedAddresses = supabaseAddresses.map((item: SupabaseUserAddress) => ({
id: item.id,
name: item.recipient_name,
phone: item.phone,
province: item.province,
city: item.city,
district: item.district,
detail: item.detail_address,
isDefault: item.is_default,
label: '' // Supabase表没有label字段可以后续考虑添加或使用其他字段
}))
const transformedAddresses = supabaseAddresses.map((item: SupabaseUserAddress): Address => {
return {
id: item.id,
name: item.recipient_name,
phone: item.phone,
province: item.province,
city: item.city,
district: item.district,
detail: item.detail_address,
isDefault: item.is_default,
label: '' // Supabase表没有label字段
} as Address
})
addresses.value = transformedAddresses
@@ -98,7 +81,7 @@ const loadAddresses = async () => {
console.error('加载地址数据失败:', error)
// 如果API调用失败尝试从本地存储加载
const storedAddresses = uni.getStorageSync('addresses')
if (storedAddresses) {
if (storedAddresses != null) {
try {
addresses.value = JSON.parse(storedAddresses as string) as Address[]
} 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 => {
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({
title: '提示',
content: '确定要删除该地址吗?',
success: async (res) => {
success: (res) => {
if (res.confirm) {
// 调用Supabase服务删除地址
const success = await supabaseService.deleteAddress(id)
if (success) {
// 从本地列表移除
const index = addresses.value.findIndex(addr => addr.id === id)
if (index !== -1) {
addresses.value.splice(index, 1)
// 更新本地存储缓存
uni.setStorageSync('addresses', JSON.stringify(addresses.value))
supabaseService.deleteAddress(id).then((success) => {
if (success) {
// 从本地列表移除
const index = addresses.value.findIndex(addr => addr.id === id)
if (index !== -1) {
addresses.value.splice(index, 1)
// 更新本地存储缓存
uni.setStorageSync('addresses', JSON.stringify(addresses.value))
uni.showToast({
title: '删除成功',
icon: 'success'
})
}
} else {
console.error('删除地址失败')
uni.showToast({
title: '删除成功',
icon: 'success'
title: '删除失败',
icon: 'none'
})
}
} else {
console.error('删除地址失败')
uni.showToast({
title: '删除失败',
icon: 'none'
})
}
})
}
}
})
@@ -162,8 +158,8 @@ const editAddress = (id: string) => {
}
const selectAddress = (item: Address) => {
if (selectionMode.value && openerEventChannel) {
openerEventChannel.emit('addressSelected', {
if (selectionMode.value) {
uni.$emit('addressSelected', {
id: item.id,
recipient_name: item.name,
phone: item.phone,
@@ -188,7 +184,14 @@ const selectAddress = (item: Address) => {
flex-direction: column; /* 竖向排列图标 */
justify-content: center;
align-items: center;
gap: 15px;
}
.action-item {
margin-bottom: 15px;
}
.action-item:last-child {
margin-bottom: 0px;
}
.footer-btn {
@@ -198,11 +201,12 @@ const selectAddress = (item: Address) => {
right: 0;
background-color: white;
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);
display: flex;
justify-content: center; /* 居中显示 */
justify-content: center;
align-items: center;
z-index: 100;
}
.add-btn {
@@ -214,7 +218,6 @@ const selectAddress = (item: Address) => {
line-height: 44px;
border: none;
width: 100%; /* 默认占满 */
max-width: 100%;
}
/* 响应式布局优化 */
@@ -234,7 +237,7 @@ const selectAddress = (item: Address) => {
left: 50%;
transform: translateX(-50%);
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 {

View File

@@ -131,7 +131,8 @@
</template>
<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'
type AddressType = {
@@ -150,7 +151,6 @@ type AddressType = {
const addressList = ref<Array<AddressType>>([])
const fromSelect = ref<boolean>(false)
const selectCallback = ref<any>(null)
const showNewAddressForm = ref<boolean>(false)
const newAddress = ref<any>({
recipient_name: '',
@@ -163,16 +163,14 @@ const newAddress = ref<any>({
})
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(() => {
const eventChannel = uni.getEventChannel()
if (eventChannel) {
eventChannel.on('fromSelect', (data: any) => {
fromSelect.value = data.fromSelect || false
selectCallback.value = data.callback
})
}
loadAddresses()
// 监听地址更新事件从checkout页面或其他页面
@@ -222,7 +220,11 @@ const loadAddresses = async () => {
// 获取当前用户ID
const getCurrentUserId = (): string | null => {
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) => {
if (fromSelect.value && selectCallback.value) {
// 返回选择的地址
selectCallback.value(address)
if (fromSelect.value) {
try {
uni.setStorageSync('selectedAddress', JSON.stringify(address))
} catch (e) {}
uni.$emit('addressSelected', address)
uni.navigateBack()
}
}

View File

@@ -4,11 +4,11 @@
<view class="section-title">退款类型</view>
<radio-group @change="handleTypeChange" class="type-group">
<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>
</label>
<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>
</label>
</radio-group>
@@ -56,7 +56,8 @@
</template>
<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'
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 () => {
try {
const orderData = await supabaseService.getOrderDetail(orderId.value)
if (orderData != null) {
const total = Number(orderData['total_amount'] ?? 0)
const shipping = Number(orderData['shipping_fee'] ?? 0)
// Cast to UTSJSONObject to access properties safely
const order = orderData as UTSJSONObject
const total = order.getNumber('total_amount') ?? 0
const shipping = order.getNumber('shipping_fee') ?? 0
maxAmount.value = total
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) => {
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 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]
}
const submitRefund = async () => {
if (!refundReason.value) {
if (refundReason.value == '') {
uni.showToast({ title: '请选择退款原因', icon: 'none' })
return
}
@@ -166,7 +178,7 @@ const submitRefund = async () => {
<style scoped>
.apply-refund-page {
min-height: 100vh;
flex: 1;
background-color: #f5f5f5;
padding: 15px;
padding-bottom: 80px;
@@ -189,16 +201,19 @@ const submitRefund = async () => {
.type-group {
display: flex;
flex-direction: column;
gap: 15px;
}
.type-item {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 15px;
font-size: 14px;
}
.type-radio {
margin-right: 10px;
}
.picker-content {
display: flex;
justify-content: space-between;

View File

@@ -25,7 +25,7 @@
</view>
<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>
</template>
@@ -34,6 +34,14 @@
import { ref, reactive } from 'vue'
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 form = reactive({
holder_name: '',
@@ -41,7 +49,7 @@ const form = reactive({
bank_name: '',
phone: '',
is_default: false
})
} as BankCardForm)
const onSwitchChange = (e: UniSwitchChangeEvent) => {
form.is_default = e.detail.value
@@ -60,7 +68,7 @@ const detectBank = (e: any) => {
}
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' })
return
}
@@ -71,6 +79,7 @@ const submit = async () => {
const cardData = new UTSJSONObject()
cardData.set('holder_name', form.holder_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位
const last4 = form.card_no.length > 4 ? form.card_no.slice(-4) : form.card_no
cardData.set('card_no_last4', last4)
@@ -100,8 +109,7 @@ const submit = async () => {
<style>
.add-card-page {
background-color: #f5f5f5;
min-height: 100vh;
padding-top: 20px;
flex: 1;
}
.form-container {
@@ -146,7 +154,7 @@ const submit = async () => {
font-size: 16px;
}
.submit-btn[disabled] {
.submit-btn.disabled {
opacity: 0.6;
}
</style>

View File

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

View File

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

View File

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

View File

@@ -25,14 +25,11 @@
</view>
</view>
<!-- 导航栏占位 -->
<view class="navbar-placeholder" :style="{ height: (statusBarHeight + 44) + 'px' }"></view>
<!-- 分类内容区 -->
<view
class="category-content"
:style="{
marginTop: (statusBarHeight + headerHeight + 10) + 'px',
height: `calc(100vh - ${statusBarHeight + headerHeight + 10}px)`
}"
>
<view class="category-content">
<!-- 左侧一级分类 -->
<scroll-view scroll-y class="primary-category">
<view
@@ -88,7 +85,7 @@
</view>
<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">
<text class="sales-count">已售{{ product.sale_count }}</text>
</view>
@@ -116,12 +113,20 @@
<script setup lang="uts">
import { ref, onMounted } from 'vue'
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 headerHeight = ref(44) // 默认头部高度
const primaryCategories = ref<Category[]>([])
const primaryCategories = ref<LocalCategory[]>([])
const productList = ref<Product[]>([])
const activePrimary = ref<string>('')
const cartCount = ref(3)
@@ -137,67 +142,10 @@ const currentCategoryDesc = ref('')
// 页面参数
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 (!activePrimary.value) {
if (activePrimary.value == '') {
console.warn('activePrimary为空无法加载商品')
return
}
@@ -222,10 +170,10 @@ const loadProducts = async () => {
hasMore.value = response.hasmore
// 更新当前分类信息
const category = primaryCategories.value.find(cat => cat.id === activePrimary.value)
if (category) {
const category = primaryCategories.value.find((cat: LocalCategory): boolean => cat.id === activePrimary.value)
if (category != null) {
currentCategoryName.value = category.name
currentCategoryDesc.value = category.description || ''
currentCategoryDesc.value = category.description
}
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) {
currentPage.value++
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('传入的categoryId:', categoryId)
console.log('当前时间:', Date.now())
// 验证categoryId是否有效
if (!categoryId) {
if (categoryId == '') {
console.error('categoryId为空尝试使用第一个分类')
if (primaryCategories.value.length > 0) {
categoryId = primaryCategories.value[0].id
@@ -425,8 +280,8 @@ const selectPrimaryCategory = async (categoryId: string) => {
console.log('更新后的activePrimary:', activePrimary.value)
// 更新当前分类信息
const category = primaryCategories.value.find(cat => cat.id === categoryId)
if (category) {
const category = primaryCategories.value.find((cat: LocalCategory): boolean => cat.id === categoryId)
if (category != null) {
currentCategoryName.value = category.name
currentCategoryDesc.value = category.description
console.log('✅ 找到分类:', category.name, '描述:', category.description)
@@ -465,11 +320,159 @@ const selectPrimaryCategory = async (categoryId: string) => {
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: '添加中...' })
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) {
uni.showToast({
title: '已添加到购物车',
@@ -491,14 +494,15 @@ const addToCart = async (product: any) => {
}
// 导航函数
const navigateToSearch = () => uni.navigateTo({ url: '/pages/mall/consumer/search' })
const navigateToCart = () => uni.navigateTo({ url: '/pages/mall/consumer/cart' })
const navigateToProduct = (product: any) => {
const id = product.id
const price = (product.base_price || 0).toString()
const originalPrice = (product.market_price || '').toString()
const name = encodeURIComponent(product.name || '')
const image = encodeURIComponent(product.main_image_url || '')
function navigateToSearch(): void { uni.navigateTo({ url: '/pages/mall/consumer/search' }) }
function navigateToCart(): void { uni.navigateTo({ url: '/pages/mall/consumer/cart' }) }
function navigateToProduct(product: Product): void {
const id = (product.id ?? '').toString()
if (id === '') return
const price = (product.base_price ?? 0).toString()
const originalPrice = (product.market_price ?? '').toString()
const name = encodeURIComponent(product.name ?? '')
const image = encodeURIComponent(product.main_image_url ?? '')
uni.navigateTo({
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({
count: 1,
sourceType: ['camera'],
@@ -531,7 +535,7 @@ const onCamera = () => {
}
// 扫码功能
const onScan = () => {
function onScan(): void {
uni.scanCode({
success: (res) => {
console.log('扫码成功:', res)
@@ -550,7 +554,8 @@ const onScan = () => {
<style>
.category-page {
width: 100%;
min-height: 100vh;
height: 100%;
overflow: hidden;
background-color: #f8fafc;
display: flex;
flex-direction: column;
@@ -567,14 +572,20 @@ const onScan = () => {
box-shadow: 0 2px 12px rgba(76, 175, 80, 0.15);
}
/* 导航栏占位 */
.navbar-placeholder {
flex-shrink: 0;
}
/* 搜索栏 */
/* 导航栏搜索框容器内边距调整 */
.search-container {
height: 44px; /* 调整为与消息页一致的高度 */
height: 44px;
padding: 0 16px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
justify-content: center;
max-width: 1400px;
margin: 0 auto;
width: 100%;
@@ -593,12 +604,11 @@ const onScan = () => {
border-radius: 20px;
padding: 0 4px 0 12px;
display: flex;
flex-direction: row; /* UVUE 显式设置 row */
flex-direction: row;
align-items: center;
cursor: pointer;
transition: all 0.3s ease;
width: 100%;
height: 32px; /* 减小高度与顶部高度44px适配略小于顶部高度 */
height: 32px;
}
.search-placeholder {
@@ -613,7 +623,7 @@ const onScan = () => {
.nav-inner-search-text {
font-size: 12px; /* 字体稍微变小 */
color: #ffffff;
font-weight: 500;
font-weight: normal;
}
.icon {
@@ -683,29 +693,28 @@ const onScan = () => {
/* 分类内容区 */
.category-content {
flex: 1;
height: 0px;
display: flex;
flex-direction: row; /* 强制水平排列 */
/* margin-top: 44px; 已通过 style 动态绑定 */
flex-direction: row;
padding: 0 16px;
max-width: 1400px;
margin-left: auto;
margin-right: auto;
width: 100%;
gap: 20px;
/* height: calc(100vh - 44px); 已通过 style 动态绑定 */
overflow: hidden; /* 防止整体滚动 */
overflow: hidden;
}
/* 左侧一级分类 */
.primary-category {
width: 120px;
height: 100%; /* 占满父容器高度 */
margin-right: 20px; /* gap replacement */
background: white;
border-radius: 12px;
padding: 12px 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
flex-shrink: 0;
overflow-y: auto; /* 允许内部滚动 */
}
.primary-item {
@@ -716,7 +725,7 @@ const onScan = () => {
padding: 12px 8px;
margin: 4px 8px;
border-radius: 8px;
cursor: pointer;
/* cursor: pointer; removed for uniapp-x support */
transition: all 0.2s ease;
color: #666;
text-align: center;
@@ -736,13 +745,13 @@ const onScan = () => {
margin-bottom: 6px;
margin-right: 0; /* 移除右边距 */
text-align: center;
display: block;
/* display: block; removed for uniapp-x support */
}
.primary-name {
font-size: 13px;
line-height: 1.4;
display: block;
/* display: block; removed for uniapp-x support */
}
/* 右侧内容区 */
@@ -750,14 +759,13 @@ const onScan = () => {
flex: 1;
height: 100%; /* 占满父容器高度 */
padding: 0; /* 移除内边距,交给内部元素 */
overflow-y: auto; /* 允许内部滚动 */
}
.category-header {
margin-bottom: 16px;
padding: 16px 8px 0 8px;
position: sticky;
top: 0;
/* position: sticky; REMOVED for uniapp-x support */
/* top: 0; */
background-color: #f8fafc;
z-index: 10;
}
@@ -776,19 +784,27 @@ const onScan = () => {
/* 商品网格 */
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
display: flex;
flex-direction: row;
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 {
background: white;
border-radius: 12px;
overflow: hidden;
cursor: pointer;
/* cursor: pointer; removed for uniapp-x support */
transition: all 0.3s ease;
border: 1px solid #e0e0e0;
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 {
@@ -805,14 +821,14 @@ const onScan = () => {
font-size: 11px;
padding: 4px 12px;
border-radius: 12px;
font-weight: 600;
font-weight: 700;
z-index: 2;
}
.product-image {
width: 100%;
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;
}
@@ -822,10 +838,10 @@ const onScan = () => {
.product-name {
font-size: 15px;
font-weight: 600;
font-weight: 700;
color: #333;
margin-bottom: 4px;
display: block;
/* display: block; REMOVED for uniapp-x support */
line-height: 1.4;
}
@@ -833,19 +849,22 @@ const onScan = () => {
font-size: 13px;
color: #666;
margin-bottom: 12px;
display: block;
/* display: block; REMOVED for uniapp-x support */
}
.price-section {
display: flex;
align-items: baseline;
gap: 8px;
flex-direction: row;
align-items: flex-end; /* changed from baseline */
/* gap: 8px; */
margin-bottom: 12px;
}
.current-price {
display: flex;
align-items: baseline;
flex-direction: row;
align-items: flex-end; /* changed from baseline */
margin-right: 8px; /* gap replacement */
}
.price-symbol {
@@ -863,7 +882,7 @@ const onScan = () => {
.original-price {
font-size: 13px;
color: #999;
text-decoration: line-through;
/* text-decoration: line-through; REMOVED for uniapp-x support */
}
.product-meta {
@@ -890,14 +909,14 @@ const onScan = () => {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
/* gap: 6px; */
background: #4CAF50;
color: white;
padding: 8px 12px;
border-radius: 8px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
font-weight: bold;
/* cursor: pointer; removed for uniapp-x support */
transition: all 0.2s ease;
}
@@ -907,6 +926,7 @@ const onScan = () => {
.cart-icon {
font-size: 14px;
margin-right: 6px; /* gap replacement */
}
/* 空状态 */
@@ -946,7 +966,7 @@ const onScan = () => {
}
.load-text {
display: inline-block;
/* display: inline-block; REMOVED for uniapp-x support */
padding: 8px 16px;
background: #f5f5f5;
border-radius: 20px;
@@ -956,10 +976,19 @@ const onScan = () => {
/* 小屏手机 (小于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 {
/* flex-direction: column; 移除这一行,保持 row 布局 */
padding: 0 8px;
gap: 10px;
}
.primary-category {
@@ -967,6 +996,7 @@ const onScan = () => {
/* display: flex; 移除flex布局保持默认 */
/* flex-wrap: wrap; 移除换行 */
padding: 8px 0;
margin-right: 10px; /* Gap replacement */
}
.primary-item {
@@ -988,10 +1018,15 @@ const onScan = () => {
}
.product-grid {
grid-template-columns: repeat(2, 1fr); /* 改为双列显示 */
gap: 8px;
/* grid-template-columns: repeat(2, 1fr); REMOVED */
/* gap: 8px; REMOVED */
padding: 0 4px 20px 4px; /* 增加底部内边距 */
}
.product-card {
width: 48%; /* 2 columns for mobile */
margin: 1%;
}
/* 手机端商品卡片极简模式 - 仿照主页样式 */
.product-spec,
@@ -1017,9 +1052,10 @@ const onScan = () => {
margin-bottom: 2px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
/* display: -webkit-box; REMOVED for support */
/* -webkit-line-clamp: 2; REMOVED for support */
/* -webkit-box-orient: vertical; REMOVED for support */
lines: 2; /* UTS text truncation */
}
.price-section {
@@ -1043,7 +1079,7 @@ const onScan = () => {
.search-container {
padding: 0 12px;
height: 55px;
height: 44px;
}
.search-box {
@@ -1057,30 +1093,45 @@ const onScan = () => {
/* 中屏手机/小平板 (415px-768px) */
@media screen and (min-width: 415px) and (max-width: 768px) {
.product-grid {
grid-template-columns: repeat(2, 1fr);
gap: 16px;
.search-container {
padding: 0 16px;
height: 44px;
}
.product-card {
width: 46%;
margin: 2%;
}
}
/* 平板设备 (769px-1024px) */
@media screen and (min-width: 769px) and (max-width: 1024px) {
.product-grid {
grid-template-columns: repeat(3, 1fr);
gap: 20px;
.search-container {
padding: 0 16px;
height: 44px;
}
.product-card {
width: 30%;
margin: 1.5%;
}
}
/* 桌面端 (1025px以上) */
@media screen and (min-width: 1025px) {
.search-container {
padding: 0 16px;
height: 44px;
}
.category-content {
gap: 30px;
padding: 0 24px;
}
.primary-category {
width: 160px;
padding: 16px 0;
margin-right: 30px; /* Gap replacement */
}
.primary-item {
@@ -1116,12 +1167,14 @@ const onScan = () => {
}
.product-grid {
grid-template-columns: repeat(4, 1fr);
gap: 24px;
/* grid-template-columns: repeat(4, 1fr); REMOVED */
/* gap: 24px; REMOVED */
}
.product-card {
border-radius: 14px;
width: 22%; /* 4 columns */
margin: 1.5%;
}
.product-info {
@@ -1145,10 +1198,14 @@ const onScan = () => {
@media screen and (min-width: 1400px) {
.category-content {
max-width: 1600px;
gap: 40px;
/* gap: 40px; REMOVED */
padding: 0 32px;
}
.primary-category {
margin-right: 40px; /* Gap replacement */
}
.primary-category {
width: 200px;
padding: 20px 0;
@@ -1187,12 +1244,14 @@ const onScan = () => {
}
.product-grid {
grid-template-columns: repeat(5, 1fr);
gap: 28px;
/* grid-template-columns: repeat(5, 1fr); REMOVED */
/* gap: 28px; REMOVED */
}
.product-card {
border-radius: 16px;
width: 17%; /* 5 columns */
margin: 1.5%;
}
.product-image {

View File

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

View File

@@ -31,7 +31,7 @@
<!-- 时间分割线 -->
<view class="time-divider">
<text>今天 14:30</text>
<text class="time-text">今天 14:30</text>
</view>
<!-- 消息项 -->
@@ -45,11 +45,11 @@
<view v-if="message.type === 'received'" class="message-wrapper">
<image
class="avatar"
src="https://picsum.photos/40/40?random=1"
src="/static/icons/shop-default.png"
mode="aspectFill"
/>
<view class="message-content-wrapper">
<text class="sender-name">客服小美</text>
<text class="sender-name">{{ headerTitle }}</text>
<view class="message-bubble">
<text class="message-text">{{ message.content }}</text>
<text class="message-time">{{ message.time }}</text>
@@ -67,7 +67,7 @@
</view>
<image
class="avatar me"
src="https://picsum.photos/40/40?random=2"
src="/static/avatar-default.png"
mode="aspectFill"
/>
</view>
@@ -103,7 +103,7 @@
</view>
<!-- 表情选择器 -->
<view v-if="showEmoji" class="emoji-picker">
<scroll-view v-if="showEmoji" class="emoji-picker" direction="vertical">
<view class="emoji-category">
<text
v-for="emoji in emojiList"
@@ -114,7 +114,7 @@
{{ emoji }}
</text>
</view>
</view>
</scroll-view>
</view>
</template>
@@ -122,10 +122,18 @@
import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue'
import { supabaseService, type ChatMessage } from '@/utils/supabaseService.uts'
import supa from '@/components/supadb/aksupainstance.uts'
import type { AkSupaRealtimeChannel } from '@/components/supadb/aksupa.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 inputFocus = ref<boolean>(false)
const showEmoji = ref<boolean>(false)
@@ -134,11 +142,106 @@ const currentUserId = ref<string>('')
const merchantId = ref<string>('') // 商家ID
const headerTitle = ref<string>('在线客服')
const navPaddingTop = ref<string>('30px') // 默认值,包含状态栏高度+原有内边距
let realtimeChannel: any = null
let realtimeChannel: AkSupaRealtimeChannel | null = null
// 模拟表情列表
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) => {
// 动态获取状态栏高度
@@ -147,113 +250,47 @@ onLoad((options: any) => {
// 状态栏高度 + 10px 原有顶部内边距
navPaddingTop.value = (statusBarH + 10) + 'px'
if (options.merchantId) {
merchantId.value = options.merchantId
}
if (options.merchantName) {
headerTitle.value = options.merchantName
}
const optObj = (options instanceof UTSJSONObject) ? (options as UTSJSONObject) : (JSON.parse(JSON.stringify(options ?? {})) as UTSJSONObject)
const mid = optObj.getString('merchantId') ?? ''
if (mid !== '') {
merchantId.value = mid
}
const mname = optObj.getString('merchantName') ?? ''
if (mname !== '') {
headerTitle.value = mname
}
})
onMounted(async () => {
const user = await getCurrentUser()
if (user) {
currentUserId.value = user.id
}
loadChatHistory()
setupRealtimeSubscription()
onMounted(() => {
supabaseService.ensureSession().then((uid) => {
if (uid != null) {
currentUserId.value = uid
} else {
getCurrentUser().then((user) => {
if (user != null) {
currentUserId.value = user.id ?? ''
}
})
}
loadChatHistory()
setupRealtimeSubscription()
})
})
onUnmounted(() => {
if (realtimeChannel) {
supa.removeChannel(realtimeChannel)
if (realtimeChannel != null) {
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 content = inputMessage.value.trim()
if (!content) return
if (content == '') return
// 添加发送的消息 (乐观更新)
const newMessage = {
id: Date.now(),
const newMessage: UiChatMessage = {
id: Date.now().toString(),
type: 'sent',
content: content,
time: getCurrentTime()
@@ -266,23 +303,15 @@ const sendMessage = async () => {
scrollToBottom()
// 发送到 Supabase
// 如果有 merchantId,发送给指定商家,否则可能是发给系统或默认客服
const success = await supabaseService.sendChatMessage(content, merchantId.value || null)
if (!success) {
uni.showToast({
title: '发送失败',
icon: 'none'
})
// 实际项目中可能需要标记消息为发送失败状态
if (merchantId.value != '') {
const success = await supabaseService.sendMessage(merchantId.value, content)
if (!success) {
uni.showToast({
title: '发送失败',
icon: 'none'
})
}
}
// 移除模拟回复,依赖 Realtime 接收真实回复
/*
setTimeout(() => {
simulateCustomerReply()
}, 2000)
*/
}
// 模拟客服回复 (已禁用,改用 Realtime)
@@ -294,37 +323,19 @@ const simulateCustomerReply = async () => {
/* 移除不再使用的 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
inputFocus.value = true
}
// 显示表情选择器
const showEmojiPicker = () => {
function showEmojiPicker(): void {
showEmoji.value = !showEmoji.value
}
// 显示图片选择器
const showImagePicker = () => {
function showImagePicker(): void {
uni.chooseImage({
count: 1,
success: (res) => {
@@ -335,7 +346,7 @@ const showImagePicker = () => {
}
// 显示更多工具
const showMoreTools = () => {
function showMoreTools(): void {
uni.showActionSheet({
itemList: ['发送位置', '发送文件', '发送语音'],
success: (res) => {
@@ -345,7 +356,7 @@ const showMoreTools = () => {
}
// 显示更多操作
const showMoreActions = () => {
function showMoreActions(): void {
uni.showActionSheet({
itemList: ['投诉客服', '结束对话', '清除记录'],
success: (res) => {
@@ -389,7 +400,7 @@ const goBack = () => {
<style>
.chat-page {
width: 100%;
height: 100vh;
flex: 1;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
@@ -425,7 +436,6 @@ const goBack = () => {
font-size: 16px;
font-weight: bold;
color: #333;
display: block;
margin-bottom: 2px;
}
@@ -460,7 +470,7 @@ const goBack = () => {
background-color: #f0f0f0;
padding: 5px 15px;
border-radius: 15px;
display: inline-block;
}
/* 时间分割线 */
@@ -469,7 +479,7 @@ const goBack = () => {
margin: 20px 0;
}
.time-divider text {
.time-text {
font-size: 12px;
color: #999;
background-color: #f0f0f0;
@@ -499,18 +509,17 @@ const goBack = () => {
.avatar.me {
margin-right: 0;
margin-left: 10px;
order: 2;
/* order: 2; removed for uni-app-x */
}
.message-content-wrapper {
max-width: 70%;
/* max-width removed */
}
.sender-name {
font-size: 12px;
color: #999;
margin-bottom: 5px;
display: block;
}
.message-bubble {
@@ -519,8 +528,7 @@ const goBack = () => {
border-radius: 18px;
position: relative;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
max-width: 100%;
word-wrap: break-word;
/* max-width wrap removed */
}
.message-bubble.me {
@@ -528,7 +536,7 @@ const goBack = () => {
border-bottom-right-radius: 4px;
}
.message-bubble:not(.me) {
.message-bubble-not-me .message-content {
border-bottom-left-radius: 4px;
}
@@ -536,14 +544,12 @@ const goBack = () => {
font-size: 15px;
color: #333;
line-height: 1.4;
display: block;
margin-bottom: 5px;
}
.message-time {
font-size: 11px;
color: #999;
display: block;
text-align: right;
}
@@ -608,8 +614,8 @@ const goBack = () => {
background-color: white;
border-top: 1px solid #eee;
padding: 10px;
max-height: 200px;
overflow-y: auto;
height: 200px;
/* overflow-y removed */
position: fixed;
bottom: 60px;
left: 0;
@@ -633,24 +639,5 @@ const goBack = () => {
justify-content: center;
}
/* 响应式适配 */
@media screen and (max-width: 320px) {
.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>
/* 响应式适配 removed for strict uv-app-x compliance */
</style>

View File

@@ -146,24 +146,24 @@ const currentUserId = ref('')
const messages = ref<any[]>([])
const emojiList = ['😊', '😂', '👍', '👌', '❤️', '🌹', '🙏', '🎉', '😡', '😭', '🤔', '👋', '🤝', '💊', '🏥']
let realtimeChannel: any = null
let realtimeChannel: any | null = null
onLoad((options: any) => {
// 获取状态栏高度
const sys = uni.getSystemInfoSync()
statusBarHeight.value = sys.statusBarHeight || 0
statusBarHeight.value = sys.statusBarHeight ?? 0
// 获取参数
if (options.merchantId) {
merchantId.value = options.merchantId
merchantName.value = options.merchantName || '商家'
merchantLogo.value = options.merchantLogo || ''
console.log('开始聊天商家ID:', merchantId.value)
} else {
// 测试模式或默认客服
// uni.showToast({ title: '参数缺失', icon: 'none' })
merchantName.value = '平台客服'
}
const optObj = (options instanceof UTSJSONObject) ? (options as UTSJSONObject) : (JSON.parse(JSON.stringify(options ?? {})) as UTSJSONObject)
const mid = optObj.getString('merchantId') ?? ''
if (mid !== '') {
merchantId.value = mid
merchantName.value = optObj.getString('merchantName') ?? '商家'
merchantLogo.value = optObj.getString('merchantLogo') ?? ''
console.log('开始聊天商家ID:', merchantId.value)
} else {
merchantName.value = '平台客服'
}
// 获取当前用户
const uid = supabaseService.getCurrentUserId()
@@ -210,31 +210,35 @@ const loadMoreHistory = () => {
// 开启实时订阅
const startRealtimeSubscription = () => {
if (!currentUserId.value) return
if (currentUserId.value == '') return
console.log('开启消息监听...')
const filterObj = ({
event: 'INSERT',
schema: 'public',
table: 'ml_chat_messages'
} as UTSJSONObject)
realtimeChannel = supa.channel(`chat_${currentUserId.value}`)
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'ml_chat_messages',
filter: `receiver_id=eq.${currentUserId.value}`
},
(payload) => {
filterObj,
(payload: any) => {
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({
id: newMsg.id,
content: newMsg.content,
msg_type: newMsg.msg_type,
sender_id: newMsg.sender_id,
receiver_id: newMsg.receiver_id,
id: newMsg.getString('id') ?? '',
content: newMsg.getString('content') ?? '',
msg_type: newMsg.getString('msg_type') ?? '',
sender_id: senderId,
receiver_id: newMsg.getString('receiver_id') ?? '',
is_from_user: false, // 收到的一定不是自己发的
created_at: newMsg.created_at
created_at: newMsg.getString('created_at') ?? ''
} as ChatMessage)
messages.value.push(formatted)
@@ -271,7 +275,7 @@ const formatMessage = (m: ChatMessage): any => {
const sendMessage = async () => {
const text = inputMessage.value.trim()
if (!text) return
if (text == '') return
// 乐观更新 UI
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[]>([])
onMounted(() => {
loadCoupons()
})
const loadCoupons = async () => {
uni.showLoading({ title: '加载中...' })
try {
const userCoupons = await supabaseService.getUserCoupons(1) // 1: unused
coupons.value = userCoupons.map((item: UserCoupon) => {
const userCoupons = await supabaseService.getUserCoupons(1)
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 {
id: item.id,
title: item.template_name || '优惠券',
amount: `¥${item.amount || 0}`,
expiry: item.expire_at ? item.expire_at.substring(0, 10) : '长期有效'
title: (item.template_name != null && item.template_name !== '') ? item.template_name : '优惠券',
amount: `¥${amountVal}`,
expiry: expiryVal
} as Coupon
})
} catch (e) {
@@ -59,6 +59,10 @@ const loadCoupons = async () => {
}
}
onMounted(() => {
loadCoupons()
})
const useCoupon = (coupon: Coupon) => {
uni.switchTab({
url: '/pages/mall/consumer/index'
@@ -70,7 +74,7 @@ const useCoupon = (coupon: Coupon) => {
.coupons-page {
padding: 15px;
background-color: #f5f5f5;
min-height: 100vh;
flex: 1;
}
.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>
<view class="favorites-page">
<scroll-view class="favorites-page" direction="vertical">
<view class="product-grid">
<view v-if="favorites.length === 0" class="empty-state">
<text class="empty-icon">❤️</text>
@@ -26,7 +26,7 @@
</view>
</view>
</view>
</view>
</scroll-view>
</template>
<script setup lang="uts">
@@ -45,13 +45,9 @@ type Product = {
const favorites = ref<Product[]>([])
onMounted(() => {
loadFavorites()
})
const addToCart = async (product: Product) => {
uni.showLoading({ title: '添加中' })
const success = await supabaseService.addToCart(product.id, 1)
const success = await supabaseService.addToCart(product.id, 1, '')
uni.hideLoading()
if (success) {
uni.showToast({ title: '已添加到购物车', icon: 'success' })
@@ -63,13 +59,16 @@ const addToCart = async (product: Product) => {
const loadFavorites = async () => {
const res = await supabaseService.getFavorites()
// Map response
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) {
prod = item.get('ml_products')
itemObj = item as UTSJSONObject
prod = itemObj.get('ml_products')
} else {
prod = item['ml_products']
itemObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
prod = itemObj.get('ml_products')
}
let image = '/static/default-product.png'
@@ -79,46 +78,33 @@ const loadFavorites = async () => {
let sales = 0
if (prod != null) {
let prodObj: UTSJSONObject
if (prod instanceof UTSJSONObject) {
id = prod.getString('id') || ''
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) {}
}
}
prodObj = prod as UTSJSONObject
} else {
id = (prod['id'] as string) || ''
name = (prod['name'] as string) || '未知商品'
price = (prod['base_price'] as number) || 0
image = (prod['main_image_url'] as string) || image
sales = (prod['sale_count'] as number) || 0
if (image === '/static/default-product.png') {
const imgUrls = prod['image_urls'] as string
if (imgUrls) {
try {
const arr = JSON.parse(imgUrls)
if (Array.isArray(arr) && arr.length > 0) image = arr[0] as string
} catch(e) {}
}
}
prodObj = JSON.parse(JSON.stringify(prod)) as UTSJSONObject
}
id = prodObj.getString('id') ?? ''
name = prodObj.getString('name') ?? '未知商品'
price = prodObj.getNumber('base_price') ?? 0
image = prodObj.getString('main_image_url') ?? image
sales = prodObj.getNumber('sale_count') ?? 0
if (image === '/static/default-product.png') {
const imgUrls = prodObj.getString('image_urls')
if (imgUrls != null) {
try {
const arr = JSON.parse(imgUrls)
if (Array.isArray(arr) && arr.length > 0) {
image = arr[0] as string
}
} catch(e) {}
}
}
} else {
// 如果没取到商品,尝试直接从 item 取 target_id
if (item instanceof UTSJSONObject) {
id = item.getString('target_id') || ''
} else {
id = (item['target_id'] as string) || ''
if (itemObj != null) {
id = itemObj.getString('target_id') ?? ''
}
}
@@ -130,7 +116,7 @@ const loadFavorites = async () => {
sales: sales,
shopId: '',
shopName: ''
}
} as Product
})
}
@@ -146,50 +132,53 @@ const goToDetail = (id: string) => {
})
}
const removeFavorite = async (id: string) => {
const removeFavorite = (id: string) => {
uni.showModal({
title: '取消收藏',
content: '确定要取消收藏该商品吗?',
success: async (res) => {
success: (res) => {
if (res.confirm) {
// toggleFavorite 返回最新的状态true=已收藏false=未收藏
const isStillFavorite = await supabaseService.toggleFavorite(id)
if (!isStillFavorite) {
// 现在的状态是"未收藏",说明取消成功
// Remove from local list
const index = favorites.value.findIndex(item => item.id === id)
supabaseService.toggleFavorite(id).then((isStillFavorite) => {
if (!isStillFavorite) {
const index = favorites.value.findIndex((item): Boolean => {
return item.id === id
})
if (index !== -1) {
favorites.value.splice(index, 1)
favorites.value.splice(index, 1)
}
uni.showToast({
title: '已取消收藏',
icon: 'none'
title: '已取消收藏',
icon: 'none'
})
} else {
} else {
uni.showToast({
title: '取消失败',
icon: 'none'
title: '取消失败',
icon: 'none'
})
}
}
})
}
}
})
}
onMounted(() => {
loadFavorites()
})
</script>
<style>
.favorites-page {
padding: 15px;
background-color: #f5f5f5;
min-height: 100vh;
flex: 1;
}
.product-grid {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 15px;
justify-content: space-between;
width: 100%;
}
@@ -224,7 +213,7 @@ const removeFavorite = async (id: string) => {
}
.product-item {
width: calc(50% - 8px); /* Default Mobile: 2 items per row */
width: 48%; /* Default Mobile: 2 items per row */
background-color: white;
border-radius: 8px;
overflow: hidden;
@@ -236,13 +225,13 @@ const removeFavorite = async (id: string) => {
/* PC/Tablet Responsive */
@media (min-width: 768px) {
.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) {
.product-item {
width: calc(16.66% - 13px) !important; /* PC: 6 items */
width: 15% !important; /* PC: 6 items */
}
/* Center content on large screens */
@@ -269,9 +258,8 @@ const removeFavorite = async (id: string) => {
font-size: 14px;
color: #333;
margin-bottom: 6px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
text-overflow: ellipsis;
lines: 2;
overflow: hidden;
height: 40px;
line-height: 20px;
@@ -300,16 +288,16 @@ const removeFavorite = async (id: string) => {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.cart-btn, .remove-btn {
width: 28px;
height: 28px;
border-radius: 50%;
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
margin-left: 8px; /* Replacement for gap */
}
.cart-btn {
@@ -328,4 +316,4 @@ const removeFavorite = async (id: string) => {
.remove-icon {
font-size: 14px;
}
</style>
</style>

View File

@@ -1,21 +1,14 @@
<!-- 足迹页面 -->
<template>
<view class="footprint-page">
<!-- 顶部栏 -->
<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">
<text class="action-btn" @click="toggleEditMode">{{ isEditMode ? '完成' : '编辑' }}</text>
<text class="action-btn" @click="clearAll">清空</text>
</view>
</view>
<!-- 日期分组 -->
<scroll-view class="footprint-content" scroll-y @scrolltolower="loadMore">
<!-- 空状态 -->
<view v-if="footprints.length === 0 && !isLoading" class="empty-footprints">
<text class="empty-icon">👣</text>
<text class="empty-text">暂无浏览记录</text>
@@ -23,20 +16,19 @@
<button class="go-shopping-btn" @click="goShopping">去逛逛</button>
</view>
<!-- 按日期分组 -->
<view v-for="(group, date) in groupedFootprints" :key="date" class="date-group">
<view v-for="(group, index) in groupedFootprints" :key="index" class="date-group">
<view class="group-header">
<text class="group-date">{{ formatGroupDate(date) }}</text>
<text v-if="isEditMode" class="group-select" @click="toggleGroupSelect(date)">
{{ isGroupSelected(date) ? '取消全选' : '全选' }}
<text class="group-date">{{ group.dateLabel }}</text>
<text v-if="isEditMode" class="group-select" @click="toggleGroupSelect(index)">
{{ isGroupSelected(index) ? '取消全选' : '全选' }}
</text>
</view>
<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 :class="['select-icon', { selected: item.selected }]">
<text v-if="item.selected" class="icon-text">✓</text>
<view :class="['select-icon', { selected: item.selected === true }]">
<text v-if="item.selected === true" class="icon-text">✓</text>
</view>
</view>
<view class="item-content" @click="viewProduct(item)">
@@ -46,13 +38,7 @@
<view class="product-bottom">
<view class="product-price-row">
<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 class="product-meta">
<text class="sales-text">已售{{ item.sales }}</text>
<text class="time-text">{{ formatTime(item.viewTime) }}</text>
</view> -->
</view>
</view>
</view>
@@ -60,7 +46,6 @@
</view>
</view>
<!-- 加载更多 -->
<view v-if="isLoading" class="loading-more">
<text class="loading-text">加载中...</text>
</view>
@@ -69,7 +54,6 @@
</view>
</scroll-view>
<!-- 编辑操作栏 -->
<view v-if="isEditMode && footprints.length > 0" class="edit-bar">
<view class="select-all" @click="toggleSelectAll">
<view :class="['all-select-icon', { selected: isAllSelected }]">
@@ -92,151 +76,46 @@ type FootprintType = {
id: string
name: string
price: number
original_price?: number
original_price: number
image: string
sales: number
shopId: string
shopName: string
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 isLoading = ref<boolean>(false)
const hasMore = ref<boolean>(false)
// 计算属性
const selectedCount = computed(() => {
return footprints.value.filter(item => item.selected).length
const selectedCount = computed((): number => {
return footprints.value.filter((item): Boolean => item.selected === true).length
})
const isAllSelected = computed(() => {
return footprints.value.length > 0 && footprints.value.every(item => item.selected)
const isAllSelected = computed((): boolean => {
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 date = new Date(dateStr)
const today = new Date()
@@ -254,24 +133,43 @@ const formatGroupDate = (dateStr: string): string => {
}
}
// 格式化时间
const formatTime = (timestamp: number): string => {
const date = new Date(timestamp)
const hours = date.getHours().toString().padStart(2, '0')
const minutes = date.getMinutes().toString().padStart(2, '0')
return `${hours}:${minutes}`
}
const groupedFootprints = computed((): FootprintGroup[] => {
const result: FootprintGroup[] = []
for (let i = 0; i < footprints.value.length; i++) {
const item = footprints.value[i]
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 = () => {
isEditMode.value = !isEditMode.value
// 重置选择状态
footprints.value.forEach(item => {
item.selected = false
})
for (let i = 0; i < footprints.value.length; i++) {
footprints.value[i].selected = false
}
}
// 清空所有足迹
const clearAll = () => {
if (footprints.value.length === 0) return
@@ -292,46 +190,41 @@ const clearAll = () => {
})
}
// 切换选择状态
const toggleSelect = (item: FootprintType) => {
item.selected = !item.selected
item.selected = !(item.selected === true)
footprints.value = [...footprints.value]
}
// 切换分组全选
const toggleGroupSelect = (dateStr: string) => {
const group = groupedFootprints.value[dateStr]
if (!group) return
const toggleGroupSelect = (groupIndex: number) => {
const group = groupedFootprints.value[groupIndex]
if (group == null) return
const isAllSelected = group.every(item => item.selected)
const newSelectedState = !isAllSelected
const allSelected = group.items.every((item): Boolean => item.selected === true)
const newSelectedState = !allSelected
group.forEach(item => {
item.selected = newSelectedState
})
for (let i = 0; i < group.items.length; i++) {
group.items[i].selected = newSelectedState
}
footprints.value = [...footprints.value]
}
// 检查组是否全选
const isGroupSelected = (dateStr: string): boolean => {
const group = groupedFootprints.value[dateStr]
if (!group || group.length === 0) return false
return group.every(item => item.selected)
const isGroupSelected = (groupIndex: number): boolean => {
const group = groupedFootprints.value[groupIndex]
if (group == null || group.items.length === 0) return false
return group.items.every((item): Boolean => item.selected === true)
}
// 全选/取消全选
const toggleSelectAll = () => {
const newSelectedState = !isAllSelected.value
footprints.value.forEach(item => {
item.selected = newSelectedState
})
for (let i = 0; i < footprints.value.length; i++) {
footprints.value[i].selected = newSelectedState
}
footprints.value = [...footprints.value]
}
// 删除选中项
const deleteSelected = () => {
const selectedItems = footprints.value.filter(item => item.selected)
const selectedItems = footprints.value.filter((item): Boolean => item.selected === true)
if (selectedItems.length === 0) {
uni.showToast({
title: '请选择要删除的记录',
@@ -343,25 +236,29 @@ const deleteSelected = () => {
uni.showModal({
title: '确认删除',
content: `确定要删除选中的${selectedItems.length}条记录吗?`,
success: async (res) => {
success: (res) => {
if (res.confirm) {
uni.showLoading({ title: '删除中' })
// 远程删除
for (const item of selectedItems) {
await supabaseService.deleteFootprint(item.id)
}
uni.hideLoading()
// 从列表移除
footprints.value = footprints.value.filter(item => !item.selected)
footprints.value = footprints.value.filter((item): Boolean => item.selected !== true)
// 保存回本地存储
const dataToSave = footprints.value.map(item => {
const { selected, ...rest } = item
return rest
})
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))
uni.showToast({
@@ -369,7 +266,6 @@ const deleteSelected = () => {
icon: 'success'
})
// 如果删完了,退出编辑模式
if (footprints.value.length === 0) {
isEditMode.value = false
}
@@ -378,34 +274,124 @@ const deleteSelected = () => {
})
}
// 查看商品
const viewProduct = (item: FootprintType) => {
if (isEditMode.value) return
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 goShopping = () => {
uni.switchTab({
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>
<style scoped>
.footprint-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
flex: 1;
}
.footprint-header {
@@ -417,32 +403,20 @@ const goShopping = () => {
justify-content: space-between;
}
.header-title {
/* margin-left: 15px; */ /* Removed */
display: none;
}
.title-text {
font-size: 18px;
font-weight: bold;
color: #333333;
}
.header-actions {
display: flex;
flex-direction: row;
gap: 20px;
flex: 1;
justify-content: flex-end; /* Align Edit/Clear to the right */
align-items: center;
padding-right: 0;
flex-direction: row;
flex: 1;
justify-content: flex-end;
align-items: center;
padding-right: 0;
}
.action-btn {
color: #007aff;
font-size: 14px;
padding: 5px;
margin-left: 20px;
margin-left: 20px;
}
.footprint-content {
@@ -492,7 +466,6 @@ const goShopping = () => {
.group-header {
padding: 15px 5px;
/* border-bottom: 1px solid #f5f5f5; */
display: flex;
align-items: center;
justify-content: space-between;
@@ -514,28 +487,19 @@ const goShopping = () => {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 10px;
justify-content: space-between;
}
.footprint-item {
display: flex;
flex-direction: column; /* 垂直排列内容 */
/* padding: 15px 0; */ /* Grid layout uses gap */
flex-direction: column;
margin-bottom: 10px;
border-bottom: none;
width: calc(50% - 5px); /* Mobile: 2 items per row */
width: 48%;
background-color: #fff;
/* border-radius: 8px; */ /* Optional card style */
/* overflow: hidden; */
position: relative; /* For absolute positioning of selector */
position: relative;
}
/*
.footprint-item:last-child {
border-bottom: none;
}
*/
.item-selector {
position: absolute;
top: 5px;
@@ -543,7 +507,6 @@ const goShopping = () => {
z-index: 10;
width: 30px;
height: 30px;
/* background-color: rgba(255,255,255,0.8); */
border-radius: 15px;
display: flex;
align-items: center;
@@ -574,12 +537,12 @@ const goShopping = () => {
.item-content {
flex: 1;
display: flex;
flex-direction: column; /* 垂直堆叠 */
flex-direction: column;
}
.product-image {
width: 100%;
height: 170px; /* Adjust height based on aspect ratio preference */
height: 170px;
border-radius: 5px;
margin-right: 0;
margin-bottom: 8px;
@@ -599,11 +562,10 @@ const goShopping = () => {
color: #333333;
line-height: 1.4;
margin-bottom: 6px;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
height: 40px;
text-overflow: ellipsis;
lines: 2;
height: 40px;
}
.product-bottom {
@@ -615,8 +577,7 @@ const goShopping = () => {
.product-price-row {
display: flex;
align-items: baseline;
margin-bottom: 0;
align-items: flex-end;
}
.current-price {
@@ -626,44 +587,21 @@ const goShopping = () => {
margin-right: 0;
}
/* PC/Tablet Responsive */
@media (min-width: 768px) {
.footprint-item {
width: calc(33.33% - 7px) !important; /* Tablet: 3 items */
}
.footprint-item {
width: 32% !important;
}
}
@media (min-width: 1024px) {
.footprint-item {
width: calc(16.66% - 9px) !important; /* PC: 6 items */
}
.footprint-content, .footprint-header {
max-width: 1200px;
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;
.footprint-item {
width: 16% !important;
}
.footprint-content, .footprint-header {
max-width: 1200px;
margin: 0 auto;
}
}
.loading-more,
@@ -725,4 +663,4 @@ const goShopping = () => {
font-size: 14px;
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: '添加中...' })
try {
// 尝试调用 Supabase 服务添加
const success = await supabaseService.addToCart(product.id, 1)
const success = await supabaseService.addToCart(product.id, 1, '')
if (success) {
uni.showToast({
title: '已添加到购物车',
@@ -990,7 +990,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
.card-name {
font-size: 15px;
font-weight: 600;
font-weight: 700;
color: #333;
margin-bottom: 4px;
text-align: center;
@@ -1046,7 +1046,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
.news-image {
width: 100%;
height: 100%;
display: block;
/* display: block; REMOVED for uniapp-x support */
}
.news-overlay {
@@ -1071,8 +1071,8 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
.news-caption {
font-size: 16px;
font-weight: 600;
display: block;
font-weight: 700;
/* display: block; REMOVED for uniapp-x support */
line-height: 1.4;
}
@@ -1086,9 +1086,11 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
}
.services-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 20px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
/* grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); REMOVED */
/* gap: 20px; REMOVED */
}
.service-card {
@@ -1098,7 +1100,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
padding: 20px;
background: #f8f9fa;
border-radius: 12px;
cursor: pointer;
/* cursor: pointer; REMOVED for uniapp-x support */
transition: all 0.3s ease;
}
@@ -1124,7 +1126,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
.service-name {
font-size: 15px;
font-weight: 600;
font-weight: 700;
color: #333;
margin-bottom: 4px;
}
@@ -1154,13 +1156,14 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
.title-section {
display: flex;
align-items: center;
gap: 8px;
/* gap: 8px; REMOVED */
width: 100%;
}
.section-icon {
font-size: 20px;
color: #4CAF50;
margin-right: 8px;
}
.sort-tabs {
@@ -1180,9 +1183,9 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
padding: 8px 12px; /* 增加左右内边距 */
border-radius: 20px;
border: 1px solid #e0e0e0;
cursor: pointer;
/* cursor: pointer; REMOVED */
transition: all 0.2s ease;
white-space: nowrap;
/* white-space: nowrap; REMOVED for uniapp-x support on view */
flex: 1; /* 均分宽度 */
min-width: 70px; /* 设置最小宽度防止过窄 */
text-align: center;
@@ -1207,13 +1210,18 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
/* 产品网格 */
.products-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
/* grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); REMOVED */
/* gap: 20px; REMOVED */
margin-top: 20px;
margin: 20px -1.5% 0 -1.5%;
}
.product-card {
width: 47%;
margin: 0 1.5% 20px 1.5%;
background: #f8f9fa;
border-radius: 12px;
overflow: hidden;
@@ -1237,7 +1245,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
font-size: 11px;
padding: 4px 12px;
border-radius: 12px;
font-weight: 600;
font-weight: 700;
z-index: 2;
}
@@ -1254,10 +1262,10 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
.product-name {
font-size: 15px;
font-weight: 600;
font-weight: 700;
color: #333;
margin-bottom: 4px;
display: block;
/* display: block; REMOVED */
line-height: 1.4;
}
@@ -1265,7 +1273,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
font-size: 13px;
color: #666;
margin-bottom: 12px;
display: block;
/* display: block; REMOVED */
}
.price-section {
@@ -1361,9 +1369,11 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
}
.family-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 16px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
/* grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); REMOVED */
/* gap: 16px; REMOVED */
margin-top: 20px;
}
@@ -1400,7 +1410,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
.family-name {
font-size: 14px;
font-weight: 600;
font-weight: 700;
color: #333;
margin-bottom: 4px;
}
@@ -1436,9 +1446,9 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
padding: 8px 12px; /* 增加左右内边距 */
border-radius: 20px;
border: 1px solid #e0e0e0;
cursor: pointer;
/* cursor: pointer; REMOVED */
transition: all 0.2s ease;
white-space: nowrap;
/* white-space: nowrap; REMOVED */
flex: 1; /* 均分宽度 */
min-width: 80px; /* 设置最小宽度防止过窄 */
text-align: center;
@@ -1462,13 +1472,18 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
}
.recommend-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
/* grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); REMOVED */
/* gap: 20px; REMOVED */
margin-top: 20px;
margin: 20px -1.5% 0 -1.5%;
}
.recommend-product {
width: 97%; /* Single col by default or adjust */
margin: 0 1.5% 20px 1.5%;
background: #f8f9fa;
border-radius: 12px;
overflow: hidden;
@@ -1506,7 +1521,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
padding: 4px 10px;
border-radius: 10px;
font-size: 11px;
font-weight: 600;
font-weight: 700;
color: white;
}
@@ -1524,7 +1539,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
.product-title {
font-size: 16px;
font-weight: 600;
font-weight: 700;
color: #333;
margin-bottom: 4px;
display: block;
@@ -1558,7 +1573,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
}
.rating-value {
font-weight: 600;
font-weight: 700;
color: #333;
}
@@ -1622,7 +1637,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
.reminder-title {
font-size: 15px;
font-weight: 600;
font-weight: 700;
}
.reminder-desc {
@@ -1661,15 +1676,17 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
height: 32px;
border: 3px solid #f0f0f0;
border-top-color: #4CAF50;
border-radius: 50%;
animation: spin 1s linear infinite;
border-radius: 16px;
/* animation: spin 1s linear infinite; REMOVED for uniapp-x support */
margin-bottom: 12px;
}
/*
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
}
*/
.loading-text {
font-size: 14px;
@@ -1705,7 +1722,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
.search-box {
padding: 8px 16px; /* 与分类页保持一致 */
max-width: 100%;
/* max-width: 100%; REMOVED for uniapp-x support */
margin: 0;
}
@@ -1783,9 +1800,16 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
.products-grid,
.recommend-grid {
grid-template-columns: repeat(2, 1fr); /* 手机端调整为双列显示 */
gap: 8px; /* 减小间距 */
/* grid-template-columns: repeat(2, 1fr); REMOVED */
/* 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-details {
@@ -1858,7 +1882,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
.hot-products .price-value,
.smart-recommend .price-value {
font-size: 14px; /* 字体变小 */
font-weight: 600;
font-weight: 700;
}
.family-grid {

View File

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

View File

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

View File

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

View File

@@ -14,7 +14,7 @@
</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">
<text class="section-title">配送信息</text>
</view>
@@ -22,17 +22,17 @@
<view class="address-icon">📍</view>
<view class="address-content">
<view class="address-user">
<text class="recipient">{{ deliveryAddress.name }}</text>
<text class="phone">{{ deliveryAddress.phone }}</text>
<text class="recipient">{{ deliveryAddress?.name ?? '' }}</text>
<text class="phone">{{ deliveryAddress?.phone ?? '' }}</text>
</view>
<text class="address-detail">{{ getFullAddress(deliveryAddress) }}</text>
<text class="address-detail">{{ getFullAddress(deliveryAddress as any) }}</text>
</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-value">{{ deliveryInfo.tracking_no }}</text>
<text class="copy-btn" @click="copyText(deliveryInfo.tracking_no)">复制</text>
<text class="courier-value">{{ deliveryInfo?.tracking_no ?? '' }}</text>
<text class="copy-btn" @click="copyText(deliveryInfo?.tracking_no ?? '')">复制</text>
</view>
</view>
@@ -44,7 +44,7 @@
<text class="arrow-right">></text>
</view>
<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">
<text class="product-name">{{ item.product_name }}</text>
<text v-if="item.specifications" class="product-spec">{{ getSpecText(item.specifications) }}</text>
@@ -57,56 +57,56 @@
</view>
<!-- 订单信息 -->
<view class="order-info card">
<view class="order-info card" v-if="order != null">
<view class="info-row">
<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 class="info-row">
<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 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-value">{{ getPaymentMethodText(order.payment_method) }}</text>
<text class="info-value">{{ getPaymentMethodText(order?.payment_method as any) }}</text>
</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-value">{{ formatTime(order.paid_at) }}</text>
<text class="info-value">{{ formatTime(order?.paid_at ?? '') }}</text>
</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-value">{{ formatTime(order.shipped_at) }}</text>
<text class="info-value">{{ formatTime(order?.shipped_at ?? '') }}</text>
</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-value">{{ formatTime(order.completed_at) }}</text>
<text class="info-value">{{ formatTime(order?.completed_at ?? '') }}</text>
</view>
</view>
<!-- 费用明细 -->
<view class="cost-detail card">
<view class="cost-detail card" v-if="order != null">
<view class="cost-row">
<text class="cost-label">商品总额</text>
<text class="cost-value">¥{{ order.product_amount }}</text>
<text class="cost-value">¥{{ order?.product_amount ?? 0 }}</text>
</view>
<view class="cost-row">
<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 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-value">-¥{{ order.discount_amount }}</text>
<text class="cost-value">-¥{{ order?.discount_amount ?? 0 }}</text>
</view>
<view class="cost-row total">
<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>
</scroll-view>
<!-- 底部操作 -->
<view class="bottom-actions">
<view class="bottom-actions" v-if="order != null">
<view class="action-left">
<view class="service-btn" @click="contactService">
<text class="service-icon">🎧</text>
@@ -114,20 +114,20 @@
</view>
</view>
<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" @click="cancelOrder">取消订单</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 === 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="remindDelivery">提醒发货</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" @click="viewLogistics">查看物流</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 === 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="applyAfterSales">申请售后</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="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>
@@ -139,60 +139,57 @@ import { onLoad } from '@dcloudio/uni-app'
import { supabaseService } from '@/utils/supabaseService.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 order = ref<any>({})
const orderItems = ref<any[]>([])
const order = ref<OrderType | null>(null)
const orderItems = ref<OrderItemType[]>([])
const shopName = ref('店铺名称')
const deliveryAddress = ref<any>({})
const deliveryInfo = ref<any>({})
const deliveryAddress = ref<AddressType | null>(null)
const deliveryInfo = ref<DeliveryInfoType | null>(null)
onLoad((options) => {
if (options['id']) {
orderId.value = options['id'] as string
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
// 辅助函数 - 必须在调用前定义
const getStatusText = (): string => {
const status = order.value?.order_status ?? 0
if (status == 1) return '待付款'
if (status == 2) return '待发货'
if (status == 3) return '待收货'
@@ -203,8 +200,8 @@ const getStatusText = () => {
return '未知状态'
}
const getStatusDesc = () => {
const status = order.value.order_status
const getStatusDesc = (): string => {
const status = order.value?.order_status ?? 0
if (status == 1) return '请尽快完成支付'
if (status == 2) return '商家正在打包商品'
if (status == 3) return '商品正在赶往您的地址'
@@ -215,8 +212,8 @@ const getStatusDesc = () => {
return ''
}
const getStatusIcon = () => {
const status = order.value.order_status
const getStatusIcon = (): string => {
const status = order.value?.order_status ?? 0
if (status === 1) return '💳'
if (status === 2) return '📦'
if (status === 3) return '🚚'
@@ -224,43 +221,85 @@ const getStatusIcon = () => {
return '📝'
}
const getStatusClass = () => {
const status = order.value.order_status
const getStatusClass = (): string => {
const status = order.value?.order_status ?? 0
return `status-${status}`
}
const getFullAddress = (addr: any) => {
if (!addr) return ''
const getFullAddress = (addr: any): string => {
if (addr == null) return ''
// 兼容简单的字符串地址和对象地址
if (typeof addr === 'string') return addr
if (addr.address) return addr.address
return (addr.province || '') + (addr.city || '') + (addr.district || '') + (addr.detail || addr.address_detail || '')
const addrObj = addr as Record<string, any>
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) => {
if (!specs) return ''
const getSpecText = (specs: any): string => {
if (specs == null) return ''
if (typeof specs === 'string') return specs
return Object.keys(specs).map(k => `${k}:${specs[k]}`).join(' ')
// 简化处理:直接返回字符串形式
return JSON.stringify(specs)
}
const formatTime = (iso: string) => {
if (!iso) return ''
const formatTime = (iso: string): string => {
if (iso == '') return ''
const d = new Date(iso)
return `${d.getFullYear()}-${d.getMonth()+1}-${d.getDate()} ${d.getHours()}:${d.getMinutes()}`
}
const getPaymentMethodText = (method: any) => {
const getPaymentMethodText = (method: any): string => {
return '在线支付'
}
const copyText = (text: string) => {
if(!text) return
if(text == '') return
uni.setClipboardData({
data: text,
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 = () => {
uni.showActionSheet({
@@ -278,22 +317,31 @@ const contactService = () => {
}
const payOrder = () => {
const totalAmount = order.value?.total_amount ?? 0
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 = () => {
uni.showModal({
title: '提示',
content: '确定要取消订单吗?',
success: async (res) => {
success: (res) => {
if (res.confirm) {
const { error } = await supa.from('ml_orders').update({ order_status: 5 }).eq('id', orderId.value)
if(!error) {
order.value.order_status = 5
uni.showToast({ title: '订单已取消' })
}
doCancelOrder()
}
}
})
@@ -307,32 +355,39 @@ const viewLogistics = () => {
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({
title: '确认收货',
content: '确保您已收到货物',
success: async (res) => {
success: (res) => {
if (res.confirm) {
const result = await supabaseService.confirmReceipt(orderId.value)
if (result.success) {
order.value.order_status = 4
uni.showToast({ title: '收货成功' })
setTimeout(() => goToReview(), 1500)
} else {
uni.showToast({ title: result.error || '失败', icon: 'none' })
}
doConfirmReceive()
}
}
})
}
const goToReview = () => {
uni.navigateTo({ url: `/pages/mall/consumer/review?orderId=${orderId.value}` })
}
const rePurchase = async () => {
uni.showLoading({ title: '处理中' })
const success = await supabaseService.rePurchase(order.value)
const orderData = order.value as any
const success = await supabaseService.rePurchase(orderData)
uni.hideLoading()
if (success) {
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 = () => {
uni.showModal({
title: '申请退款',
editable: true,
placeholderText: '请输入退款原因',
success: async (res) => {
success: (res) => {
if (res.confirm) {
const reason = res.content || '用户主动申请'
const success = await supabaseService.applyRefund(orderId.value, reason)
if (success) {
order.value.order_status = 6
uni.showToast({ title: '申请已提交' })
} else {
uni.showToast({ title: '提交失败', icon: 'none' })
}
const reason = res.content ?? '用户主动申请'
doApplyRefund(reason)
}
}
})
@@ -379,19 +440,31 @@ const goToProduct = (pid: string) => {
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>
<style scoped>
.order-detail-page {
display: flex;
flex-direction: column;
height: 100vh;
flex: 1;
background-color: #f5f5f5;
}
.scroll-content {
flex: 1;
overflow-y: auto;
padding-bottom: 20px;
}
@@ -419,14 +492,12 @@ const goToProduct = (pid: string) => {
.status-text {
font-size: 18px;
font-weight: bold;
display: block;
}
.status-desc {
font-size: 12px;
opacity: 0.9;
margin-top: 5px;
display: block;
}
/* 配送信息 */
@@ -434,7 +505,6 @@ const goToProduct = (pid: string) => {
font-weight: bold;
font-size: 16px;
margin-bottom: 10px;
display: block;
}
.delivery-address {
@@ -535,10 +605,9 @@ const goToProduct = (pid: string) => {
font-size: 14px;
line-height: 1.4;
color: #333;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
lines: 2; /* uvue specific */
}
.product-spec {
@@ -618,7 +687,7 @@ const goToProduct = (pid: string) => {
justify-content: space-between;
align-items: center;
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 {
@@ -631,7 +700,7 @@ const goToProduct = (pid: string) => {
align-items: center;
font-size: 10px;
color: #666;
background: none;
background-color: transparent;
line-height: 1.2;
}
@@ -642,7 +711,11 @@ const goToProduct = (pid: string) => {
.action-right {
display: flex;
gap: 10px;
}
/* Add margin to buttons inside action-right for spacing */
.action-right .btn {
margin-left: 10px;
}
.btn {
@@ -665,4 +738,4 @@ const goToProduct = (pid: string) => {
/* 状态样式 */
.status-4 .status-text { /* Completed */ }
</style>
</style>

View File

@@ -29,6 +29,7 @@
>
<text class="tab-name">{{ tab.name }}</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>
</scroll-view>
@@ -179,9 +180,38 @@ onBackPress((options) => {
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 allOrdersList = ref<any[]>([]) // Store all fetched orders for client-side filtering
const orders = ref<OrderItem[]>([])
const allOrdersList = ref<OrderItem[]>([]) // Store all fetched orders for client-side filtering
const loading = ref<boolean>(false)
const loadingMore = ref<boolean>(false)
const hasMore = ref<boolean>(true)
@@ -190,8 +220,8 @@ const page = ref<number>(1)
const activeTab = ref<string>('all')
const searchKeyword = ref<string>('')
// 订单标签页
const orderTabs = reactive([
// 订单标签页 - 使用 ref 以便整体替换
const orderTabs = ref<OrderTabItem[]>([
{ id: 'all', name: '全部', count: 0 },
{ id: 'pending', name: '待付款', count: 0 },
{ id: 'shipping', name: '待发货', count: 0 },
@@ -203,44 +233,82 @@ const orderTabs = reactive([
// 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') {
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 () => {
@@ -251,32 +319,67 @@ const loadOrders = async () => {
const fetchedOrders = await supabaseService.getOrders(0)
// Map to View Model
const mappedOrders = fetchedOrders.map((order: any) => ({
id: order.id,
order_no: order.order_no,
status: order.order_status,
create_time: order.created_at,
product_amount: order.product_amount || order.actual_amount,
shipping_fee: order.delivery_fee,
total_amount: order.actual_amount,
products: (order.ml_order_items || []).map((item: any) => ({
id: item.product_id,
name: item.product_name,
price: item.price,
image: item.image_url || '/static/default-product.png',
spec: item.specifications ? (typeof item.specifications === 'string' ? item.specifications : Object.values(item.specifications).join(' ')) : '',
quantity: item.quantity
}))
}))
const mappedOrders: any[] = []
for (let i = 0; i < fetchedOrders.length; i++) {
const order = fetchedOrders[i]
const orderObj = order as Record<string, any>
const items = orderObj['ml_order_items'] as any[]
const productsList: any[] = []
if (items != null) {
for (let j = 0; j < items.length; j++) {
const item = items[j]
const itemObj = item as Record<string, any>
const specRaw = itemObj['specifications']
const specText = specRaw != null ? parseSpecText(specRaw) : ''
productsList.push({
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
mappedOrders.sort((a: any, b: any) => {
const timeA = new Date(a.create_time).getTime()
const timeB = new Date(b.create_time).getTime()
const aObj = a as Record<string, any>
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
})
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
updateTabsCounts(mappedOrders)
@@ -292,41 +395,77 @@ const loadOrders = async () => {
}
}
const updateTabsCounts = (allOrders: any[]) => {
orderTabs[0].count = allOrders.length
orderTabs[1].count = allOrders.filter((o: any) => o.status === 1).length
orderTabs[2].count = allOrders.filter((o: any) => o.status === 2).length
orderTabs[3].count = allOrders.filter((o: any) => o.status === 3).length
orderTabs[4].count = allOrders.filter((o: any) => o.status === 4).length
orderTabs[5].count = allOrders.filter((o: any) => o.status === 5).length
}
// 生命周期
onLoad((options) => {
if (options['status'] != null) {
const status = options['status'] as string
if (['all', 'pending', 'shipping', 'delivering', 'completed', 'cancelled'].includes(status)) {
activeTab.value = status
}
}
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 = () => {
const statusMap: Record<string, number> = {
'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)
}
}
onShow(() => {
loadOrders()
})
const formatDate = (isoString: string): string => {
if (!isoString) return ''
if (isoString == '') return ''
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')}`
}
// 辅助函数:获取当前订单数据(必须在 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) => {
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()
}
@@ -339,44 +478,13 @@ const clearSearch = () => {
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 => {
if (!specs) return ''
if (specs == null) return ''
if (typeof specs === 'string') return specs
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 statusMap: Record<number, string> = {
1: '待付款',
2: '待货',
3: '待收货',
4: '已完成',
5: '已取消'
}
return statusMap[status] || '未知状态'
if (status == 1) return '待付款'
if (status == 2) return '待发货'
if (status == 3) return '待货'
if (status == 4) return '已完成'
if (status == 5) return '已取消'
return '未知状态'
}
// 获取状态类名
const getStatusClass = (status: number): string => {
const classMap: Record<number, string> = {
1: 'status-pending',
2: 'status-shipping',
3: 'status-delivering',
4: 'status-completed',
5: 'status-cancelled'
}
return classMap[status] || 'status-unknown'
if (status == 1) return 'status-pending'
if (status == 2) return 'status-shipping'
if (status == 3) return 'status-delivering'
if (status == 4) return 'status-completed'
if (status == 5) return 'status-cancelled'
return '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) {
orders.value[index].status = 5
const orderObj = orders.value[index] as Record<string, any>
orderObj['status'] = 5
orders.value = [...orders.value]
}
}
@@ -473,59 +581,78 @@ const viewLogistics = (orderId: string) => {
})
}
const confirmReceipt = async (orderId: string) => {
uni.showModal({
title: '确认收货',
content: '请确认您已收到商品,且商品无误',
success: async (res) => {
if (res.confirm) {
uni.showLoading({ title: '处理中...' })
try {
const result = await supabaseService.confirmReceipt(orderId)
uni.hideLoading()
if (result.success) {
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'
})
}
}
}
// goReview 必须在 doConfirmReceipt 之前定义,因为 doConfirmReceipt 会调用它
const goReview = (order: any) => {
const orderObj = order as Record<string, any>
const products = orderObj['products'] as any[]
const productIds = products.map((p: any) => {
const pObj = p as Record<string, any>
const pid = pObj['id']
return pid != null ? pid as string : ''
}).join(',')
const orderId = orderObj['id'] as string
uni.navigateTo({
url: `/pages/mall/consumer/review?orderId=${orderId}&productIds=${productIds}`
})
}
const goReview = (order: any) => {
const productIds = order.products.map((p: any) => p.id).join(',')
uni.navigateTo({
url: `/pages/mall/consumer/review?orderId=${order.id}&productIds=${productIds}`
const doConfirmReceipt = async (orderId: string) => {
uni.showLoading({ title: '处理中...' })
try {
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 orderObj = order as Record<string, any>
const orderId = orderObj['id']
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) => {
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 = () => {
@@ -574,7 +705,9 @@ const goShopping = () => {
<style>
.orders-page {
width: 100%;
min-height: 100vh;
flex: 1;
display: flex;
flex-direction: column;
background-color: #f5f5f5;
}
@@ -586,8 +719,8 @@ const goShopping = () => {
align-items: center;
justify-content: center;
border-bottom: 1px solid #eee;
position: sticky;
top: 0;
/* position: sticky; removed */
/* top: 0; removed */
z-index: 10;
}
@@ -614,9 +747,7 @@ const goShopping = () => {
color: #999;
font-size: 12px;
}
.search-input:focus {
outline: none;
border-color: #ff5000;
background-color: white;
}
@@ -637,17 +768,17 @@ const goShopping = () => {
height: 20px;
line-height: 18px;
text-align: center;
border-radius: 50%;
border-radius: 10px; /* fixed 50% */
background-color: #ddd;
cursor: pointer;
/* cursor: pointer; removed */
}
/* 标签页 */
.order-tabs {
background-color: #ffffff;
border-bottom: 1px solid #e5e5e5;
position: sticky;
top: 50px;
/* position: sticky; removed */
/* top: 50px; removed */
z-index: 10;
}
@@ -660,9 +791,8 @@ const goShopping = () => {
display: flex;
flex-direction: row;
padding: 0 10px;
/* 关键:确保宽度包含所有子元素,允许滚动 */
width: max-content;
min-width: 100%;
/* width: max-content; removed */
/* min-width: 100%; removed */
}
.tab-item {
@@ -683,7 +813,17 @@ const goShopping = () => {
}
.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;
bottom: 0;
left: 0;
@@ -709,7 +849,7 @@ const goShopping = () => {
/* 内容区 */
.orders-content {
height: calc(100vh - 100px);
flex: 1;
}
/* 空状态 */
@@ -830,7 +970,6 @@ const goShopping = () => {
font-size: 15px;
color: #333;
margin-bottom: 5px;
display: block;
line-height: 1.4;
}
@@ -838,7 +977,6 @@ const goShopping = () => {
font-size: 13px;
color: #999;
margin-bottom: 10px;
display: block;
}
.product-footer {
@@ -906,7 +1044,7 @@ const goShopping = () => {
.action-buttons {
display: flex;
justify-content: flex-end;
gap: 10px;
/* gap: 10px; removed */
}
.action-btn {
@@ -914,7 +1052,8 @@ const goShopping = () => {
border-radius: 15px;
font-size: 13px;
border: 1px solid;
background: none;
background-color: transparent; /* fixed background: none */
margin-left: 10px; /* alternative to gap */
}
.action-btn.cancel {
@@ -966,16 +1105,10 @@ const goShopping = () => {
height: 24px;
border: 2px solid #f0f5ff;
border-top-color: #ff5000;
border-radius: 50%;
animation: spin 1s linear infinite;
border-radius: 12px;
margin-bottom: 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.no-more {
text-align: center;
color: #999;

View File

@@ -22,61 +22,71 @@
<script setup lang="uts">
import { ref, onMounted } from 'vue'
// import supa from '@/components/supadb/aksupainstance.uts' // 暂时注释掉数据库连接
import { supabaseService } from '@/utils/supabaseService.uts'
const orderId = ref('')
const orderNo = ref('')
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(() => {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const options = currentPage.options as any
const options = currentPage.options as Record<string, any>
if (options.orderId) {
orderId.value = options.orderId
orderNo.value = options.orderId // 使用订单ID作为订单号
console.log('[payment-success] options:', JSON.stringify(options))
const orderIdValue = options['orderId']
if (orderIdValue != null) {
orderId.value = orderIdValue as string
orderNo.value = orderIdValue as string
// 优先使用传递的 amount
if (options.amount) {
amount.value = parseFloat(options.amount)
} else {
// 如果没有传 amount尝试从本地存储查找订单
try {
const ordersStr = uni.getStorageSync('orders')
if (ordersStr) {
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)
const amountValue = options['amount']
if (amountValue != null) {
const amountStr = amountValue.toString()
console.log('[payment-success] amountStr:', amountStr)
const parsed = parseFloat(amountStr)
console.log('[payment-success] parsed:', parsed)
if (isNaN(parsed) == false) {
amount.value = parsed
}
}
// 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 = () => {
uni.navigateTo({
url: '/pages/mall/consumer/orders'
@@ -96,7 +106,8 @@ const goHome = () => {
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
/* height: 100vh; */
flex: 1;
background-color: #ffffff;
padding: 0 30px;
}
@@ -162,7 +173,7 @@ const goHome = () => {
width: 100%;
display: flex;
flex-direction: column;
gap: 15px;
/* gap: 15px; */
}
.btn {
@@ -174,6 +185,7 @@ const goHome = () => {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 15px;
}
.primary-btn {

View File

@@ -66,7 +66,7 @@
<view v-for="(_, index) in 6"
:key="index"
class="password-dot">
<text v-if="password.length > index">●</text>
<text v-if="password.length > index" class="password-dot-text">●</text>
</view>
</view>
<text class="forgot-password" @click="forgotPassword">忘记密码?</text>
@@ -136,109 +136,9 @@ const productAmount = ref<number>(0) // 商品总价
const deliveryFee = ref<number>(0) // 运费
const discountAmount = ref<number>(0) // 优惠减免
// 生命周期
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)
}
}
// 加载支付方式
// 加载支付方式(必须在 onMounted 之前定义)
const loadPaymentMethods = () => {
paymentMethods.value = [
const methods: PaymentMethodType[] = [
{
id: 'wechat',
name: '微信支付',
@@ -268,9 +168,10 @@ const loadPaymentMethods = () => {
enabled: true
}
]
paymentMethods.value = methods
}
// 加载用户余额
// 加载用户余额(必须在 onMounted 之前定义)
const loadUserBalance = async () => {
try {
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
const getCurrentUserId = (): string => {
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 icons: Record<string, string> = {
wechat: '💳',
alipay: '💳',
balance: '💰',
bankcard: '💳'
if (methodId === 'wechat') {
return '💳'
} else if (methodId === 'alipay') {
return '💳'
} else if (methodId === 'balance') {
return '💰'
} else if (methodId === 'bankcard') {
return '💳'
}
return icons[methodId] || '💳'
return '💳'
}
// 选择支付方式
@@ -319,14 +414,16 @@ const getPayButtonText = (): string => {
return '余额不足'
}
const texts: Record<string, string> = {
wechat: '微信支付',
alipay: '支付宝支付',
balance: '余额支付',
bankcard: '银行卡支付'
if (selectedMethod.value === 'wechat') {
return '微信支付'
} else if (selectedMethod.value === 'alipay') {
return '支付宝支付'
} else if (selectedMethod.value === 'balance') {
return '余额支付'
} else if (selectedMethod.value === 'bankcard') {
return '银行卡支付'
}
return texts[selectedMethod.value] || '确认支付'
return '确认支付'
}
// 减少商品库存
@@ -363,29 +460,37 @@ const confirmPayment = async () => {
}
isPaying.value = true
uni.showLoading({ title: '支付中...' })
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)
console.log('[confirmPayment] 支付结果:', 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
}
// 更新订单状态
updateOrderInStorage(2) // 2: 待发货(已支付)
uni.hideLoading()
updateOrderInStorage(orderId.value, 2)
// 支付成功
uni.showToast({
title: '支付成功',
icon: 'success',
duration: 2000
})
// 发布订单更新事件让profile页面可以刷新数据
uni.$emit('orderUpdated', { orderId: orderId.value, status: 2 }) // 2: 待发货
uni.$emit('orderUpdated', { orderId: orderId.value, status: 2 })
// 跳转到支付成功页面
setTimeout(() => {
uni.redirectTo({
url: `/pages/mall/consumer/payment-success?orderId=${orderId.value}`
@@ -393,56 +498,42 @@ const confirmPayment = async () => {
}, 1500)
} catch (err) {
console.error('支付失败:', err)
console.error('[confirmPayment] 支付异常:', err)
uni.hideLoading()
uni.showToast({
title: '支付失败',
icon: 'none'
})
} finally {
isPaying.value = false
}
}
// 获取支付方式代码
const getPaymentMethodCode = (methodId: string): number => {
const codes: Record<string, number> = {
wechat: 1,
alipay: 2,
balance: 3,
bankcard: 4
if (methodId === 'wechat') {
return 1
} else if (methodId === 'alipay') {
return 2
} else if (methodId === 'balance') {
return 3
} else if (methodId === 'bankcard') {
return 4
}
return codes[methodId] || 0
return 0
}
// 输入密码
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()
}
})
// 验证密码
// 验证密码(必须在 watch 之前定义)
const verifyPassword = async () => {
// 这里应该验证支付密码,这里简单模拟
const userId = getCurrentUserId()
try {
// 模拟验证
await new Promise(resolve => setTimeout(resolve, 500))
await new Promise<void>((resolve: (value: void) => void) => {
setTimeout(() => {
resolve()
}, 500)
})
// 假设密码正确
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 = () => {
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(() => {
// 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>
<style scoped>
.payment-page {
display: flex;
flex-direction: column;
height: 100vh;
flex: 1;
background-color: #f5f5f5;
}
@@ -589,7 +619,7 @@ const cancelPayment = async () => {
.payment-content {
flex: 1;
overflow-y: auto;
/* overflow-y: auto; */
}
/* 价格明细部分 */
@@ -640,7 +670,7 @@ const cancelPayment = async () => {
}
.order-no {
display: block;
/* display: block; */
font-size: 12px;
color: #999999;
text-align: center;
@@ -653,7 +683,7 @@ const cancelPayment = async () => {
}
.section-title {
display: block;
/* display: block; */
font-size: 16px;
font-weight: bold;
color: #333333;
@@ -663,7 +693,7 @@ const cancelPayment = async () => {
.method-list {
display: flex;
flex-direction: column;
gap: 10px;
/* gap: 10px; */
}
.method-item {
@@ -673,6 +703,7 @@ const cancelPayment = async () => {
padding: 15px;
border: 1px solid #e5e5e5;
border-radius: 8px;
margin-bottom: 10px;
}
.method-item.selected {
@@ -765,7 +796,7 @@ const cancelPayment = async () => {
}
.password-title {
display: block;
/* display: block; */
font-size: 16px;
color: #333333;
margin-bottom: 30px;
@@ -774,7 +805,7 @@ const cancelPayment = async () => {
.password-input {
display: flex;
justify-content: center;
gap: 15px;
/* gap: 15px; */
margin-bottom: 20px;
}
@@ -786,9 +817,10 @@ const cancelPayment = async () => {
display: flex;
align-items: center;
justify-content: center;
margin: 0 7.5px;
}
.password-dot text {
.password-dot-text {
color: #ffffff;
font-size: 8px;
}
@@ -809,7 +841,7 @@ const cancelPayment = async () => {
.price-summary {
display: flex;
align-items: baseline;
align-items: flex-end;
}
.summary-label {
@@ -847,18 +879,22 @@ const cancelPayment = async () => {
}
.keyboard-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 1px;
display: flex;
flex-wrap: wrap;
/* grid-template-columns: repeat(3, 1fr); uvue unsupported */
/* grid-gap: 1px; uvue unsupported */
background-color: #e5e5e5;
}
.keyboard-key {
width: 33.33%;
background-color: #ffffff;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #f5f5f5; /* mimic grid gap */
box-sizing: border-box;
}
.key-text {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -57,17 +57,17 @@
<view class="list-item">
<text class="item-icon">🔔</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 class="list-item">
<text class="item-icon">🎁</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 class="list-item">
<text class="item-icon">⭐</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>
@@ -79,17 +79,17 @@
<view class="list-item">
<text class="item-icon">👁️</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 class="list-item">
<text class="item-icon">🔍</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 class="list-item">
<text class="item-icon">💬</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>
@@ -265,7 +265,7 @@ const statusBarHeight = ref(0)
// 生命周期
onMounted(() => {
const systemInfo = uni.getSystemInfoSync()
statusBarHeight.value = systemInfo.statusBarHeight || 0
statusBarHeight.value = systemInfo.statusBarHeight
loadUserInfo()
loadSettings()
})
@@ -565,9 +565,13 @@ const deleteAccount = () => {
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 {
// 标记用户状态为注销 (status=3)
await supa
@@ -661,7 +665,7 @@ const deleteAccount = () => {
}
/* 针对 switch 组件的特殊处理,如果需要显示开关,可能需要调整布局 */
.list-item switch {
.settings-switch {
transform: scale(0.7);
margin-top: 5px;
}
@@ -669,7 +673,7 @@ const deleteAccount = () => {
/* 屏幕宽度大于 480px (大屏手机/平板/PC) 时,启用更宽的网格布局或列表布局 */
@media screen and (min-width: 480px) {
.list-item {
width: calc(50% - 10px); /* 每行两个,留出间隙 */
width: 47%; /* width: calc(50% - 10px); REPLACED */
margin: 5px;
border: 1px solid #f0f0f0;
border-radius: 8px;
@@ -697,7 +701,7 @@ const deleteAccount = () => {
/* 增加针对手机横屏的媒体查询 */
@media screen and (orientation: landscape) and (max-height: 500px) {
.list-item {
width: calc(25% - 10px); /* 横屏也保持4个一行或者根据需要调整 */
width: 22%; /* width: calc(25% - 10px); REPLACED */
margin: 5px;
border: 1px solid #f0f0f0;
border-radius: 8px;
@@ -723,7 +727,7 @@ const deleteAccount = () => {
}
.list-item {
width: calc(33.33% - 10px); /* 每行三个 */
width: 31%; /* width: calc(33.33% - 10px); REPLACED */
flex-direction: row; /* PC端保持水平排列 */
justify-content: flex-start;
text-align: left;
@@ -743,7 +747,7 @@ const deleteAccount = () => {
.settings-page {
display: flex;
flex-direction: column;
height: 100vh;
height: 100%;
background-color: #f5f5f5;
}
@@ -779,7 +783,6 @@ const deleteAccount = () => {
}
.section-title {
display: block;
font-size: 16px;
font-weight: bold;
color: #333333;
@@ -853,6 +856,5 @@ const deleteAccount = () => {
.delete-account {
color: #999999;
font-size: 14px;
text-decoration: underline;
}
}/* text-decoration: underline; REMOVED */
</style>

View File

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

View File

@@ -6,13 +6,13 @@
<view class="shop-list" v-if="shops.length > 0">
<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">
<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">
<text class="rating">⭐ {{ shop.rating_avg || 5.0 }}</text>
<text class="sales">销量: {{ shop.total_sales || 0 }}</text>
<text class="rating shop-meta-text">⭐ {{ shop.rating_avg }}</text>
<text class="sales shop-meta-text">销量: {{ shop.total_sales }}</text>
</view>
</view>
<button class="unfollow-btn" @click.stop="unfollow(shop)">已关注</button>
@@ -72,8 +72,8 @@ const loadFollowedShops = async () => {
shop_name: shopData['shop_name'] as string,
shop_logo: shopData['shop_logo'] as string | null,
description: shopData['description'] as string | null,
rating_avg: (shopData['rating_avg'] || 5.0) as number,
total_sales: (shopData['total_sales'] || 0) as number
rating_avg: (shopData['rating_avg'] != null) ? (shopData['rating_avg'] as number) : 5.0,
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?
// 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.
const targetId = shop.merchant_id != '' ? shop.merchant_id : shop.id
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 {
padding: 15px;
background-color: #f5f5f5;
min-height: 100vh;
flex: 1;
}
.header {
margin-bottom: 15px;
@@ -137,7 +138,6 @@ const goHome = () => {
.shop-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.shop-item {
background-color: #fff;
@@ -146,6 +146,10 @@ const goHome = () => {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 10px;
}
.shop-item:last-child {
margin-bottom: 0;
}
.shop-logo {
width: 50px;
@@ -178,7 +182,9 @@ const goHome = () => {
color: #999;
margin-top: 4px;
display: flex;
gap: 8px;
}
.shop-meta-text {
margin-right: 8px;
}
.unfollow-btn {
font-size: 12px;

View File

@@ -11,8 +11,8 @@
<view v-else class="list">
<view class="card" v-for="s in items" :key="s['id']">
<view class="row between">
<text class="name">{{ s['plan']?.['name'] || '订阅' }}</text>
<text class="status" :class="'st-' + (s['status'] || 'active')">{{ statusText(s['status'] as string) }}</text>
<text class="name">{{ s['plan']?.['name'] != null ? s['plan']?.['name'] : '订阅' }}</text>
<text class="status" :class="'st-' + (s['status'] != null ? s['status'] : 'active')">{{ statusText(s['status'] as string) }}</text>
</view>
<view class="row">
<text class="label">周期</text>
@@ -33,7 +33,7 @@
<view class="actions">
<label class="toggle">
<switch :checked="!!s['auto_renew']" @change="e => toggleAutoRenew(s, e.detail.value as boolean)" />
<text>自动续费</text>
<text class="toggle-text">自动续费</text>
</label>
<button class="danger" @click="cancelAtPeriodEnd(s)" :disabled="(s['status'] as string) !== 'active'">到期取消</button>
</view>
@@ -59,7 +59,8 @@ const fmt = (s: string | null): string => {
const statusText = (st: string): string => {
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 () => {
@@ -133,14 +134,16 @@ onMounted(loadSubs)
<style scoped>
.my-subs { padding: 12px; }
.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; }
.loading, .empty { padding: 24px; text-align: center; color: #888; }
.list { display: flex; flex-direction: column; gap: 12px; }
.card { background: #fff; border-radius: 10px; padding: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.05); }
.row { display: flex; gap: 8px; padding: 4px 0; }
.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); margin-bottom: 12px; }
.card:last-child { margin-bottom: 0; }
.row { display: flex; padding: 4px 0; }
.label { margin-right: 8px; }
.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; }
.st-trial { background: #e6f7ff; color: #1677ff; }
.st-active { background: #f6ffed; color: #52c41a; }
@@ -149,6 +152,7 @@ onMounted(loadSubs)
.label { color: #666; width: 80px; }
.value { color: #111; flex: 1; }
.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; }
</style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -46,7 +46,7 @@
<style>
.page {
min-height: 100vh;
flex: 1;
background: #f7f8fa;
display: flex;
flex-direction: column;
@@ -97,7 +97,6 @@
font-weight: 700;
color: #111;
margin-bottom: 12rpx;
display: block;
}
.p {
@@ -105,7 +104,6 @@
color: rgba(0, 0, 0, 0.65);
line-height: 44rpx;
margin-bottom: 12rpx;
display: block;
}
.divider {
@@ -125,7 +123,7 @@
background: linear-gradient(135deg, #ff4d4f 0%, #ff7a45 100%);
color: #fff;
font-size: 30rpx;
font-weight: 600;
font-weight: 700;
box-shadow: 0 16rpx 32rpx rgba(255, 77, 79, 0.24);
}
</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,
REFUNDING: 7,
REFUNDED: 8
} as const
}
// 优惠券类型枚举
export const COUPON_TYPE = {
@@ -263,7 +263,7 @@ export const COUPON_TYPE = {
CATEGORY: 6, // 品类券
MERCHANT: 7, // 商家券
LIMITED_TIME: 8 // 限时券
} as const
}
// 支付方式枚举
export const PAYMENT_METHOD = {
@@ -271,7 +271,7 @@ export const PAYMENT_METHOD = {
ALIPAY: 2,
UNIONPAY: 3,
BALANCE: 4
} as const
}
// 配送状态枚举
export const DELIVERY_STATUS = {
@@ -281,7 +281,7 @@ export const DELIVERY_STATUS = {
IN_TRANSIT: 4,
DELIVERED: 5,
FAILED: 6
} as const
}
// 用户类型枚举
export const MALL_USER_TYPE = {
@@ -290,7 +290,7 @@ export const MALL_USER_TYPE = {
DELIVERY: 3, // 配送员
SERVICE: 4, // 客服
ADMIN: 5 // 管理员
} as const
}
// 用户状态枚举
export const USER_STATUS = {
@@ -305,7 +305,7 @@ export const VERIFICATION_STATUS = {
UNVERIFIED: 0, // 未认证
VERIFIED: 1, // 已认证
FAILED: 2 // 认证失败
} as const
}
// 地址标签枚举
export const ADDRESS_LABEL = {
@@ -313,13 +313,13 @@ export const ADDRESS_LABEL = {
OFFICE: 'office', // 公司
SCHOOL: 'school', // 学校
OTHER: 'other' // 其他
} as const
}
// 收藏类型枚举
export const FAVORITE_TYPE = {
PRODUCT: 'product', // 商品
SHOP: 'shop' // 店铺
} as const
}
// =========================
// 订阅相关类型与枚举
@@ -329,7 +329,7 @@ export const FAVORITE_TYPE = {
export const SUBSCRIPTION_PERIOD = {
MONTHLY: 'monthly',
YEARLY: 'yearly'
} as const
}
// 订阅状态枚举
export const SUBSCRIPTION_STATUS = {
@@ -338,7 +338,7 @@ export const SUBSCRIPTION_STATUS = {
PAST_DUE: 'past_due',
CANCELED: 'canceled',
EXPIRED: 'expired'
} as const
}
// 软件订阅方案类型
export type SubscriptionPlanType = {
@@ -349,7 +349,7 @@ export type SubscriptionPlanType = {
features: UTSJSONObject | null // { featureKey: description }
price: number // 单位:元(或分,取决于后端;前端以显示为准)
currency: string | null // 'CNY' | 'USD' ...
billing_period: keyof typeof SUBSCRIPTION_PERIOD | string // 'monthly' | 'yearly'
billing_period: string // 'monthly' | 'yearly'
trial_days: number | null
is_active: boolean
sort_order?: number | null
@@ -362,7 +362,7 @@ export type UserSubscriptionType = {
id: string
user_id: string
plan_id: string
status: keyof typeof SUBSCRIPTION_STATUS | string
status: string
start_date: string
end_date: string | null
next_billing_date: string | null
@@ -372,76 +372,6 @@ export type UserSubscriptionType = {
created_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)
export type UserProfile = {

View File

@@ -1,5 +1,5 @@
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
const ACCESS_TOKEN_KEY = 'akreq_access_token';
@@ -231,8 +231,9 @@ export class AkReq {
} catch (e) {}
try {
// 动态读取配置,避免 ak-req 模块与业务工程强耦合
const cfg = require('@/ak/config.uts') as any
const isTest = cfg != null ? (cfg.IS_TEST_MODE === true) : false
// const cfg = require('@/ak/config.uts') as any
// const isTest = cfg != null ? (cfg.IS_TEST_MODE === true) : false
const isTest = IS_TEST_MODE
// if (!isTest) {
// uni.reLaunch({ url: '/pages/user/login' });
// }

View File

@@ -19,21 +19,29 @@ function t(key: string, values: UTSJSONObject | null = null, locale: string | nu
}
// 创建响应式 locale 对象
const localeObj = {
get value(): string {
return currentLocale
},
set value(newLocale: string) {
currentLocale = newLocale
class LocaleWrapper {
get value(): string {
return currentLocale
}
set value(newLocale: string) {
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
const i18nInstance = {
global: {
t: t,
locale: localeObj
}
// I18n Instance
class I18nInstance {
global: I18nGlobal = new I18nGlobal()
}
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