consumer模块完成度95%,能编译在安卓端运行,在解决数据获取和页面布局问题
This commit is contained in:
@@ -2,6 +2,9 @@
|
||||
"version" : "1.0",
|
||||
"configurations" : [
|
||||
{
|
||||
"customPlaygroundType" : "local",
|
||||
"localRepoPath" : "D:/companyproject/mall",
|
||||
"packageName" : "com.huawei.hisuite",
|
||||
"playground" : "standard",
|
||||
"type" : "uni-app:app-android"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mall",
|
||||
"appid": "__UNI__YOUR_APP_ID__",
|
||||
"appid": "__UNI__EC68BC3",
|
||||
"description": "A multi-role e-commerce application.",
|
||||
"versionName": "1.0.0",
|
||||
"versionCode": "100",
|
||||
@@ -73,21 +73,15 @@
|
||||
},
|
||||
"app-android": {
|
||||
"distribute": {
|
||||
"modules": {
|
||||
"uni-payment": {
|
||||
"alipay": {},
|
||||
"wxpay": {}
|
||||
},
|
||||
"uni-push": {
|
||||
"hms": {},
|
||||
"oppo": {},
|
||||
"vivo": {},
|
||||
"xiaomi": {},
|
||||
"meizu": {},
|
||||
"honor": {},
|
||||
"fcm": {}
|
||||
}
|
||||
"modules": {},
|
||||
"android": {
|
||||
"usesCleartextTraffic": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"web": {
|
||||
"router": {
|
||||
"mode": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
326
pages.json
326
pages.json
@@ -243,30 +243,6 @@
|
||||
"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": {
|
||||
@@ -586,33 +562,321 @@
|
||||
// }
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
|
||||
// {
|
||||
// "root": "pages/mall/merchant",
|
||||
// "root": "pages/mall/delivery",
|
||||
// "pages": [
|
||||
// {
|
||||
// "path": "index",
|
||||
// "style": {
|
||||
// "navigationBarTitleText": "商家中心",
|
||||
// "navigationBarTitleText": "配送中心",
|
||||
// "navigationStyle": "custom"
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// "path": "product-detail",
|
||||
// "path": "order-detail",
|
||||
// "style": {
|
||||
// "navigationBarTitleText": "商品管理详情",
|
||||
// "enablePullDownRefresh": false
|
||||
// "navigationBarTitleText": "订单详情页",
|
||||
// "navigationStyle": "custom"
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// "path": "profile",
|
||||
// "style": {
|
||||
// "navigationBarTitleText": "个人资料"
|
||||
// "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/service",
|
||||
// "pages": [
|
||||
// {
|
||||
// "path": "index",
|
||||
// "style": {
|
||||
// "navigationBarTitleText": "客服工作台",
|
||||
// "navigationStyle": "custom"
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// "path": "profile",
|
||||
// "style": {
|
||||
// "navigationBarTitleText": "客服个人中心"
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// "path": "ticket-detail",
|
||||
// "style": {
|
||||
// "navigationBarTitleText": "工单详情",
|
||||
// "enablePullDownRefresh": false
|
||||
// }
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
// {
|
||||
// "root": "pages/mall/service",
|
||||
// "pages": [
|
||||
// {
|
||||
|
||||
2526
pages/mall/consumer/666/index.uvue
Normal file
2526
pages/mall/consumer/666/index.uvue
Normal file
File diff suppressed because it is too large
Load Diff
3888
pages/mall/consumer/666/supabaseService.uts
Normal file
3888
pages/mall/consumer/666/supabaseService.uts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -326,7 +326,7 @@ const loadCartData = async () => {
|
||||
shopId: p.merchant_id ?? 'unknown',
|
||||
shopName: p.shop_name ?? '商城推荐',
|
||||
name: p.name,
|
||||
price: p.base_price,
|
||||
price: p.base_price ?? p.price ?? 0,
|
||||
image: p.main_image_url ?? '/static/images/default-product.png',
|
||||
skuId: ''
|
||||
}
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 导航栏占位 -->
|
||||
<view class="navbar-placeholder" :style="{ height: (statusBarHeight + 44) + 'px' }"></view>
|
||||
<!-- 导航栏占位 - 修复:只使用44px高度,因为search-bar的paddingTop已处理statusBarHeight -->
|
||||
<view class="navbar-placeholder" :style="{ height: '44px' }"></view>
|
||||
|
||||
<!-- 分类内容区 -->
|
||||
<view class="category-content">
|
||||
@@ -77,9 +77,9 @@
|
||||
<view class="price-section">
|
||||
<view class="current-price">
|
||||
<text class="price-symbol">¥</text>
|
||||
<text class="price-value">{{ product.base_price }}</text>
|
||||
<text class="price-value">{{ product.base_price ?? product.price ?? 0 }}</text>
|
||||
</view>
|
||||
<text class="original-price" v-if="product.market_price != null && product.market_price! > product.base_price">
|
||||
<text class="original-price" v-if="product.market_price != null && product.base_price != null && product.market_price! > product.base_price!">
|
||||
¥{{ product.market_price }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
@@ -597,6 +597,12 @@ const processCheckoutItems = (items: any[]) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前用户ID
|
||||
function getCurrentUserId(): string {
|
||||
const userId = supabaseService.getCurrentUserId()
|
||||
return userId ?? ''
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
// 监听地址更新事件
|
||||
@@ -774,14 +780,8 @@ async function loadDefaultAddress(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前用户ID
|
||||
function getCurrentUserId(): string {
|
||||
const userId = supabaseService.getCurrentUserId()
|
||||
return userId ?? ''
|
||||
}
|
||||
|
||||
// 用户登录状态
|
||||
const isLoggedIn = computed(() => {
|
||||
const isLoggedIn = computed((): boolean => {
|
||||
const userId = getCurrentUserId()
|
||||
return userId != ''
|
||||
})
|
||||
|
||||
@@ -113,34 +113,163 @@
|
||||
| `selected` | Boolean | 是否勾选 | 购物车状态 |
|
||||
| `created_at` | Timestamp | 创建时间 | |
|
||||
|
||||
### 3.2 订单主表 (`ml_orders`) (推测结构)
|
||||
### 3.2 订单主表 (`ml_orders`)
|
||||
商家端处理订单的核心表。
|
||||
|
||||
| 字段名 | 类型 | 描述 | 备注 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `id` | UUID | 主键 | |
|
||||
| `order_no` | VARCHAR | 订单号 | 唯一业务单号 |
|
||||
| `user_id` | UUID | 用户 ID | |
|
||||
| `merchant_id` | UUID | 商家 ID | |
|
||||
| `order_no` | Text | 订单号 | 唯一业务单号 |
|
||||
| `total_amount` | Numeric | 订单总金额 | |
|
||||
| `pay_amount` | Numeric | 实付金额 | |
|
||||
| `status` | Integer | 订单状态 | 0: 待付款, 1: 待发货, 2: 待收货, 3: 已完成, -1: 已取消 |
|
||||
| `address_snapshot` | JSONB | 收货地址快照 | 下单时的地址信息 |
|
||||
| `remark` | Text | 订单备注 | |
|
||||
| `created_at` | Timestamp | 下单时间 | |
|
||||
| `product_amount` | NUMERIC | 商品金额 | 默认0 |
|
||||
| `discount_amount` | NUMERIC | 优惠金额 | |
|
||||
| `shipping_fee` | NUMERIC | 运费 | |
|
||||
| `total_amount` | NUMERIC | 订单总金额 | |
|
||||
| `paid_amount` | NUMERIC | 实付金额 | |
|
||||
| `shipping_address` | JSONB | 收货地址 | |
|
||||
| `order_status` | INTEGER | 订单状态 | 1:待付款, 2:待发货, 3:待收货, 4:已完成, 5:已取消, 0:退款中, -1:已取消 |
|
||||
| `payment_status` | INTEGER | 支付状态 | 默认1 |
|
||||
| `shipping_status` | INTEGER | 发货状态 | 默认1 |
|
||||
| `paid_at` | TIMESTAMP | 支付时间 | |
|
||||
| `shipped_at` | TIMESTAMP | 发货时间 | |
|
||||
| `delivered_at` | TIMESTAMP | 收货时间 | |
|
||||
| `completed_at` | TIMESTAMP | 完成时间 | |
|
||||
| `remark` | TEXT | 订单备注 | |
|
||||
| `merchant_memo` | TEXT | 商家备注 | |
|
||||
| `cancel_reason` | TEXT | 取消原因 | |
|
||||
| `created_at` | TIMESTAMP | 创建时间 | 默认now() |
|
||||
| `updated_at` | TIMESTAMP | 更新时间 | |
|
||||
| `cid` | INTEGER | 序号 | |
|
||||
| `payment_method` | VARCHAR | 支付方式 | |
|
||||
| `payment_time` | TIMESTAMP | 支付时间 | |
|
||||
| `shipping_company` | VARCHAR | 物流公司 | 商家端发货填写 |
|
||||
| `tracking_number` | VARCHAR | 物流单号 | 商家端发货填写 |
|
||||
|
||||
### 3.3 订单项表 (`ml_order_items`) (推测结构)
|
||||
### 3.3 订单项表 (`ml_order_items`)
|
||||
|
||||
| 字段名 | 类型 | 描述 | 备注 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `id` | UUID | 主键 | |
|
||||
| `order_id` | UUID | 订单 ID | |
|
||||
| `product_id` | UUID | 商品 ID | |
|
||||
| `sku_id` | UUID | SKU ID | |
|
||||
| `product_name` | Text | 商品名称快照 | |
|
||||
| `price` | Numeric | 成交单价 | |
|
||||
| `quantity` | Integer | 购买数量 | |
|
||||
| `sku_snapshot` | JSONB | 规格快照 | |
|
||||
| `sku_id` | UUID | SKU ID | 可空 |
|
||||
| `product_name` | VARCHAR | 商品名称快照 | |
|
||||
| `sku_name` | VARCHAR | SKU名称 | 可空 |
|
||||
| `specifications` | JSONB | 规格信息 | 默认{} |
|
||||
| `image_url` | TEXT | 商品图片 | 可空 |
|
||||
| `price` | NUMERIC | 成交单价 | |
|
||||
| `quantity` | INTEGER | 购买数量 | |
|
||||
| `total_amount` | NUMERIC | 小计金额 | |
|
||||
| `created_at` | TIMESTAMP | 创建时间 | |
|
||||
| `sku_snapshot` | JSONB | SKU快照 | 默认{} |
|
||||
|
||||
### 3.4 订单详情视图 (`ml_orders_detail_view`)
|
||||
订单联合查询视图。
|
||||
|
||||
| 字段名 | 类型 | 描述 | 备注 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `id` | UUID | 主键 | |
|
||||
| `order_no` | VARCHAR | 订单号 | |
|
||||
| `user_id` | UUID | 用户 ID | |
|
||||
| `merchant_id` | UUID | 商家 ID | |
|
||||
| `product_amount` | NUMERIC | 商品金额 | |
|
||||
| `discount_amount` | NUMERIC | 优惠金额 | |
|
||||
| `shipping_fee` | NUMERIC | 运费 | |
|
||||
| `total_amount` | NUMERIC | 订单总金额 | |
|
||||
| `paid_amount` | NUMERIC | 实付金额 | |
|
||||
| `shipping_address` | JSONB | 收货地址 | |
|
||||
| `order_status` | INTEGER | 订单状态 | |
|
||||
| `payment_status` | INTEGER | 支付状态 | |
|
||||
| `shipping_status` | INTEGER | 发货状态 | |
|
||||
| `paid_at` | TIMESTAMP | 支付时间 | |
|
||||
| `shipped_at` | TIMESTAMP | 发货时间 | |
|
||||
| `delivered_at` | TIMESTAMP | 收货时间 | |
|
||||
| `completed_at` | TIMESTAMP | 完成时间 | |
|
||||
| `remark` | TEXT | 订单备注 | |
|
||||
| `merchant_memo` | TEXT | 商家备注 | |
|
||||
| `cancel_reason` | TEXT | 取消原因 | |
|
||||
| `created_at` | TIMESTAMP | 创建时间 | |
|
||||
| `updated_at` | TIMESTAMP | 更新时间 | |
|
||||
| `customer_name` | VARCHAR | 客户姓名 | |
|
||||
| `customer_phone` | TEXT | 客户电话 | |
|
||||
| `merchant_name` | VARCHAR | 商家名称 | |
|
||||
| `shop_name` | VARCHAR | 店铺名称 | |
|
||||
| `order_status_name` | TEXT | 订单状态名称 | |
|
||||
| `payment_status_name` | TEXT | 支付状态名称 | |
|
||||
|
||||
### 3.5 支付订单表 (`pay_order`)
|
||||
支付网关订单表。
|
||||
|
||||
| 字段名 | 类型 | 描述 | 备注 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `id` | BIGINT | 主键 | |
|
||||
| `merchant_id` | BIGINT | 商户ID | |
|
||||
| `app_id` | BIGINT | 应用ID | |
|
||||
| `channel_id` | BIGINT | 渠道ID | 可空 |
|
||||
| `channel_code` | VARCHAR | 渠道编码 | 可空 |
|
||||
| `merchant_order_id` | VARCHAR | 商户订单号 | |
|
||||
| `subject` | VARCHAR | 订单标题 | |
|
||||
| `body` | VARCHAR | 订单描述 | |
|
||||
| `notify_url` | VARCHAR | 通知地址 | |
|
||||
| `notify_status` | SMALLINT | 通知状态 | |
|
||||
| `amount` | BIGINT | 金额 | |
|
||||
| `channel_fee_rate` | DOUBLE | 渠道费率 | 可空 |
|
||||
| `channel_fee_amount` | BIGINT | 渠道手续费 | 可空 |
|
||||
| `status` | SMALLINT | 状态 | |
|
||||
| `user_ip` | VARCHAR | 用户IP | |
|
||||
| `expire_time` | TIMESTAMP | 过期时间 | |
|
||||
| `success_time` | TIMESTAMP | 成功时间 | 可空 |
|
||||
| `notify_time` | TIMESTAMP | 通知时间 | 可空 |
|
||||
| `success_extension_id` | BIGINT | 成功扩展ID | 可空 |
|
||||
| `refund_status` | SMALLINT | 退款状态 | |
|
||||
| `refund_times` | SMALLINT | 退款次数 | |
|
||||
| `refund_amount` | BIGINT | 退款金额 | |
|
||||
| `channel_user_id` | VARCHAR | 渠道用户ID | 可空 |
|
||||
| `channel_order_no` | VARCHAR | 渠道订单号 | 可空 |
|
||||
| `creator` | VARCHAR | 创建人 | 可空 |
|
||||
| `create_time` | TIMESTAMP | 创建时间 | |
|
||||
| `updater` | VARCHAR | 更新人 | 可空 |
|
||||
| `update_time` | TIMESTAMP | 更新时间 | |
|
||||
| `deleted` | SMALLINT | 删除标记 | 默认0 |
|
||||
| `tenant_id` | BIGINT | 租户ID | 默认0 |
|
||||
|
||||
### 3.6 支付订单扩展表 (`pay_order_extension`)
|
||||
支付订单扩展信息表。
|
||||
|
||||
| 字段名 | 类型 | 描述 | 备注 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `id` | BIGINT | 主键 | |
|
||||
| `no` | VARCHAR | 支付流水号 | |
|
||||
| `order_id` | BIGINT | 支付订单ID | |
|
||||
| `channel_id` | BIGINT | 渠道ID | |
|
||||
| `channel_code` | VARCHAR | 渠道编码 | |
|
||||
| `user_ip` | VARCHAR | 用户IP | |
|
||||
| `status` | SMALLINT | 状态 | |
|
||||
| `channel_extras` | VARCHAR | 渠道额外信息 | 可空 |
|
||||
| `channel_notify_data` | VARCHAR | 渠道通知数据 | 可空 |
|
||||
| `creator` | VARCHAR | 创建人 | 可空 |
|
||||
| `create_time` | TIMESTAMP | 创建时间 | |
|
||||
| `updater` | VARCHAR | 更新人 | 可空 |
|
||||
| `update_time` | TIMESTAMP | 更新时间 | |
|
||||
| `deleted` | SMALLINT | 删除标记 | 默认0 |
|
||||
| `tenant_id` | BIGINT | 租户ID | 默认0 |
|
||||
|
||||
### 3.7 库存订单表 (`stock_orders`)
|
||||
库存相关订单表。
|
||||
|
||||
| 字段名 | 类型 | 描述 | 备注 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `id` | UUID | 主键 | |
|
||||
| `simulation_id` | UUID | 模拟ID | 可空 |
|
||||
| `symbol` | TEXT | 交易标的 | |
|
||||
| `side` | TEXT | 交易方向 | |
|
||||
| `qty` | NUMERIC | 数量 | |
|
||||
| `price` | NUMERIC | 价格 | 可空 |
|
||||
| `status` | TEXT | 状态 | 默认created |
|
||||
| `placed_at` | TIMESTAMP | 下单时间 | |
|
||||
| `executed_at` | TIMESTAMP | 执行时间 | 可空 |
|
||||
| `fee` | NUMERIC | 手续费 | 默认0 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -83,21 +83,44 @@
|
||||
<text class="section-desc">快速定位</text>
|
||||
</view>
|
||||
<view class="category-grid" v-if="categoryTab === 'category'">
|
||||
<!-- 一级分类 -->
|
||||
<view
|
||||
v-for="category in categories"
|
||||
v-for="category in parentCategories"
|
||||
:key="category.id"
|
||||
class="category-card"
|
||||
@click="switchCategory(category)"
|
||||
@click="onParentCategoryClick(category)"
|
||||
:style="{ '--card-color': category.color }"
|
||||
>
|
||||
<view class="card-icon">
|
||||
<text class="card-icon-text">{{ category.icon }}</text>
|
||||
</view>
|
||||
<text class="card-name">{{ category.name }}</text>
|
||||
<text class="card-desc">{{ category.description }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="category-grid" v-else>
|
||||
|
||||
<!-- 二级分类 -->
|
||||
<view v-if="categoryTab === 'category' && showSubCategories && subCategories.length > 0" class="sub-category-grid">
|
||||
<view class="sub-category-header">
|
||||
<text class="sub-category-title">{{ selectedParentCategory?.name }}分类</text>
|
||||
<text class="sub-category-close" @click="showSubCategories = false">✕</text>
|
||||
</view>
|
||||
<view class="sub-category-wrapper">
|
||||
<view
|
||||
v-for="subCat in subCategories"
|
||||
:key="subCat.id"
|
||||
class="sub-category-card"
|
||||
@click="onSubCategoryClick(subCat)"
|
||||
>
|
||||
<view class="card-icon">
|
||||
<text class="card-icon-text">{{ subCat.icon }}</text>
|
||||
</view>
|
||||
<text class="card-name">{{ subCat.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 品牌列表 -->
|
||||
<view class="category-grid" v-if="categoryTab === 'brand'">
|
||||
<view
|
||||
v-for="brand in brands"
|
||||
:key="brand.id"
|
||||
@@ -190,9 +213,9 @@
|
||||
<view class="price-section">
|
||||
<view class="current-price">
|
||||
<text class="price-symbol">¥</text>
|
||||
<text class="price-value">{{ product.base_price }}</text>
|
||||
<text class="price-value">{{ product.base_price ?? product.price ?? 0 }}</text>
|
||||
</view>
|
||||
<text class="original-price" v-if="product.market_price != null && product.market_price! > product.base_price">
|
||||
<text class="original-price" v-if="product.market_price != null && product.base_price != null && product.market_price! > product.base_price!">
|
||||
¥{{ product.market_price }}
|
||||
</text>
|
||||
</view>
|
||||
@@ -304,6 +327,12 @@ const categoryTab = ref<string>('category')
|
||||
const categories = ref<Category[]>([])
|
||||
const brands = ref<Brand[]>([])
|
||||
|
||||
// 一级分类和二级分类
|
||||
const parentCategories = ref<Category[]>([])
|
||||
const subCategories = ref<Category[]>([])
|
||||
const selectedParentCategory = ref<Category | null>(null)
|
||||
const showSubCategories = ref(false)
|
||||
|
||||
// 排序标签类型
|
||||
type SortTab = {
|
||||
id: string
|
||||
@@ -345,40 +374,33 @@ const healthNews = [
|
||||
// 获取分类数据
|
||||
const loadCategories = async (): Promise<void> => {
|
||||
try {
|
||||
const categoriesData = await supabaseService.getCategories()
|
||||
// 映射字段:根据ml_categories表结构映射
|
||||
const mappedCategories: Category[] = []
|
||||
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'
|
||||
// 使用 JSON.parse 创建对象,避免接口实例化问题
|
||||
const categoryItem = JSON.parse(`{"id":"${id}","name":"${name}","icon":"${icon}","description":"${description}","color":"${color}"}`) as Category
|
||||
mappedCategories.push(categoryItem)
|
||||
}
|
||||
// 保持原始顺序或按ID排序,移除随机打乱
|
||||
categories.value = mappedCategories
|
||||
const categoriesData = await supabaseService.getParentCategories()
|
||||
parentCategories.value = categoriesData
|
||||
// 兼容其他使用 categories 的地方
|
||||
categories.value = categoriesData
|
||||
console.log('一级分类数据:', JSON.stringify(parentCategories.value))
|
||||
} catch (error) {
|
||||
console.error('加载分类数据失败:', error)
|
||||
// 如果加载失败,使用默认分类作为后备
|
||||
parentCategories.value = []
|
||||
categories.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 获取二级分类数据
|
||||
const loadSubCategories = async (parentId: string): Promise<void> => {
|
||||
try {
|
||||
const subData = await supabaseService.getSubCategories(parentId)
|
||||
subCategories.value = subData
|
||||
} catch (error) {
|
||||
console.error('加载子分类数据失败:', error)
|
||||
subCategories.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 获取品牌数据
|
||||
const loadBrands = async () => {
|
||||
const loadBrands = async (): Promise<void> => {
|
||||
try {
|
||||
const brandsData = await supabaseService.getBrands()
|
||||
// 保持原始顺序
|
||||
brands.value = brandsData
|
||||
} catch (e) {
|
||||
console.error('加载品牌失败:', e)
|
||||
@@ -626,6 +648,35 @@ const resetNavbar = () => {
|
||||
lastScrollTop.value = 0
|
||||
}
|
||||
|
||||
// 点击一级分类
|
||||
const onParentCategoryClick = async (category: Category): Promise<void> => {
|
||||
// 如果已经选中,则切换显示/隐藏二级分类
|
||||
if (selectedParentCategory.value != null && selectedParentCategory.value.id === category.id) {
|
||||
showSubCategories.value = !showSubCategories.value
|
||||
return
|
||||
}
|
||||
|
||||
// 选中新的分类
|
||||
selectedParentCategory.value = category
|
||||
showSubCategories.value = true
|
||||
|
||||
// 加载二级分类
|
||||
await loadSubCategories(category.id)
|
||||
}
|
||||
|
||||
// 点击二级分类
|
||||
const onSubCategoryClick = (category: Category): void => {
|
||||
// 跳转到分类页面
|
||||
uni.setStorageSync('selectedCategory', category.id)
|
||||
const timestamp = Date.now()
|
||||
const randomParam = Math.random().toString(36).substring(2, 8)
|
||||
const url = `/pages/mall/consumer/category?categoryId=${category.id}&name=${encodeURIComponent(category.name)}×tamp=${timestamp}&random=${randomParam}`
|
||||
|
||||
uni.switchTab({
|
||||
url: '/pages/mall/consumer/category'
|
||||
})
|
||||
}
|
||||
|
||||
// 切换分类 - 跳转到分类页面并传递分类ID
|
||||
const switchCategory = (category: any) => {
|
||||
console.log('=== switchCategory函数开始执行 ===')
|
||||
@@ -1149,6 +1200,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
|
||||
/* cursor: pointer; removed for uniapp-x support */
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid transparent;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.category-card:hover {
|
||||
@@ -1157,6 +1209,78 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
|
||||
border-color: var(--card-color, #4CAF50);
|
||||
}
|
||||
|
||||
/* 二级分类样式 */
|
||||
.sub-category-grid {
|
||||
background: #f8f9fa;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.sub-category-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.sub-category-title {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.sub-category-close {
|
||||
font-size: 16px;
|
||||
color: #999;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.sub-category-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.sub-category-card {
|
||||
width: 23%;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 10px 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid #eee;
|
||||
margin-right: 2%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.sub-category-card .card-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 18px;
|
||||
margin-bottom: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.sub-category-card .card-icon-text {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.sub-category-card .card-name {
|
||||
font-size: 11px;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
lines: 1;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
|
||||
@@ -193,9 +193,9 @@
|
||||
<view class="price-section">
|
||||
<view class="current-price">
|
||||
<text class="price-symbol">¥</text>
|
||||
<text class="price-value">{{ product.base_price }}</text>
|
||||
<text class="price-value">{{ product.base_price ?? product.price ?? 0 }}</text>
|
||||
</view>
|
||||
<text class="original-price" v-if="product.market_price != null && product.market_price! > product.base_price">
|
||||
<text class="original-price" v-if="product.market_price != null && product.base_price != null && product.market_price! > product.base_price!">
|
||||
¥{{ product.market_price }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
@@ -55,19 +55,6 @@ const totalPoints = ref<number>(0)
|
||||
const records = ref<PointRecord[]>([])
|
||||
const loading = ref<boolean>(true)
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
await Promise.all([
|
||||
loadPoints(),
|
||||
loadRecords()
|
||||
])
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
const loadPoints = async () => {
|
||||
// 调用 service 获取积分 (需要supabaseService支持)
|
||||
// 暂时如果service没更新,先用mock
|
||||
@@ -84,12 +71,30 @@ const loadPoints = async () => {
|
||||
const loadRecords = async () => {
|
||||
try {
|
||||
const list = await supabaseService.getPointRecords()
|
||||
records.value = list
|
||||
const typedList: PointRecord[] = []
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const item = list[i] as PointRecord
|
||||
typedList.push(item)
|
||||
}
|
||||
records.value = typedList
|
||||
} catch (e) {
|
||||
console.error('获取积分记录失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
await Promise.all<void>([
|
||||
loadPoints(),
|
||||
loadRecords()
|
||||
])
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
const handleExchange = () => {
|
||||
uni.showToast({
|
||||
title: '积分商城开发中',
|
||||
@@ -98,14 +103,12 @@ const handleExchange = () => {
|
||||
}
|
||||
|
||||
const getTypeText = (type: string): string => {
|
||||
const map: Record<string, string> = {
|
||||
'signin': '每日签到',
|
||||
'shopping': '购物奖励',
|
||||
'redeem': '积分兑换',
|
||||
'admin': '系统调整',
|
||||
'register': '注册赠送'
|
||||
}
|
||||
return map[type] ?? '积分变动'
|
||||
if (type === 'signin') return '每日签到'
|
||||
if (type === 'shopping') return '购物奖励'
|
||||
if (type === 'redeem') return '积分兑换'
|
||||
if (type === 'admin') return '系统调整'
|
||||
if (type === 'register') return '注册赠送'
|
||||
return '积分变动'
|
||||
}
|
||||
|
||||
const formatTime = (timeStr: string): string => {
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
</view>
|
||||
<input class="quantity-input"
|
||||
type="number"
|
||||
v-model="quantity"
|
||||
:value="quantity.toString()"
|
||||
:min="1"
|
||||
:max="getMaxQuantity()"
|
||||
@input="validateQuantity" />
|
||||
@@ -97,7 +97,7 @@
|
||||
<view class="section-title">商品详情</view>
|
||||
<text class="description-text">{{ product.description ?? '暂无详细描述' }}</text>
|
||||
<!-- 商品详情图片 -->
|
||||
<view class="detail-images" v-if="product.images && product.images.length > 0">
|
||||
<view class="detail-images" v-if="product.images.length > 0">
|
||||
<image v-for="(img, index) in product.images"
|
||||
:key="index"
|
||||
:src="img"
|
||||
@@ -187,9 +187,9 @@
|
||||
<text class="params-label">批准文号</text>
|
||||
<text class="params-value">{{ product.approval_number }}</text>
|
||||
</view>
|
||||
<view class="params-item" v-if="product.tags && product.tags.length > 0">
|
||||
<view class="params-item" v-if="product.tags != null && product.tags.length > 0">
|
||||
<text class="params-label">标签</text>
|
||||
<text class="params-value">{{ product.tags.join(', ') }}</text>
|
||||
<text class="params-value">{{ product.tags!.join(', ') }}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
@@ -226,7 +226,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ProductType, MerchantType, ProductSkuType } from '@/types/mall-types.uts'
|
||||
import { ProductType, MerchantType, ProductSkuType, CouponTemplateType, FootprintItemType } from '@/types/mall-types.uts'
|
||||
import { supabaseService } from '@/utils/supabaseService.uts'
|
||||
|
||||
export default {
|
||||
@@ -269,36 +269,39 @@ export default {
|
||||
isFavorite: false,
|
||||
showParams: false,
|
||||
// 新增: 优惠券相关
|
||||
coupons: [] as any[],
|
||||
coupons: [] as Array<CouponTemplateType>,
|
||||
showCoupons: false
|
||||
}
|
||||
},
|
||||
onLoad(options: any) {
|
||||
const productId = (options['productId'] ?? options['id']) as string
|
||||
const productPrice = options.price ? parseFloat(options.price) : null
|
||||
const productOriginalPrice = options.originalPrice ? parseFloat(options.originalPrice) : null
|
||||
const opts = options as UTSJSONObject
|
||||
const productId = (opts.getString('productId') ?? opts.getString('id')) as string
|
||||
const priceStr = opts.getString('price')
|
||||
const productPrice = priceStr != null ? parseFloat(priceStr) : null
|
||||
const originalPriceStr = opts.getString('originalPrice')
|
||||
const productOriginalPrice = originalPriceStr != null ? parseFloat(originalPriceStr) : null
|
||||
|
||||
// 处理商品名称:如果是编码的则解码,否则直接使用
|
||||
let productName = options.name as string
|
||||
if (productName) {
|
||||
let productName = opts.getString('name') as string | null
|
||||
if (productName != null) {
|
||||
try {
|
||||
// 尝试解码,如果失败(不是有效的URI组件)则使用原值
|
||||
productName = decodeURIComponent(productName)
|
||||
const decodedName = decodeURIComponent(productName)
|
||||
productName = decodedName
|
||||
} catch (e) {
|
||||
console.warn('ProductName decode failed, using original:', productName)
|
||||
}
|
||||
}
|
||||
|
||||
let productImage = options.image as string
|
||||
if (productImage) {
|
||||
let productImage = opts.getString('image') as string | null
|
||||
if (productImage != null) {
|
||||
try {
|
||||
productImage = decodeURIComponent(productImage)
|
||||
const decodedImage = decodeURIComponent(productImage)
|
||||
productImage = decodedImage
|
||||
} catch (e) {
|
||||
console.warn('ProductImage decode failed, using original:', productImage)
|
||||
}
|
||||
}
|
||||
|
||||
if (productId) {
|
||||
if (productId != null) {
|
||||
this.loadProductDetail(productId, {
|
||||
price: productPrice,
|
||||
originalPrice: productOriginalPrice,
|
||||
@@ -308,8 +311,7 @@ export default {
|
||||
this.checkFavoriteStatus(productId)
|
||||
this.saveFootprint(productId)
|
||||
|
||||
// 设置导航栏标题为商品名称
|
||||
if (productName) {
|
||||
if (productName != null) {
|
||||
uni.setNavigationBarTitle({
|
||||
title: productName
|
||||
})
|
||||
@@ -318,9 +320,9 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
displayPrice(): number {
|
||||
if (this.selectedSkuId) {
|
||||
if (this.selectedSkuId != null && this.selectedSkuId !== '') {
|
||||
const sku = this.productSkus.find(s => s.id === this.selectedSkuId)
|
||||
if (sku) return sku.price
|
||||
if (sku != null) return sku!.price
|
||||
}
|
||||
return this.product.price
|
||||
}
|
||||
@@ -329,17 +331,17 @@ export default {
|
||||
saveFootprint(productId: string) {
|
||||
// 调用后端API记录足迹
|
||||
supabaseService.addFootprint(productId).then(success => {
|
||||
if (success) {
|
||||
if (success === true) {
|
||||
console.log('足迹已同步到服务器')
|
||||
}
|
||||
})
|
||||
|
||||
const footprintData = uni.getStorageSync('footprints')
|
||||
let footprints: any[] = []
|
||||
const footprintData = uni.getStorageSync('footprints') as string | null
|
||||
let footprints: Array<FootprintItemType> = []
|
||||
|
||||
if (footprintData) {
|
||||
if (footprintData != null && footprintData !== '') {
|
||||
try {
|
||||
footprints = JSON.parse(footprintData as string) as any[]
|
||||
footprints = JSON.parse(footprintData) as Array<FootprintItemType>
|
||||
} catch (e) {
|
||||
console.error('Failed to parse footprints', e)
|
||||
}
|
||||
@@ -372,55 +374,59 @@ export default {
|
||||
uni.showLoading({ title: '加载中...' })
|
||||
try {
|
||||
const dbProductResponse = await supabaseService.getProductById(productId)
|
||||
let dbProduct: any | null = null
|
||||
if (Array.isArray(dbProductResponse) && dbProductResponse.length > 0) {
|
||||
dbProduct = dbProductResponse[0]
|
||||
} else if (dbProductResponse && !Array.isArray(dbProductResponse)) {
|
||||
dbProduct = dbProductResponse
|
||||
let dbProduct: UTSJSONObject | null = null
|
||||
if (Array.isArray(dbProductResponse)) {
|
||||
const arr = dbProductResponse as any[]
|
||||
if (arr.length > 0) {
|
||||
dbProduct = arr[0] as UTSJSONObject
|
||||
}
|
||||
} else if (dbProductResponse != null) {
|
||||
dbProduct = dbProductResponse as UTSJSONObject
|
||||
}
|
||||
|
||||
if (dbProduct) {
|
||||
if (dbProduct != null) {
|
||||
// Map DB product to local product
|
||||
this.product = {
|
||||
id: dbProduct.id,
|
||||
merchant_id: dbProduct.merchant_id ?? dbProduct.shop_id ?? '',
|
||||
category_id: dbProduct.category_id ?? '',
|
||||
name: dbProduct.name,
|
||||
description: dbProduct.description ?? '',
|
||||
id: dbProduct['id'] as string,
|
||||
merchant_id: (dbProduct['merchant_id'] ?? dbProduct['shop_id'] ?? '') as string,
|
||||
category_id: (dbProduct['category_id'] ?? '') as string,
|
||||
name: dbProduct['name'] as string,
|
||||
description: (dbProduct['description'] ?? '') as string,
|
||||
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,
|
||||
status: dbProduct.status !== undefined ? dbProduct.status : 1,
|
||||
created_at: dbProduct.created_at ?? new Date().toISOString(),
|
||||
price: (dbProduct['base_price'] ?? dbProduct['price'] ?? 0) as number,
|
||||
original_price: (dbProduct['market_price'] ?? dbProduct['original_price'] ?? 0) as number,
|
||||
stock: (dbProduct['available_stock'] ?? dbProduct['total_stock'] ?? dbProduct['stock'] ?? 0) as number,
|
||||
sales: (dbProduct['sale_count'] ?? dbProduct['sales'] ?? 0) as number,
|
||||
status: dbProduct['status'] != null ? dbProduct['status'] as number : 1,
|
||||
created_at: (dbProduct['created_at'] ?? new Date().toISOString()) as string,
|
||||
// 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'] as string | null,
|
||||
usage: dbProduct['usage'] as string | null,
|
||||
side_effects: dbProduct['side_effects'] as string | null,
|
||||
precautions: dbProduct['precautions'] as string | null,
|
||||
expiry_date: dbProduct['expiry_date'] as string | null,
|
||||
storage_conditions: dbProduct['storage_conditions'] as string | null,
|
||||
approval_number: dbProduct['approval_number'] as string | null,
|
||||
tags: [] as string[]
|
||||
} as ProductType
|
||||
|
||||
// Handle Images
|
||||
if (dbProduct.image_urls) {
|
||||
if (dbProduct['image_urls'] != null) {
|
||||
try {
|
||||
const parsed = typeof dbProduct.image_urls === 'string' ? JSON.parse(dbProduct.image_urls) : dbProduct.image_urls
|
||||
const imageUrls = dbProduct['image_urls']
|
||||
const parsed = typeof imageUrls === 'string' ? JSON.parse(imageUrls) : imageUrls
|
||||
if (Array.isArray(parsed)) {
|
||||
this.product.images = parsed.map((i: any) => String(i))
|
||||
this.product.images = (parsed as any[]).map((i: any): string => i as string)
|
||||
}
|
||||
} catch (e) { console.error('Error parsing image_urls', e) }
|
||||
}
|
||||
// Fallback to main_image_url if no images found
|
||||
if (this.product.images.length === 0 && dbProduct.main_image_url) {
|
||||
this.product.images.push(dbProduct.main_image_url)
|
||||
if (this.product.images.length === 0 && dbProduct['main_image_url'] != null) {
|
||||
this.product.images.push(dbProduct['main_image_url'] as string)
|
||||
}
|
||||
// Fallback to 'image' field (legacy)
|
||||
if (this.product.images.length === 0 && dbProduct.image) {
|
||||
this.product.images.push(dbProduct.image)
|
||||
if (this.product.images.length === 0 && dbProduct['image'] != null) {
|
||||
this.product.images.push(dbProduct['image'] as string)
|
||||
}
|
||||
// Final fallback
|
||||
if (this.product.images.length === 0) {
|
||||
@@ -428,23 +434,25 @@ export default {
|
||||
}
|
||||
|
||||
// Handle Tags
|
||||
if (dbProduct.tags) {
|
||||
if (dbProduct['tags'] != null) {
|
||||
try {
|
||||
const parsedTags = typeof dbProduct.tags === 'string' ? JSON.parse(dbProduct.tags) : dbProduct.tags
|
||||
const tagsData = dbProduct['tags']
|
||||
const parsedTags = typeof tagsData === 'string' ? JSON.parse(tagsData) : tagsData
|
||||
if (Array.isArray(parsedTags)) {
|
||||
this.product.tags = parsedTags.map((t: any) => String(t))
|
||||
this.product.tags = (parsedTags as any[]).map((t: any): string => t as string)
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// Handle JSON attributes if present
|
||||
if (dbProduct.attributes && typeof dbProduct.attributes === 'string') {
|
||||
const attributes = dbProduct['attributes']
|
||||
if (attributes != null && typeof attributes === 'string') {
|
||||
try {
|
||||
const attrs = JSON.parse(dbProduct.attributes)
|
||||
if (attrs) {
|
||||
const attrs = JSON.parse(attributes) as UTSJSONObject | null
|
||||
if (attrs != null) {
|
||||
// Merge attributes into product if they match keys
|
||||
if (attrs.specification) this.product.specification = attrs.specification
|
||||
if (attrs.usage) this.product.usage = attrs.usage
|
||||
if (attrs['specification'] != null) this.product.specification = attrs['specification'] as string
|
||||
if (attrs['usage'] != null) this.product.usage = attrs['usage'] as string
|
||||
// ... augment as needed
|
||||
}
|
||||
} catch(e) {}
|
||||
@@ -456,18 +464,23 @@ export default {
|
||||
console.error('Failed to load product detail:', e)
|
||||
// Fallback to options if available
|
||||
this.product.id = productId
|
||||
this.product.name = options.name ? decodeURIComponent(options.name) : '未知商品'
|
||||
this.product.price = options.price ? parseFloat(options.price) : 0
|
||||
this.product.images = options.image ? [decodeURIComponent(options.image)] : ['/static/default-product.png']
|
||||
const opts = options as UTSJSONObject
|
||||
const nameOpt = opts['name'] as string | null
|
||||
this.product.name = (nameOpt != null && nameOpt !== '') ? decodeURIComponent(nameOpt) ?? '未知商品' : '未知商品'
|
||||
const priceOpt = opts['price'] as string | null
|
||||
this.product.price = (priceOpt != null && priceOpt !== '') ? parseFloat(priceOpt) : 0
|
||||
const imageOpt = opts['image'] as string | null
|
||||
const decodedImage = (imageOpt != null && imageOpt !== '') ? decodeURIComponent(imageOpt) : null
|
||||
this.product.images = decodedImage != null ? [decodedImage] : ['/static/default-product.png']
|
||||
}
|
||||
|
||||
// Load Merchant and SKUs
|
||||
if (this.product.merchant_id) {
|
||||
if (this.product.merchant_id != null && this.product.merchant_id !== '') {
|
||||
await this.loadMerchantInfo(this.product.merchant_id)
|
||||
// 加载优惠券
|
||||
this.loadCoupons()
|
||||
}
|
||||
if (this.product.id) {
|
||||
if (this.product.id != null && this.product.id !== '') {
|
||||
this.loadProductSkus(this.product.id)
|
||||
}
|
||||
|
||||
@@ -478,21 +491,22 @@ export default {
|
||||
let realMerchantLoaded = false
|
||||
if (merchantId.includes('-') || !merchantId.startsWith('merchant_')) {
|
||||
try {
|
||||
const shop = await supabaseService.getShopByMerchantId(merchantId)
|
||||
if (shop) {
|
||||
const shopResponse = await supabaseService.getShopByMerchantId(merchantId)
|
||||
if (shopResponse != null) {
|
||||
const shop = shopResponse as UTSJSONObject
|
||||
this.merchant = {
|
||||
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 ?? '',
|
||||
id: shop['id'] as string,
|
||||
user_id: shop['merchant_id'] as string,
|
||||
shop_name: shop['shop_name'] as string,
|
||||
shop_logo: (shop['shop_logo'] ?? '/static/default-shop.png') as string,
|
||||
shop_banner: (shop['shop_banner'] ?? '/static/default-banner.png') as string,
|
||||
shop_description: (shop['description'] ?? '') as string,
|
||||
contact_name: (shop['contact_name'] ?? '店主') as string,
|
||||
contact_phone: (shop['contact_phone'] ?? '') as string,
|
||||
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) as number,
|
||||
total_sales: (shop['total_sales'] ?? 0) as number,
|
||||
created_at: (shop['created_at'] ?? new Date().toISOString()) as string
|
||||
} as MerchantType
|
||||
realMerchantLoaded = true
|
||||
}
|
||||
@@ -502,7 +516,14 @@ export default {
|
||||
}
|
||||
|
||||
if (!realMerchantLoaded) {
|
||||
const merchantIndex = Math.abs(merchantId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 5
|
||||
let charSum: number = 0
|
||||
for (let i = 0; i < merchantId.length; i++) {
|
||||
const charCode = merchantId.charCodeAt(i)
|
||||
if (charCode != null) {
|
||||
charSum += charCode
|
||||
}
|
||||
}
|
||||
const merchantIndex = Math.abs(charSum) % 5
|
||||
const shopNames = ['优质好店', '品牌直营店', '官方旗舰店', '专卖店', '精品小店']
|
||||
|
||||
this.merchant = {
|
||||
@@ -528,29 +549,31 @@ export default {
|
||||
const skus = await supabaseService.getProductSkus(productId)
|
||||
if (skus.length > 0) {
|
||||
console.log('加载到商品SKU:', skus.length)
|
||||
this.productSkus = skus.map((sku): ProductSkuType => {
|
||||
let specs: UTSJSONObject = {}
|
||||
if (sku.specifications) {
|
||||
try {
|
||||
if (typeof sku.specifications === 'string') {
|
||||
specs = JSON.parse(sku.specifications) as UTSJSONObject
|
||||
} else {
|
||||
// 假设已经是对象
|
||||
specs = sku.specifications as unknown as UTSJSONObject
|
||||
this.productSkus = skus.map((skuData): ProductSkuType => {
|
||||
const sku = skuData as UTSJSONObject
|
||||
let specs: UTSJSONObject = {}
|
||||
const specsData = sku['specifications']
|
||||
if (specsData != null) {
|
||||
try {
|
||||
if (typeof specsData === 'string') {
|
||||
specs = JSON.parse(specsData) as UTSJSONObject
|
||||
} else {
|
||||
// 假设已经是对象
|
||||
specs = specsData as UTSJSONObject
|
||||
}
|
||||
} catch(e) {
|
||||
console.error('解析SKU规格失败', e)
|
||||
}
|
||||
} catch(e) {
|
||||
console.error('解析SKU规格失败', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
id: sku.id,
|
||||
product_id: sku.product_id,
|
||||
sku_code: sku.sku_code,
|
||||
id: sku['id'] as string,
|
||||
product_id: sku['product_id'] as string,
|
||||
sku_code: sku['sku_code'] as string,
|
||||
specifications: specs,
|
||||
price: sku.price,
|
||||
stock: sku.stock !== undefined ? sku.stock : 0,
|
||||
image_url: sku.image_url != null ? sku.image_url : '',
|
||||
status: sku.status !== undefined ? sku.status : 1
|
||||
price: sku['price'] as number,
|
||||
stock: sku['stock'] != null ? sku['stock'] as number : 0,
|
||||
image_url: sku['image_url'] != null ? sku['image_url'] as string : '',
|
||||
status: sku['status'] != null ? sku['status'] as number : 1
|
||||
} as ProductSkuType
|
||||
})
|
||||
return
|
||||
@@ -564,13 +587,18 @@ export default {
|
||||
async loadCoupons() {
|
||||
if (this.product.merchant_id == '') return
|
||||
// Safety check for cached service definition
|
||||
// @ts-ignore
|
||||
if (typeof supabaseService.fetchShopCoupons === 'function') {
|
||||
this.coupons = await supabaseService.fetchShopCoupons(this.product.merchant_id)
|
||||
} else if (typeof supabaseService.getAvailableCoupons === 'function') {
|
||||
this.coupons = await supabaseService.getAvailableCoupons(this.product.merchant_id)
|
||||
} else {
|
||||
console.warn('SupabaseService.fetchShopCoupons method missing in runtime. Please restart project.')
|
||||
try {
|
||||
// @ts-ignore
|
||||
const couponData = await supabaseService.fetchShopCoupons(this.product.merchant_id)
|
||||
this.coupons = couponData as Array<CouponTemplateType>
|
||||
} catch (e) {
|
||||
try {
|
||||
// @ts-ignore
|
||||
const couponData2 = await supabaseService.getAvailableCoupons(this.product.merchant_id)
|
||||
this.coupons = couponData2 as Array<CouponTemplateType>
|
||||
} catch (e2) {
|
||||
console.warn('SupabaseService coupon methods not available:', e2)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -596,7 +624,7 @@ export default {
|
||||
},
|
||||
|
||||
// 新增:领取优惠券
|
||||
async claimCoupon(coupon: any) {
|
||||
async claimCoupon(coupon: CouponTemplateType) {
|
||||
const userId = supabaseService.getCurrentUserId()
|
||||
if (userId == '') {
|
||||
uni.navigateTo({ url: '/pages/auth/login' })
|
||||
@@ -605,17 +633,21 @@ export default {
|
||||
uni.showLoading({ title: '领取中' })
|
||||
|
||||
let success = false
|
||||
// @ts-ignore
|
||||
if (typeof supabaseService.claimShopCoupon === 'function') {
|
||||
success = await supabaseService.claimShopCoupon(coupon.id, userId)
|
||||
} else if (typeof supabaseService.claimCoupon === 'function') {
|
||||
success = await supabaseService.claimCoupon(coupon.id, userId)
|
||||
} else {
|
||||
console.warn('claimCoupon method missing')
|
||||
const couponId = coupon.id
|
||||
try {
|
||||
// @ts-ignore
|
||||
success = await supabaseService.claimShopCoupon(couponId, userId!)
|
||||
} catch (e) {
|
||||
try {
|
||||
// @ts-ignore
|
||||
success = await supabaseService.claimCoupon(couponId, userId!)
|
||||
} catch (e2) {
|
||||
console.warn('claimCoupon method missing:', e2)
|
||||
}
|
||||
}
|
||||
|
||||
uni.hideLoading()
|
||||
if (success) {
|
||||
if (success === true) {
|
||||
uni.showToast({ title: '领取成功', icon: 'success' })
|
||||
} else {
|
||||
uni.showToast({ title: '领取失败或已领取', icon: 'none' })
|
||||
@@ -629,7 +661,9 @@ export default {
|
||||
},
|
||||
|
||||
onSwiperChange(e: any) {
|
||||
this.currentImageIndex = e.detail.current
|
||||
const eventObj = e as UTSJSONObject
|
||||
const detail = eventObj['detail'] as UTSJSONObject
|
||||
this.currentImageIndex = detail['current'] as number
|
||||
},
|
||||
|
||||
showSpecModal() {
|
||||
@@ -647,15 +681,16 @@ export default {
|
||||
},
|
||||
|
||||
getSkuSpecText(sku: ProductSkuType): string {
|
||||
if (sku.specifications) {
|
||||
const specs: any = sku.specifications
|
||||
return Object.keys(specs).map(key => `${key}: ${specs[key]}`).join(', ')
|
||||
if (sku.specifications != null) {
|
||||
const specs = sku.specifications as UTSJSONObject
|
||||
// 简化处理,直接返回 JSON 字符串
|
||||
return JSON.stringify(specs)
|
||||
}
|
||||
return sku.sku_code
|
||||
},
|
||||
|
||||
async addToCart() {
|
||||
if (this.productSkus.length > 0 && !this.selectedSkuId) {
|
||||
if (this.productSkus.length > 0 && (this.selectedSkuId == null || this.selectedSkuId === '')) {
|
||||
uni.showToast({
|
||||
title: '请选择规格',
|
||||
icon: 'none'
|
||||
@@ -673,7 +708,7 @@ export default {
|
||||
)
|
||||
uni.hideLoading()
|
||||
|
||||
if (success) {
|
||||
if (success === true) {
|
||||
uni.showToast({ title: '已添加到购物车', icon: 'success' })
|
||||
} else {
|
||||
console.error('添加购物车返回失败')
|
||||
@@ -687,7 +722,7 @@ export default {
|
||||
},
|
||||
|
||||
buyNow() {
|
||||
if (this.productSkus.length > 0 && !this.selectedSkuId) {
|
||||
if (this.productSkus.length > 0 && (this.selectedSkuId == null || this.selectedSkuId === '')) {
|
||||
uni.showToast({
|
||||
title: '请选择规格',
|
||||
icon: 'none'
|
||||
@@ -695,29 +730,24 @@ export default {
|
||||
return
|
||||
}
|
||||
|
||||
const sku = this.selectedSkuId ? this.productSkus.find(s => s.id === this.selectedSkuId) : null
|
||||
const sku = (this.selectedSkuId != null && this.selectedSkuId !== '') ? this.productSkus.find(s => s.id === this.selectedSkuId) : null
|
||||
|
||||
const selectedItem = {
|
||||
id: this.selectedSkuId,
|
||||
product_id: this.product.id,
|
||||
sku_id: this.selectedSkuId,
|
||||
product_name: this.product.name,
|
||||
product_image: (sku && sku.image_url) ? sku.image_url : this.product.images[0],
|
||||
sku_specifications: sku ? sku.specifications : {},
|
||||
price: Number(parseFloat((sku ? sku.price : this.product.price).toString()).toFixed(2)),
|
||||
quantity: Number(this.quantity)
|
||||
product_image: (sku != null && sku.image_url != null) ? sku!.image_url : this.product.images[0],
|
||||
sku_specifications: sku != null ? sku!.specifications : {},
|
||||
price: parseFloat((sku != null ? sku!.price : this.product.price).toString()).toFixed(2) as string,
|
||||
quantity: this.quantity as number
|
||||
}
|
||||
|
||||
uni.setStorageSync('checkout_type', 'buy_now')
|
||||
uni.setStorageSync('checkout_items', JSON.stringify([selectedItem]))
|
||||
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/consumer/checkout',
|
||||
success: (res) => {
|
||||
res.eventChannel.emit('acceptData', {
|
||||
selectedItems: [selectedItem]
|
||||
})
|
||||
}
|
||||
url: '/pages/mall/consumer/checkout'
|
||||
})
|
||||
},
|
||||
|
||||
@@ -761,7 +791,7 @@ export default {
|
||||
},
|
||||
|
||||
goToShop() {
|
||||
if (this.merchant.user_id) {
|
||||
if (this.merchant.user_id != null && this.merchant.user_id !== '') {
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/consumer/shop-detail?merchantId=${this.merchant.id}`
|
||||
})
|
||||
@@ -788,8 +818,7 @@ export default {
|
||||
},
|
||||
|
||||
validateQuantity() {
|
||||
let num = parseInt(this.quantity)
|
||||
if (isNaN(num)) num = 1
|
||||
let num = this.quantity
|
||||
const maxQuantity = this.getMaxQuantity()
|
||||
if (num < 1) num = 1
|
||||
else if (num > maxQuantity) {
|
||||
@@ -800,9 +829,9 @@ export default {
|
||||
},
|
||||
|
||||
getMaxQuantity() {
|
||||
if (this.selectedSkuId) {
|
||||
if (this.selectedSkuId != null && this.selectedSkuId !== '') {
|
||||
const sku = this.productSkus.find(s => s.id === this.selectedSkuId)
|
||||
if (sku) return sku.stock
|
||||
if (sku != null) return sku!.stock
|
||||
}
|
||||
return this.product.stock
|
||||
},
|
||||
|
||||
@@ -263,6 +263,7 @@ type OrderCountsType = {
|
||||
pending: number
|
||||
toship: number
|
||||
shipped: number
|
||||
review: number
|
||||
}
|
||||
|
||||
type ServiceCountsType = {
|
||||
@@ -306,7 +307,8 @@ export default {
|
||||
pending: 0,
|
||||
toship: 0,
|
||||
shipped: 0,
|
||||
} as any,
|
||||
review: 0
|
||||
} as OrderCountsType,
|
||||
serviceCounts: {
|
||||
coupons: 0,
|
||||
favorites: 0
|
||||
@@ -346,20 +348,32 @@ export default {
|
||||
uni.$off('orderUpdated', this.handleOrderUpdated)
|
||||
},
|
||||
computed: {
|
||||
// 根据当前Tab筛选订单
|
||||
filteredOrders(): Array<OrderType> {
|
||||
const result: Array<OrderType> = []
|
||||
if (this.currentOrderTab === 'all') {
|
||||
return this.allOrders
|
||||
} else if (this.currentOrderTab === 'pending') {
|
||||
return this.allOrders.filter((order: OrderType): boolean => order.status === 1)
|
||||
} else if (this.currentOrderTab === 'toship') {
|
||||
return this.allOrders.filter((order: OrderType): boolean => order.status === 2)
|
||||
} else if (this.currentOrderTab === 'shipped') {
|
||||
return this.allOrders.filter((order: OrderType): boolean => order.status === 3)
|
||||
} else if (this.currentOrderTab === 'review') {
|
||||
return this.allOrders.filter((order: OrderType): boolean => order.status === 4)
|
||||
for (let i: number = 0; i < this.allOrders.length; i++) {
|
||||
result.push(this.allOrders[i])
|
||||
}
|
||||
return result
|
||||
}
|
||||
return []
|
||||
let targetStatus: number = 0
|
||||
if (this.currentOrderTab === 'pending') {
|
||||
targetStatus = 1
|
||||
} else if (this.currentOrderTab === 'toship') {
|
||||
targetStatus = 2
|
||||
} else if (this.currentOrderTab === 'shipped') {
|
||||
targetStatus = 3
|
||||
} else if (this.currentOrderTab === 'review') {
|
||||
targetStatus = 4
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
for (let i: number = 0; i < this.allOrders.length; i++) {
|
||||
if (this.allOrders[i].status === targetStatus) {
|
||||
result.push(this.allOrders[i])
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -368,38 +382,70 @@ export default {
|
||||
try {
|
||||
const orders = await supabaseService.getOrders()
|
||||
|
||||
// 映射数据库字段到前端类型
|
||||
this.allOrders = orders.map((o: any): OrderType => {
|
||||
// 确保 status 字段存在
|
||||
if (o['status'] == null && o['order_status'] != null) {
|
||||
o['status'] = o['order_status']
|
||||
}
|
||||
// 确保 actual_amount 存在
|
||||
if (o['actual_amount'] == null && o['total_amount'] != null) {
|
||||
o['actual_amount'] = o['total_amount']
|
||||
}
|
||||
return o as OrderType
|
||||
})
|
||||
const mappedOrders: Array<OrderType> = []
|
||||
for (let i: number = 0; i < orders.length; i++) {
|
||||
const o = orders[i] as UTSJSONObject
|
||||
const orderObj = new UTSJSONObject()
|
||||
|
||||
// 按时间倒序 (created_at)
|
||||
this.allOrders.sort((a: any, b: any) => {
|
||||
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
|
||||
})
|
||||
const keys = UTSJSONObject.keys(o)
|
||||
for (let j: number = 0; j < keys.length; j++) {
|
||||
const key = keys[j]
|
||||
orderObj.set(key, o.get(key))
|
||||
}
|
||||
|
||||
// 过滤最近的订单
|
||||
this.recentOrders = this.allOrders.slice(0, 5)
|
||||
if (o.getNumber('status') == null && o.getNumber('order_status') != null) {
|
||||
orderObj.set('status', o.getNumber('order_status'))
|
||||
}
|
||||
if (o.getNumber('actual_amount') == null && o.getNumber('total_amount') != null) {
|
||||
orderObj.set('actual_amount', o.getNumber('total_amount'))
|
||||
}
|
||||
mappedOrders.push(orderObj as OrderType)
|
||||
}
|
||||
|
||||
for (let i: number = 0; i < mappedOrders.length; i++) {
|
||||
for (let j: number = i + 1; j < mappedOrders.length; j++) {
|
||||
const dateA = mappedOrders[i]['created_at'] as string
|
||||
const dateB = mappedOrders[j]['created_at'] as string
|
||||
const timeA = new Date(dateA != null ? dateA : '1970-01-01').getTime()
|
||||
const timeB = new Date(dateB != null ? dateB : '1970-01-01').getTime()
|
||||
if (timeA < timeB) {
|
||||
const temp = mappedOrders[i]
|
||||
mappedOrders[i] = mappedOrders[j]
|
||||
mappedOrders[j] = temp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.allOrders = mappedOrders
|
||||
|
||||
const recentList: Array<OrderType> = []
|
||||
const limit = mappedOrders.length < 5 ? mappedOrders.length : 5
|
||||
for (let i: number = 0; i < limit; i++) {
|
||||
recentList.push(mappedOrders[i])
|
||||
}
|
||||
this.recentOrders = recentList
|
||||
|
||||
let total = 0
|
||||
let pending = 0
|
||||
let toship = 0
|
||||
let shipped = 0
|
||||
let review = 0
|
||||
|
||||
for (let i: number = 0; i < mappedOrders.length; i++) {
|
||||
total++
|
||||
const status = mappedOrders[i].status
|
||||
if (status === 1) pending++
|
||||
else if (status === 2) toship++
|
||||
else if (status === 3) shipped++
|
||||
else if (status === 4) review++
|
||||
}
|
||||
|
||||
// 更新角标统计 (确保状态码一致: 1=待支付, 2=待发货, 3=待收货, 4=待评价)
|
||||
this.orderCounts = {
|
||||
total: this.allOrders.length,
|
||||
pending: this.allOrders.filter((o: any) => o.status === 1).length,
|
||||
toship: this.allOrders.filter((o: any) => o.status === 2).length,
|
||||
shipped: this.allOrders.filter((o: any) => o.status === 3).length,
|
||||
review: this.allOrders.filter((o: any) => o.status === 4).length
|
||||
total: total,
|
||||
pending: pending,
|
||||
toship: toship,
|
||||
shipped: shipped,
|
||||
review: review
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载订单异常', e)
|
||||
@@ -413,14 +459,11 @@ export default {
|
||||
|
||||
// 获取当前订单部分标题
|
||||
getOrderSectionTitle(): string {
|
||||
const titles: Record<string, string> = {
|
||||
'all': '全部订单',
|
||||
'pending': '待支付订单',
|
||||
'shipped': '待收货订单',
|
||||
'review': '待评价订单'
|
||||
}
|
||||
const title = titles[this.currentOrderTab]
|
||||
return title != null ? title : '我的订单'
|
||||
if (this.currentOrderTab === 'all') return '全部订单'
|
||||
if (this.currentOrderTab === 'pending') return '待支付订单'
|
||||
if (this.currentOrderTab === 'shipped') return '待收货订单'
|
||||
if (this.currentOrderTab === 'review') return '待评价订单'
|
||||
return '我的订单'
|
||||
},
|
||||
|
||||
initPage() {
|
||||
@@ -448,16 +491,16 @@ export default {
|
||||
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
|
||||
const profileObj = profile as UTSJSONObject
|
||||
uId = (profileObj.getString('user_id') ?? '') as string
|
||||
uPhone = (profileObj.getString('phone') ?? '') as string
|
||||
uEmail = (profileObj.getString('email') ?? '') as string
|
||||
uNickname = (profileObj.getString('nickname') ?? '') as string
|
||||
uAvatar = (profileObj.getString('avatar_url') ?? '') as string
|
||||
uGender = (profileObj.getNumber('gender') ?? 0) as number
|
||||
}
|
||||
|
||||
// 如果昵称为空,使用手机号脱敏显示
|
||||
if (!uNickname && uPhone) {
|
||||
if (uNickname === '' && uPhone !== '') {
|
||||
uNickname = uPhone.substring(0, 3) + '****' + uPhone.substring(7)
|
||||
}
|
||||
|
||||
@@ -510,35 +553,35 @@ export default {
|
||||
},
|
||||
|
||||
loadConsumptionStats() {
|
||||
// 模拟加载消费统计数据
|
||||
const statsData: Record<string, ConsumptionStatsType> = {
|
||||
month: {
|
||||
if (this.activeStatsPeriod === 'month') {
|
||||
this.currentStats = {
|
||||
total_amount: 1280.50,
|
||||
order_count: 8,
|
||||
avg_amount: 160.06,
|
||||
save_amount: 85.20
|
||||
},
|
||||
quarter: {
|
||||
} as ConsumptionStatsType
|
||||
} else if (this.activeStatsPeriod === 'quarter') {
|
||||
this.currentStats = {
|
||||
total_amount: 3680.80,
|
||||
order_count: 18,
|
||||
avg_amount: 204.49,
|
||||
save_amount: 256.30
|
||||
},
|
||||
year: {
|
||||
} as ConsumptionStatsType
|
||||
} else if (this.activeStatsPeriod === 'year') {
|
||||
this.currentStats = {
|
||||
total_amount: 15680.90,
|
||||
order_count: 56,
|
||||
avg_amount: 280.02,
|
||||
save_amount: 986.50
|
||||
},
|
||||
all: {
|
||||
} as ConsumptionStatsType
|
||||
} else {
|
||||
this.currentStats = {
|
||||
total_amount: 25680.50,
|
||||
order_count: 89,
|
||||
avg_amount: 288.55,
|
||||
save_amount: 1580.20
|
||||
}
|
||||
} as ConsumptionStatsType
|
||||
}
|
||||
|
||||
this.currentStats = statsData[this.activeStatsPeriod]
|
||||
},
|
||||
|
||||
refreshData() {
|
||||
@@ -584,26 +627,29 @@ export default {
|
||||
},
|
||||
|
||||
getOrderMainImage(order: any): string {
|
||||
// 尝试从 ml_order_items 获取第一张图
|
||||
const items = order['ml_order_items'] as any[]
|
||||
if (items != null && items.length > 0) {
|
||||
const firstItem = items[0]
|
||||
// 数据库字段通常是 image_url
|
||||
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
|
||||
const orderObj = order as UTSJSONObject
|
||||
const itemsRaw = orderObj.get('ml_order_items')
|
||||
if (itemsRaw == null) return '/static/product1.jpg'
|
||||
const items = itemsRaw as any[]
|
||||
if (items.length > 0) {
|
||||
const firstItem = items[0] as UTSJSONObject
|
||||
const imgUrl = firstItem.getString('image_url') ?? ''
|
||||
const prodImg = firstItem.getString('product_image') ?? ''
|
||||
const img = imgUrl !== '' ? imgUrl : prodImg
|
||||
if (img !== '') return img
|
||||
}
|
||||
return '/static/product1.jpg'
|
||||
},
|
||||
|
||||
getOrderTitle(order: any): string {
|
||||
const items = order['ml_order_items'] as any[]
|
||||
if (items != null && items.length > 0) {
|
||||
const firstItem = items[0]
|
||||
const pName = firstItem['product_name'] as string
|
||||
const name = (pName != null && pName != '') ? pName : '商品'
|
||||
const orderObj = order as UTSJSONObject
|
||||
const itemsRaw = orderObj.get('ml_order_items')
|
||||
if (itemsRaw == null) return '精选商品'
|
||||
const items = itemsRaw as any[]
|
||||
if (items.length > 0) {
|
||||
const firstItem = items[0] as UTSJSONObject
|
||||
const pName = firstItem.getString('product_name') ?? ''
|
||||
const name = pName !== '' ? pName : '商品'
|
||||
|
||||
if (items.length > 1) {
|
||||
return `${name} 等${items.length}件商品`
|
||||
@@ -780,19 +826,18 @@ export default {
|
||||
})
|
||||
},
|
||||
|
||||
// 处理订单更新事件
|
||||
handleOrderUpdated(data: any) {
|
||||
// 当收到订单更新事件时,刷新订单数据
|
||||
console.log('收到订单更新事件:', data)
|
||||
this.refreshData()
|
||||
|
||||
// 显示提示
|
||||
if (data.status === 1) {
|
||||
const dataObj = data as UTSJSONObject
|
||||
const status = dataObj.getNumber('status')
|
||||
if (status === 1) {
|
||||
uni.showToast({
|
||||
title: '订单已保存到待支付',
|
||||
icon: 'success'
|
||||
})
|
||||
} else if (data.status === 2) {
|
||||
} else if (status === 2) {
|
||||
uni.showToast({
|
||||
title: '支付成功,订单待发货',
|
||||
icon: 'success'
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
|
||||
<scroll-view v-else class="packet-list" scroll-y>
|
||||
<scroll-view v-else class="packet-list" direction="vertical">
|
||||
<view v-if="filteredPackets.length === 0" class="empty-state">
|
||||
<text class="empty-text">暂无相关红包</text>
|
||||
</view>
|
||||
@@ -55,58 +55,45 @@ type RedPacket = {
|
||||
|
||||
const loading = ref(true)
|
||||
const currentTab = ref(0)
|
||||
const packets = ref<RedPacket[]>([])
|
||||
const packets = ref<Array<RedPacket>>([])
|
||||
|
||||
const filteredPackets = computed((): RedPacket[] => {
|
||||
const filteredPackets = computed((): Array<RedPacket> => {
|
||||
const result: Array<RedPacket> = []
|
||||
if (currentTab.value === 0) {
|
||||
return packets.value.filter((p:RedPacket):boolean => p.status === 0)
|
||||
for (let i: number = 0; i < packets.value.length; i++) {
|
||||
if (packets.value[i].status === 0) {
|
||||
result.push(packets.value[i])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return packets.value.filter((p:RedPacket):boolean => p.status !== 0)
|
||||
for (let i: number = 0; i < packets.value.length; i++) {
|
||||
if (packets.value[i].status !== 0) {
|
||||
result.push(packets.value[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
return result
|
||||
})
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const rawList = await supabaseService.getUserRedPackets()
|
||||
packets.value = rawList.map((item: any): RedPacket => {
|
||||
let id = ''
|
||||
let amount = 0
|
||||
let name = ''
|
||||
let status = 0
|
||||
let expireAt = ''
|
||||
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') ?? ''
|
||||
} 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) ?? ''
|
||||
}
|
||||
|
||||
return {
|
||||
id: id,
|
||||
const mappedList: Array<RedPacket> = []
|
||||
for (let i: number = 0; i < rawList.length; i++) {
|
||||
const item = rawList[i] as UTSJSONObject
|
||||
const packet: RedPacket = {
|
||||
id: item.getString('id') ?? '',
|
||||
user_id: '',
|
||||
amount: amount,
|
||||
name: name,
|
||||
status: status,
|
||||
expire_at: expireAt,
|
||||
created_at: createdAt
|
||||
amount: item.getNumber('amount') ?? 0,
|
||||
name: item.getString('name') ?? '',
|
||||
status: item.getNumber('status') ?? 0,
|
||||
expire_at: item.getString('expire_at') ?? '',
|
||||
created_at: item.getString('created_at') ?? ''
|
||||
} as RedPacket
|
||||
})
|
||||
mappedList.push(packet)
|
||||
}
|
||||
packets.value = mappedList
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
@@ -114,6 +101,10 @@ const loadData = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
const usePacket = (item: RedPacket) => {
|
||||
uni.switchTab({
|
||||
url: '/pages/mall/consumer/index'
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
</view>
|
||||
|
||||
<!-- 售后列表 -->
|
||||
<scroll-view class="refund-content" scroll-y @scrolltolower="loadMore">
|
||||
<scroll-view class="refund-content" direction="vertical" @scrolltolower="loadMore">
|
||||
<!-- 空状态 -->
|
||||
<view v-if="refunds.length === 0 && !isLoading" class="empty-refunds">
|
||||
<text class="empty-icon">🔄</text>
|
||||
@@ -58,7 +58,7 @@
|
||||
</view>
|
||||
|
||||
<!-- 进度时间线 -->
|
||||
<view v-if="refund.status_history?.length > 0" class="timeline">
|
||||
<view v-if="refund.status_history != null && refund.status_history.length > 0" class="timeline">
|
||||
<view v-for="(step, index) in getTimelineSteps(refund)"
|
||||
:key="index"
|
||||
class="timeline-step">
|
||||
@@ -116,7 +116,7 @@ type RefundProductInfo = {
|
||||
type RefundOrderItem = {
|
||||
id: string
|
||||
product_name: string
|
||||
sku_specifications: any
|
||||
sku_specifications: any | null
|
||||
price: number
|
||||
quantity: number
|
||||
product?: RefundProductInfo
|
||||
@@ -157,27 +157,17 @@ const currentPage = ref<number>(1)
|
||||
const pageSize = ref<number>(15)
|
||||
const hasMore = ref<boolean>(true)
|
||||
|
||||
// 监听标签页变化
|
||||
watch(activeTab, () => {
|
||||
resetData()
|
||||
loadRefunds()
|
||||
})
|
||||
const getCurrentUserId = (): string => {
|
||||
return supabaseService.getCurrentUserId() ?? ''
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadRefunds()
|
||||
loadTabCounts()
|
||||
})
|
||||
|
||||
// 重置数据
|
||||
const resetData = () => {
|
||||
refunds.value = []
|
||||
currentPage.value = 1
|
||||
hasMore.value = true
|
||||
}
|
||||
|
||||
// 加载售后数据
|
||||
const loadRefunds = async (loadMore: boolean = false) => {
|
||||
const loadRefunds = async (loadMore: boolean): Promise<void> => {
|
||||
if (isLoading.value || (!hasMore.value && loadMore)) {
|
||||
return
|
||||
}
|
||||
@@ -197,46 +187,66 @@ const loadRefunds = async (loadMore: boolean = false) => {
|
||||
|
||||
let statusList: number[] = []
|
||||
if (activeTab.value === 'processing') {
|
||||
statusList = [1, 2] // 待处理和处理中
|
||||
statusList = [1, 2]
|
||||
} else if (activeTab.value === 'completed') {
|
||||
statusList = [3, 4, 5] // 已完成、已取消、已拒绝
|
||||
statusList = [3, 4, 5]
|
||||
}
|
||||
|
||||
const rawData = await supabaseService.getRefunds(statusList, page, pageSize.value)
|
||||
|
||||
// Map data to UI structure (RefundType)
|
||||
const newRefunds = rawData.map((item: any): RefundType => {
|
||||
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'] }
|
||||
}))
|
||||
const newRefunds: Array<RefundType> = []
|
||||
for (let i: number = 0; i < rawData.length; i++) {
|
||||
const item = rawData[i] as UTSJSONObject
|
||||
const orderObjRaw = item.get('order')
|
||||
const orderObj = (orderObjRaw != null) ? (orderObjRaw as UTSJSONObject) : (new UTSJSONObject())
|
||||
const dbItemsRaw = orderObj.get('ml_order_items')
|
||||
const dbItems = (dbItemsRaw != null) ? (dbItemsRaw as any[]) : []
|
||||
|
||||
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'],
|
||||
// Handle missing timeline by defaulting or leaving empty
|
||||
status_history: (item['status_history'] as RefundStatusHistoryItem[]) ?? [],
|
||||
created_at: item.created_at,
|
||||
const uiItems: Array<RefundOrderItem> = []
|
||||
for (let j: number = 0; j < dbItems.length; j++) {
|
||||
const di = dbItems[j] as UTSJSONObject
|
||||
const imgRaw = di.get('image_url')
|
||||
const imgUrl = (imgRaw != null) ? (imgRaw as string) : '/static/default-product.png'
|
||||
const productInfo: RefundProductInfo = {
|
||||
images: [imgUrl]
|
||||
} as RefundProductInfo
|
||||
|
||||
const specRaw = di.get('specifications')
|
||||
const specifications = (specRaw != null) ? (specRaw as any) : null
|
||||
const orderItem: RefundOrderItem = {
|
||||
id: di.getString('id') ?? '',
|
||||
product_name: di.getString('product_name') ?? '',
|
||||
sku_specifications: specifications,
|
||||
price: 0,
|
||||
quantity: di.getNumber('quantity') ?? 1,
|
||||
product: productInfo
|
||||
} as RefundOrderItem
|
||||
uiItems.push(orderItem)
|
||||
}
|
||||
|
||||
const statusHistoryRaw = item.get('status_history')
|
||||
const statusHistory = (statusHistoryRaw != null) ? (statusHistoryRaw as RefundStatusHistoryItem[]) : []
|
||||
|
||||
const refundItem: RefundType = {
|
||||
id: item.getString('id') ?? '',
|
||||
user_id: item.getString('user_id') ?? '',
|
||||
order_id: item.getString('order_id') ?? '',
|
||||
refund_no: item.getString('refund_no') ?? '',
|
||||
refund_type: item.getNumber('refund_type') ?? 1,
|
||||
refund_reason: item.getString('refund_reason') ?? '',
|
||||
refund_amount: item.getNumber('refund_amount') ?? 0,
|
||||
status: item.getNumber('status') ?? 1,
|
||||
status_history: statusHistory,
|
||||
created_at: item.getString('created_at') ?? '',
|
||||
order: {
|
||||
id: item.order_id,
|
||||
order_no: orderObj.order_no,
|
||||
created_at: orderObj.created_at,
|
||||
id: item.getString('order_id') ?? '',
|
||||
order_no: orderObj.getString('order_no') ?? '',
|
||||
created_at: orderObj.getString('created_at') ?? '',
|
||||
order_items: uiItems
|
||||
}
|
||||
} as RefundOrderInfo
|
||||
} as RefundType
|
||||
})
|
||||
newRefunds.push(refundItem)
|
||||
}
|
||||
|
||||
if (loadMore) {
|
||||
refunds.value.push(...newRefunds)
|
||||
@@ -254,57 +264,44 @@ const loadRefunds = async (loadMore: boolean = false) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 加载标签页计数
|
||||
const loadTabCounts = async () => {
|
||||
const userId = getCurrentUserId()
|
||||
if (userId == '') return
|
||||
|
||||
try {
|
||||
const { count, error } = await supa
|
||||
.from('refunds')
|
||||
.select('*', { count: 'exact' })
|
||||
.eq('user_id', userId)
|
||||
.in('status', [1, 2])
|
||||
|
||||
if (error !== null) {
|
||||
console.error('加载计数失败:', error)
|
||||
return
|
||||
}
|
||||
|
||||
tabCounts.value.processing = count ?? 0
|
||||
const processingRefunds = await supabaseService.getRefunds([1, 2], 1, 100)
|
||||
tabCounts.value.processing = processingRefunds.length
|
||||
} catch (err) {
|
||||
console.error('加载计数异常:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前用户ID
|
||||
const getCurrentUserId = (): string => {
|
||||
const userStore = uni.getStorageSync('userInfo')
|
||||
return userStore['id'] ?? ''
|
||||
}
|
||||
watch(activeTab, () => {
|
||||
resetData()
|
||||
loadRefunds(false)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
loadRefunds(false)
|
||||
loadTabCounts()
|
||||
})
|
||||
|
||||
// 获取状态文本
|
||||
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-processing',
|
||||
3: 'status-completed',
|
||||
4: 'status-cancelled',
|
||||
5: 'status-rejected'
|
||||
}
|
||||
return classMap[status] ?? 'status-unknown'
|
||||
if (status === 1) return 'status-pending'
|
||||
if (status === 2) return 'status-processing'
|
||||
if (status === 3) return 'status-completed'
|
||||
if (status === 4) return 'status-cancelled'
|
||||
if (status === 5) return 'status-rejected'
|
||||
return 'status-unknown'
|
||||
}
|
||||
|
||||
// 获取商品图片
|
||||
@@ -337,45 +334,58 @@ const formatTime = (timeStr?: string): string => {
|
||||
return `${month}-${day}`
|
||||
}
|
||||
|
||||
// 获取时间线步骤
|
||||
const getTimelineSteps = (refund: RefundType): Array<any> => {
|
||||
const steps = [
|
||||
{ status: 0, title: '提交申请', time: refund.created_at },
|
||||
{ status: 1, title: '商家处理', time: '' },
|
||||
{ status: 3, title: '退款完成', time: '' }
|
||||
]
|
||||
|
||||
// 如果有状态历史,更新时间和描述
|
||||
if (refund.status_history) {
|
||||
refund.status_history.forEach(history => {
|
||||
if (history.status === 1 || history.status === 2) {
|
||||
steps[1].time = history.created_at
|
||||
steps[1].desc = history.remark
|
||||
} else if (history.status === 3) {
|
||||
steps[2].time = history.created_at
|
||||
steps[2].desc = history.remark
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 标记当前状态
|
||||
return steps.map((step, index) => ({
|
||||
...step,
|
||||
active: index === getCurrentStepIndex(refund.status),
|
||||
completed: index < getCurrentStepIndex(refund.status)
|
||||
}))
|
||||
const getCurrentStepIndex = (status: number): number => {
|
||||
if (status === 1) return 0
|
||||
if (status === 2) return 1
|
||||
if (status === 3) return 2
|
||||
if (status === 4) return 0
|
||||
if (status === 5) return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
// 获取当前步骤索引
|
||||
const getCurrentStepIndex = (status: number): number => {
|
||||
switch (status) {
|
||||
case 1: return 0 // 待处理
|
||||
case 2: return 1 // 处理中
|
||||
case 3: return 2 // 已完成
|
||||
case 4: return 0 // 已取消
|
||||
case 5: return 1 // 已拒绝
|
||||
default: return 0
|
||||
type TimelineStepType = {
|
||||
status: number,
|
||||
title: string,
|
||||
time: string,
|
||||
active: boolean,
|
||||
completed: boolean,
|
||||
desc: string
|
||||
}
|
||||
|
||||
const getTimelineSteps = (refund: RefundType): Array<TimelineStepType> => {
|
||||
const steps: Array<TimelineStepType> = [
|
||||
{ status: 0, title: '提交申请', time: refund.created_at, active: false, completed: false, desc: '' },
|
||||
{ status: 1, title: '商家处理', time: '', active: false, completed: false, desc: '' },
|
||||
{ status: 3, title: '退款完成', time: '', active: false, completed: false, desc: '' }
|
||||
]
|
||||
|
||||
if (refund.status_history != null) {
|
||||
for (let i: number = 0; i < refund.status_history.length; i++) {
|
||||
const history = refund.status_history[i]
|
||||
if (history.status === 1 || history.status === 2) {
|
||||
steps[1].time = history.created_at ?? ''
|
||||
steps[1].desc = history.remark ?? ''
|
||||
} else if (history.status === 3) {
|
||||
steps[2].time = history.created_at ?? ''
|
||||
steps[2].desc = history.remark ?? ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const currentStepIndex = getCurrentStepIndex(refund.status)
|
||||
const result: Array<TimelineStepType> = []
|
||||
for (let i: number = 0; i < steps.length; i++) {
|
||||
const step = steps[i]
|
||||
result.push({
|
||||
status: step.status,
|
||||
title: step.title,
|
||||
time: step.time,
|
||||
desc: step.desc,
|
||||
active: i === currentStepIndex,
|
||||
completed: i < currentStepIndex
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// 切换标签页
|
||||
@@ -397,45 +407,42 @@ const viewOrder = (orderId: string) => {
|
||||
})
|
||||
}
|
||||
|
||||
// 取消退款申请
|
||||
const doCancelRefund = async (refund: RefundType) => {
|
||||
try {
|
||||
const result = await supabaseService.createRefund({
|
||||
id: refund.id,
|
||||
status: 4
|
||||
} as any)
|
||||
|
||||
if (result.success) {
|
||||
refund.status = 4
|
||||
loadTabCounts()
|
||||
uni.showToast({
|
||||
title: '已取消',
|
||||
icon: 'success'
|
||||
})
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '取消失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('取消退款失败:', err)
|
||||
uni.showToast({
|
||||
title: '取消失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const cancelRefund = (refund: RefundType) => {
|
||||
uni.showModal({
|
||||
title: '取消申请',
|
||||
content: '确定要取消这个退款申请吗?',
|
||||
success: async (res) => {
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
const { error } = await supa
|
||||
.from('refunds')
|
||||
.update({
|
||||
status: 4, // 已取消
|
||||
status_history: [...(refund.status_history ?? []), {
|
||||
status: 4,
|
||||
remark: '用户取消申请',
|
||||
created_at: new Date().toISOString()
|
||||
}]
|
||||
})
|
||||
.eq('id', refund.id)
|
||||
|
||||
if (error !== null) {
|
||||
throw error
|
||||
}
|
||||
|
||||
refund.status = 4
|
||||
loadTabCounts() // 重新加载计数
|
||||
|
||||
uni.showToast({
|
||||
title: '已取消',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
} catch (err) {
|
||||
console.error('取消退款失败:', err)
|
||||
uni.showToast({
|
||||
title: '取消失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
doCancelRefund(refund)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -455,41 +462,45 @@ const reviewRefund = (refund: RefundType) => {
|
||||
})
|
||||
}
|
||||
|
||||
// 删除记录
|
||||
const doDeleteRefund = async (refund: RefundType) => {
|
||||
try {
|
||||
const result = await supabaseService.deleteRefund(refund.id)
|
||||
|
||||
if (result) {
|
||||
const newRefunds: Array<RefundType> = []
|
||||
for (let i: number = 0; i < refunds.value.length; i++) {
|
||||
if (refunds.value[i].id !== refund.id) {
|
||||
newRefunds.push(refunds.value[i])
|
||||
}
|
||||
}
|
||||
refunds.value = newRefunds
|
||||
|
||||
uni.showToast({
|
||||
title: '删除成功',
|
||||
icon: 'success'
|
||||
})
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '删除失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('删除记录失败:', err)
|
||||
uni.showToast({
|
||||
title: '删除失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const deleteRefund = (refund: RefundType) => {
|
||||
uni.showModal({
|
||||
title: '删除记录',
|
||||
content: '确定要删除这个售后记录吗?',
|
||||
success: async (res) => {
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
const { error } = await supa
|
||||
.from('refunds')
|
||||
.delete()
|
||||
.eq('id', refund.id)
|
||||
|
||||
if (error !== null) {
|
||||
throw error
|
||||
}
|
||||
|
||||
const index = refunds.value.findIndex(r => r.id === refund.id)
|
||||
if (index !== -1) {
|
||||
refunds.value.splice(index, 1)
|
||||
refunds.value = [...refunds.value]
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: '删除成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
} catch (err) {
|
||||
console.error('删除记录失败:', err)
|
||||
uni.showToast({
|
||||
title: '删除失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
doDeleteRefund(refund)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
<text class="header-title">评价商品</text>
|
||||
</view>
|
||||
|
||||
<scroll-view class="review-content" scroll-y>
|
||||
<scroll-view class="review-content" direction="vertical">
|
||||
<!-- 订单信息 -->
|
||||
<view class="order-section">
|
||||
<text class="order-no">订单号: {{ order?.order_no }}</text>
|
||||
<text class="order-time">下单时间: {{ formatTime(order?.created_at) }}</text>
|
||||
<text class="order-no">订单号: {{ order != null ? order.order_no : '' }}</text>
|
||||
<text class="order-time">下单时间: {{ formatTime(order != null ? order.created_at : '') }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 商品评价 -->
|
||||
@@ -21,7 +21,7 @@
|
||||
<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>
|
||||
<text v-if="item.sku_specifications != null" class="product-spec">{{ getSpecText(item.sku_specifications) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -133,9 +133,9 @@
|
||||
<!-- 提交按钮 -->
|
||||
<view class="submit-section">
|
||||
<button class="submit-btn"
|
||||
:class="{ disabled: !canSubmit || isSubmitting }"
|
||||
:class="{ disabled: canSubmit === false || isSubmitting }"
|
||||
@click="submitReview">
|
||||
<text v-if="!isSubmitting" class="submit-text">提交评价</text>
|
||||
<text v-if="isSubmitting === false" class="submit-text">提交评价</text>
|
||||
<text v-else class="submit-text">提交中...</text>
|
||||
</button>
|
||||
</view>
|
||||
@@ -146,17 +146,32 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
import { supabaseService } from '@/utils/supabaseService.uts'
|
||||
|
||||
type OrderType = {
|
||||
id: string
|
||||
order_no: string
|
||||
created_at: string
|
||||
merchant_id: string
|
||||
}
|
||||
|
||||
type OrderItemType = {
|
||||
id: string
|
||||
product_id: string
|
||||
id: number
|
||||
order_id: number
|
||||
product_id: number
|
||||
product_name: string
|
||||
product_image: string
|
||||
sku_specifications: any
|
||||
sku_specifications: any | null
|
||||
price: number
|
||||
quantity: number
|
||||
}
|
||||
|
||||
type MerchantRatingType = {
|
||||
description: number
|
||||
logistics: number
|
||||
service: number
|
||||
}
|
||||
|
||||
type MerchantType = {
|
||||
id: string
|
||||
shop_name: string
|
||||
@@ -164,85 +179,113 @@ type MerchantType = {
|
||||
}
|
||||
|
||||
const orderId = ref<string>('')
|
||||
const order = ref<any>({})
|
||||
const order = ref<OrderType | null>(null)
|
||||
const orderItems = ref<Array<OrderItemType>>([])
|
||||
const merchant = ref<MerchantType | null>(null)
|
||||
const ratings = ref<Array<number>>([])
|
||||
const contents = ref<Array<string>>([])
|
||||
const images = ref<Array<Array<string>>>([])
|
||||
const anonymous = ref<boolean>(false)
|
||||
const merchantRating = ref({
|
||||
const merchantRating = ref<MerchantRatingType>({
|
||||
description: 5,
|
||||
logistics: 5,
|
||||
service: 5
|
||||
})
|
||||
} as MerchantRatingType)
|
||||
const isSubmitting = ref<boolean>(false)
|
||||
|
||||
// 计算属性
|
||||
const canSubmit = computed(() => {
|
||||
// 检查是否所有商品都已评分
|
||||
if (ratings.value.length === 0) return false
|
||||
return ratings.value.every(rating => rating > 0)
|
||||
})
|
||||
|
||||
// 生命周期
|
||||
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()
|
||||
})
|
||||
|
||||
// 加载订单数据
|
||||
const loadOrderData = async () => {
|
||||
const loadOrderData = async (): Promise<void> => {
|
||||
try {
|
||||
const { data: orderData, error: orderError } = await supa
|
||||
const orderRes = await supa
|
||||
.from('ml_orders')
|
||||
.select('*')
|
||||
.eq('id', orderId.value)
|
||||
.single()
|
||||
.execute()
|
||||
|
||||
if (orderError !== null) {
|
||||
console.error('加载订单失败:', orderError)
|
||||
if (orderRes.error != null) {
|
||||
console.error('加载订单失败:', orderRes.error)
|
||||
return
|
||||
}
|
||||
|
||||
order.value = orderData
|
||||
if (orderRes.data != null) {
|
||||
const orderData = orderRes.data as UTSJSONObject
|
||||
order.value = {
|
||||
id: orderData.getString('id') ?? '',
|
||||
order_no: orderData.getString('order_no') ?? '',
|
||||
created_at: orderData.getString('created_at') ?? '',
|
||||
merchant_id: orderData.getString('merchant_id') ?? ''
|
||||
} as OrderType
|
||||
}
|
||||
|
||||
// 加载订单商品
|
||||
const { data: itemsData, error: itemsError } = await supa
|
||||
const itemsRes = await supa
|
||||
.from('ml_order_items')
|
||||
.select(`
|
||||
*,
|
||||
product:product_id(images)
|
||||
`)
|
||||
.eq('order_id', orderId.value)
|
||||
.execute()
|
||||
|
||||
if (itemsError !== null) {
|
||||
console.error('加载订单商品失败:', itemsError)
|
||||
if (itemsRes.error != null) {
|
||||
console.error('加载订单商品失败:', itemsRes.error)
|
||||
return
|
||||
}
|
||||
|
||||
orderItems.value = (itemsData ?? []).map((item: any) => ({
|
||||
...item,
|
||||
product_image: item.product?.images?.[0] ?? '/static/default-product.png'
|
||||
}))
|
||||
const rawData = itemsRes.data
|
||||
let itemsArray: Array<any> = []
|
||||
if (rawData != null) {
|
||||
itemsArray = rawData as Array<any>
|
||||
}
|
||||
|
||||
const processedItems: Array<OrderItemType> = []
|
||||
for (let i: number = 0; i < itemsArray.length; i++) {
|
||||
const item = itemsArray[i] as UTSJSONObject
|
||||
const productObjRaw = item.get('product')
|
||||
const productObj = (productObjRaw != null) ? (productObjRaw as UTSJSONObject) : null
|
||||
const imagesArrRaw = (productObj != null) ? productObj.get('images') : null
|
||||
const imagesArr = (imagesArrRaw != null) ? (imagesArrRaw as Array<string>) : []
|
||||
const firstImage = (imagesArr.length > 0) ? imagesArr[0] : '/static/default-product.png'
|
||||
const skuSpecRaw = item.get('sku_specifications')
|
||||
const skuSpec = (skuSpecRaw != null) ? (skuSpecRaw as any) : null
|
||||
const processedItem: OrderItemType = {
|
||||
id: (item.getNumber('id') ?? 0) as number,
|
||||
order_id: (item.getNumber('order_id') ?? 0) as number,
|
||||
product_id: (item.getNumber('product_id') ?? 0) as number,
|
||||
product_name: item.getString('product_name') ?? '',
|
||||
price: (item.getNumber('price') ?? 0) as number,
|
||||
quantity: (item.getNumber('quantity') ?? 1) as number,
|
||||
sku_specifications: skuSpec,
|
||||
product_image: firstImage
|
||||
}
|
||||
processedItems.push(processedItem)
|
||||
}
|
||||
orderItems.value = processedItems
|
||||
|
||||
// 初始化评分和内容数组
|
||||
const count = orderItems.value.length
|
||||
ratings.value = new Array(count).fill(5)
|
||||
contents.value = new Array(count).fill('')
|
||||
images.value = new Array(count).fill([])
|
||||
const newRatings: Array<number> = []
|
||||
const newContents: Array<string> = []
|
||||
const newImages: Array<Array<string>> = []
|
||||
for (let i: number = 0; i < count; i++) {
|
||||
newRatings.push(5)
|
||||
newContents.push('')
|
||||
newImages.push([])
|
||||
}
|
||||
ratings.value = newRatings
|
||||
contents.value = newContents
|
||||
images.value = newImages
|
||||
|
||||
// 加载商家信息
|
||||
if (order.value.merchant_id) {
|
||||
const { data: merchantData, error: merchantError } = await supa
|
||||
const orderObj = order.value as UTSJSONObject
|
||||
const merchantId = orderObj.getString('merchant_id')
|
||||
if (merchantId != null && merchantId !== '') {
|
||||
const merchantRes = await supa
|
||||
.from('ml_shops')
|
||||
.select('id, shop_name, rating')
|
||||
.eq('id', order.value.merchant_id)
|
||||
.eq('id', merchantId)
|
||||
.single()
|
||||
.execute()
|
||||
|
||||
if (merchantError == null) {
|
||||
merchant.value = merchantData
|
||||
if (merchantRes.error == null && merchantRes.data != null) {
|
||||
merchant.value = merchantRes.data as MerchantType
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,6 +294,22 @@ const loadOrderData = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const canSubmit = computed((): boolean => {
|
||||
if (ratings.value.length === 0) return false
|
||||
for (let i: number = 0; i < ratings.value.length; i++) {
|
||||
if (ratings.value[i] <= 0) return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
onLoad((options: any) => {
|
||||
if (options != null) {
|
||||
const optObj = options as UTSJSONObject
|
||||
orderId.value = optObj.getString('orderId') ?? ''
|
||||
if (orderId.value != '') loadOrderData()
|
||||
}
|
||||
})
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (timeStr?: string): string => {
|
||||
if (timeStr == null) return ''
|
||||
@@ -261,38 +320,52 @@ const formatTime = (timeStr?: string): string => {
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
|
||||
// 获取规格文本
|
||||
const getSpecText = (specs: any): string => {
|
||||
const getSpecText = (specs: any | null): string => {
|
||||
if (specs == null) return ''
|
||||
if (typeof specs === 'object') {
|
||||
return Object.keys(specs)
|
||||
.map(key => `${key}: ${specs[key]}`)
|
||||
.join('; ')
|
||||
if (specs instanceof UTSJSONObject) {
|
||||
return '规格信息'
|
||||
}
|
||||
return String(specs)
|
||||
return specs as string
|
||||
}
|
||||
|
||||
// 获取评分文本
|
||||
const getRatingText = (rating: number): string => {
|
||||
const texts = ['非常差', '差', '一般', '好', '非常好']
|
||||
return texts[rating - 1] ?? '未评价'
|
||||
if (rating === 1) return '非常差'
|
||||
if (rating === 2) return '差'
|
||||
if (rating === 3) return '一般'
|
||||
if (rating === 4) return '好'
|
||||
if (rating === 5) return '非常好'
|
||||
return '未评价'
|
||||
}
|
||||
|
||||
// 设置商品评分
|
||||
const setRating = (index: number, rating: number) => {
|
||||
ratings.value[index] = rating
|
||||
ratings.value = [...ratings.value]
|
||||
// 触发响应式更新
|
||||
const newRatings: number[] = []
|
||||
for (let i: number = 0; i < ratings.value.length; i++) {
|
||||
newRatings.push(ratings.value[i])
|
||||
}
|
||||
ratings.value = newRatings
|
||||
}
|
||||
|
||||
// 设置商家评分
|
||||
const setMerchantRating = (type: keyof typeof merchantRating.value, rating: number) => {
|
||||
merchantRating.value[type] = rating
|
||||
merchantRating.value = { ...merchantRating.value }
|
||||
const setMerchantRating = (type: string, rating: number) => {
|
||||
if (type === 'description') {
|
||||
merchantRating.value.description = rating
|
||||
} else if (type === 'logistics') {
|
||||
merchantRating.value.logistics = rating
|
||||
} else if (type === 'service') {
|
||||
merchantRating.value.service = rating
|
||||
}
|
||||
}
|
||||
|
||||
// 切换匿名
|
||||
const toggleAnonymous = (event: any) => {
|
||||
anonymous.value = event.detail.value
|
||||
const eventObj = event as UTSJSONObject
|
||||
const detailRaw = eventObj.get('detail')
|
||||
const detail = detailRaw != null ? (detailRaw as UTSJSONObject) : (new UTSJSONObject())
|
||||
const valueRaw = detail.get('value')
|
||||
anonymous.value = valueRaw != null ? (valueRaw as boolean) : false
|
||||
}
|
||||
|
||||
// 上传图片
|
||||
@@ -312,17 +385,27 @@ const uploadImage = async (index: number) => {
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: (res) => {
|
||||
const tempFiles = res.tempFilePaths
|
||||
const resObj = res as UTSJSONObject
|
||||
const tempFilesRaw = resObj.get('tempFilePaths')
|
||||
const tempFiles = tempFilesRaw != null ? (tempFilesRaw as Array<string>) : []
|
||||
|
||||
// 模拟上传过程
|
||||
uni.showLoading({
|
||||
title: '上传中...'
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
// 这里应该调用真实的上传接口
|
||||
images.value[index].push(...tempFiles)
|
||||
images.value = [...images.value]
|
||||
for (let i: number = 0; i < tempFiles.length; i++) {
|
||||
images.value[index].push(tempFiles[i])
|
||||
}
|
||||
const newImages: Array<Array<string>> = []
|
||||
for (let i: number = 0; i < images.value.length; i++) {
|
||||
const innerArray: Array<string> = []
|
||||
for (let j: number = 0; j < images.value[i].length; j++) {
|
||||
innerArray.push(images.value[i][j])
|
||||
}
|
||||
newImages.push(innerArray)
|
||||
}
|
||||
images.value = newImages
|
||||
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
@@ -337,12 +420,28 @@ const uploadImage = async (index: number) => {
|
||||
// 删除图片
|
||||
const deleteImage = (index: number, imgIndex: number) => {
|
||||
images.value[index].splice(imgIndex, 1)
|
||||
images.value = [...images.value]
|
||||
// 触发响应式更新
|
||||
const newImages: string[][] = []
|
||||
for (let i: number = 0; i < images.value.length; i++) {
|
||||
const innerArray: string[] = []
|
||||
for (let j: number = 0; j < images.value[i].length; j++) {
|
||||
innerArray.push(images.value[i][j])
|
||||
}
|
||||
newImages.push(innerArray)
|
||||
}
|
||||
images.value = newImages
|
||||
}
|
||||
|
||||
// 提交评价
|
||||
const submitReview = async () => {
|
||||
if (!canSubmit.value || isSubmitting.value) return
|
||||
// 获取当前用户ID
|
||||
const getCurrentUserId = (): string => {
|
||||
const userStore = uni.getStorageSync('userInfo')
|
||||
if (userStore == null) return ''
|
||||
const userInfo = userStore as UTSJSONObject
|
||||
return userInfo.getString('id') ?? ''
|
||||
}
|
||||
|
||||
const submitReview = async (): Promise<void> => {
|
||||
if (canSubmit.value === false || isSubmitting.value) return
|
||||
|
||||
isSubmitting.value = true
|
||||
|
||||
@@ -356,57 +455,61 @@ const submitReview = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
// 提交商品评价
|
||||
const productReviews = orderItems.value.map((item, index) => ({
|
||||
user_id: userId,
|
||||
product_id: item.product_id,
|
||||
order_id: orderId.value,
|
||||
rating: ratings.value[index],
|
||||
content: contents.value[index] != '' ? contents.value[index] : '',
|
||||
images: images.value[index],
|
||||
is_anonymous: anonymous.value
|
||||
}))
|
||||
|
||||
const { error: reviewsError } = await supa
|
||||
.from('ml_product_reviews')
|
||||
.insert(productReviews)
|
||||
|
||||
if (reviewsError !== null) {
|
||||
throw reviewsError
|
||||
type ProductReviewType = {
|
||||
user_id: string,
|
||||
product_id: number,
|
||||
order_id: string,
|
||||
rating: number,
|
||||
content: string,
|
||||
images: Array<string>,
|
||||
is_anonymous: boolean
|
||||
}
|
||||
const productReviews: Array<UTSJSONObject> = []
|
||||
for (let index: number = 0; index < orderItems.value.length; index++) {
|
||||
const item = orderItems.value[index]
|
||||
const reviewObj: UTSJSONObject = new UTSJSONObject()
|
||||
reviewObj.set('user_id', userId)
|
||||
reviewObj.set('product_id', item.product_id)
|
||||
reviewObj.set('order_id', orderId.value)
|
||||
reviewObj.set('rating', ratings.value[index])
|
||||
reviewObj.set('content', contents.value[index] != '' ? contents.value[index] : '')
|
||||
reviewObj.set('images', images.value[index])
|
||||
reviewObj.set('is_anonymous', anonymous.value)
|
||||
productReviews.push(reviewObj)
|
||||
}
|
||||
|
||||
// 提交店铺评价
|
||||
if (merchant.value) {
|
||||
const merchantReview = {
|
||||
user_id: userId,
|
||||
shop_id: merchant.value.id,
|
||||
order_id: orderId.value,
|
||||
description_rating: merchantRating.value.description,
|
||||
logistics_rating: merchantRating.value.logistics,
|
||||
service_rating: merchantRating.value.service
|
||||
const reviewsSuccess = await supabaseService.submitProductReviews(productReviews)
|
||||
if (reviewsSuccess == false) {
|
||||
uni.showToast({
|
||||
title: '提交失败',
|
||||
icon: 'none'
|
||||
})
|
||||
isSubmitting.value = false
|
||||
return
|
||||
}
|
||||
|
||||
if (merchant.value != null) {
|
||||
type MerchantReviewType = {
|
||||
user_id: string,
|
||||
shop_id: string,
|
||||
order_id: string,
|
||||
description_rating: number,
|
||||
logistics_rating: number,
|
||||
service_rating: number
|
||||
}
|
||||
const merchantReviewObj: UTSJSONObject = new UTSJSONObject()
|
||||
merchantReviewObj.set('user_id', userId)
|
||||
merchantReviewObj.set('shop_id', merchant.value.id)
|
||||
merchantReviewObj.set('order_id', orderId.value)
|
||||
merchantReviewObj.set('description_rating', merchantRating.value.description)
|
||||
merchantReviewObj.set('logistics_rating', merchantRating.value.logistics)
|
||||
merchantReviewObj.set('service_rating', merchantRating.value.service)
|
||||
|
||||
const { error: merchantError } = await supa
|
||||
.from('ml_shop_reviews')
|
||||
.insert(merchantReview)
|
||||
|
||||
if (merchantError !== null) {
|
||||
console.error('提交店铺评价失败:', merchantError)
|
||||
}
|
||||
await supabaseService.submitShopReview(merchantReviewObj)
|
||||
}
|
||||
|
||||
// 更新订单状态为已评价 (如果需要标记为已评价,可以在这里处理,例如 status=5 implies Reviewed or keeping at 4)
|
||||
// 这里保持为 4 (Completed)
|
||||
const { error: orderError } = await supa
|
||||
.from('ml_orders')
|
||||
.update({ order_status: 4 })
|
||||
.eq('id', orderId.value)
|
||||
await supabaseService.updateOrderStatus(orderId.value, 4)
|
||||
|
||||
if (orderError !== null) {
|
||||
console.error('更新订单状态失败:', orderError)
|
||||
}
|
||||
|
||||
// 显示成功提示
|
||||
uni.showToast({
|
||||
title: '评价成功',
|
||||
icon: 'success',
|
||||
@@ -429,14 +532,8 @@ const submitReview = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前用户ID
|
||||
const getCurrentUserId = (): string => {
|
||||
const userStore = uni.getStorageSync('userInfo')
|
||||
return userStore?.getString('id') ?? ''
|
||||
}
|
||||
|
||||
// 返回
|
||||
const goBack = () => {
|
||||
const goBack = (): void => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
</script>
|
||||
@@ -503,10 +600,6 @@ const goBack = () => {
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
}
|
||||
|
||||
.product-review:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.product-header {
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
@@ -643,8 +736,6 @@ const goBack = () => {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
margin-right: 10px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.upload-btn {
|
||||
width: 70px;
|
||||
@@ -738,10 +829,6 @@ margin-right: 10px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.tip-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.submit-section {
|
||||
background-color: #ffffff;
|
||||
padding: 15px;
|
||||
|
||||
@@ -51,13 +51,13 @@
|
||||
<!-- 主内容区域 -->
|
||||
<scroll-view
|
||||
v-else
|
||||
scroll-y
|
||||
direction="vertical"
|
||||
class="main-content"
|
||||
:style="{ height: scrollHeight + 'px' }"
|
||||
@scrolltolower="loadMore"
|
||||
>
|
||||
<!-- 初始状态(无搜索词) -->
|
||||
<view v-if="!searchKeyword && !showResults">
|
||||
<view v-if="searchKeyword == '' && showResults == false">
|
||||
<!-- 搜索历史 -->
|
||||
<view v-if="searchHistory.length > 0" class="search-history">
|
||||
<view class="section-header">
|
||||
@@ -92,12 +92,12 @@
|
||||
v-for="(item, index) in hotSearchList"
|
||||
:key="index"
|
||||
class="hot-tag"
|
||||
:class="{ 'hot': item.hot }"
|
||||
:class="item.hot == true ? 'hot' : ''"
|
||||
@click="searchFromHot(item.keyword)"
|
||||
>
|
||||
<text class="hot-rank" :class="{ 'top-three': index < 3 }">{{ index + 1 }}</text>
|
||||
<text class="hot-rank" :class="index < 3 ? 'top-three' : ''">{{ index + 1 }}</text>
|
||||
<text class="hot-text">{{ item.keyword }}</text>
|
||||
<text v-if="item.hot" class="hot-icon">🔥</text>
|
||||
<text v-if="item.hot == true" class="hot-icon">🔥</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -136,7 +136,7 @@
|
||||
</view>
|
||||
|
||||
<!-- 搜索建议 -->
|
||||
<view v-if="searchKeyword && !showResults" class="search-suggestions">
|
||||
<view v-if="searchKeyword != '' && showResults == false" class="search-suggestions">
|
||||
<view class="suggestions-list">
|
||||
<view
|
||||
v-for="(suggestion, index) in searchSuggestions"
|
||||
@@ -157,7 +157,7 @@
|
||||
<view class="section-top">
|
||||
<text class="result-title-sm">相关店铺</text>
|
||||
</view>
|
||||
<scroll-view scroll-x class="shop-list-scroll">
|
||||
<scroll-view direction="horizontal" class="shop-list-scroll">
|
||||
<view class="shop-list-row">
|
||||
<view
|
||||
v-for="shop in searchShopResults"
|
||||
@@ -267,110 +267,47 @@ const autoFocus = ref(true)
|
||||
const activeSort = ref('default') // 当前排序方式: default, sales, price
|
||||
const priceSortAsc = ref(false) // 价格排序是否为升序
|
||||
|
||||
// 数据定义
|
||||
const searchHistory = ref<string[]>([])
|
||||
const hotSearchList = ref<any[]>([])
|
||||
const guessList = ref<any[]>([])
|
||||
const allGuessItems = ref<any[]>([]) // 缓存所有猜你喜欢商品
|
||||
const searchResults = ref<any[]>([])
|
||||
const searchShopResults = ref<any[]>([]) // 搜索到的店铺
|
||||
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
initPage()
|
||||
})
|
||||
|
||||
|
||||
const initPage = () => {
|
||||
try {
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
statusBarHeight.value = systemInfo.statusBarHeight ?? 0
|
||||
const windowHeight = systemInfo.windowHeight
|
||||
// 减去头部高度 (约60px + statusBarHeight)
|
||||
scrollHeight.value = windowHeight - (60 + statusBarHeight.value)
|
||||
|
||||
loadData()
|
||||
|
||||
// 检查页面参数
|
||||
const pages = getCurrentPages()
|
||||
if (pages.length > 0) {
|
||||
const currentPage = pages[pages.length - 1]
|
||||
// @ts-ignore
|
||||
const options = currentPage.options
|
||||
if (options && options['keyword']) {
|
||||
const keyword = decodeURIComponent(options['keyword'])
|
||||
searchKeyword.value = keyword
|
||||
|
||||
if (options['type'] === 'family' || options['type'] === 'brand') {
|
||||
// 如果是家庭常备药或品牌类型,直接添加到历史并搜索
|
||||
if (options['type'] === 'family') {
|
||||
addToHistory(keyword)
|
||||
}
|
||||
// 立即显示结果区域并设置为加载中
|
||||
showResults.value = true
|
||||
loading.value = true
|
||||
// 确保searchResults不为空数组导致闪烁(虽然loading=true已经拦截了empty-result,但双重保险)
|
||||
// 此时不要置空searchResults,或者给一个初始值
|
||||
|
||||
// 直接调用搜索,移除setTimeout,防止中间状态
|
||||
performSearch()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('初始化失败', e)
|
||||
isError.value = true
|
||||
}
|
||||
type HotSearchItemType = {
|
||||
keyword: string
|
||||
hot: boolean
|
||||
}
|
||||
|
||||
// 加载基础数据
|
||||
const loadData = async () => {
|
||||
isError.value = false
|
||||
|
||||
try {
|
||||
loadSearchHistory()
|
||||
// 获取热门商品作为热门搜索推荐和猜你喜欢
|
||||
// 获取更多数据以便"换一批"
|
||||
const hotProducts = await supabaseService.getHotProducts(30)
|
||||
|
||||
hotSearchList.value = hotProducts.slice(0, 10).map((p: any) => ({
|
||||
keyword: p.name,
|
||||
hot: true
|
||||
}))
|
||||
|
||||
allGuessItems.value = hotProducts.map((p: any) => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
price: p.base_price,
|
||||
image: p.main_image_url ?? '/static/default.jpg',
|
||||
sales: typeof p.sale_count === 'number' ? p.sale_count : 0
|
||||
}))
|
||||
|
||||
// 初始显示随机6个
|
||||
refreshGuessListItems()
|
||||
|
||||
} catch (e) {
|
||||
console.error('Load data failed', e)
|
||||
isError.value = true
|
||||
}
|
||||
type GuessItemType = {
|
||||
id: string
|
||||
name: string
|
||||
price: number
|
||||
image: string
|
||||
sales: number
|
||||
}
|
||||
|
||||
// 点击重试
|
||||
const retryLoad = () => {
|
||||
uni.showLoading({ title: '重新加载中' })
|
||||
setTimeout(() => {
|
||||
uni.hideLoading()
|
||||
loadData()
|
||||
}, 1000)
|
||||
type SearchResultType = {
|
||||
id: string
|
||||
name: string
|
||||
image: string
|
||||
price: number
|
||||
specification: string
|
||||
tag: string
|
||||
sales: number
|
||||
}
|
||||
|
||||
// 历史记录管理
|
||||
type ShopResultType = {
|
||||
id: string
|
||||
name: string
|
||||
logo: string
|
||||
productCount: number
|
||||
}
|
||||
|
||||
const searchHistory = ref<Array<string>>([])
|
||||
const hotSearchList = ref<Array<HotSearchItemType>>([])
|
||||
const guessList = ref<Array<GuessItemType>>([])
|
||||
const allGuessItems = ref<Array<GuessItemType>>([])
|
||||
const searchResults = ref<Array<SearchResultType>>([])
|
||||
const searchShopResults = ref<Array<ShopResultType>>([])
|
||||
|
||||
const loadSearchHistory = () => {
|
||||
const history = uni.getStorageSync('searchHistory')
|
||||
if (history) {
|
||||
if (history != null) {
|
||||
try {
|
||||
// 确保是数组
|
||||
const parsed = JSON.parse(history as string)
|
||||
if (Array.isArray(parsed)) {
|
||||
searchHistory.value = parsed as string[]
|
||||
@@ -414,13 +351,258 @@ const deleteHistoryItem = (index: number) => {
|
||||
saveSearchHistory()
|
||||
}
|
||||
|
||||
// 搜索建议 - 改为实时获取
|
||||
const searchSuggestions = ref<string[]>([])
|
||||
let suggestTimer = 0
|
||||
const refreshGuessListItems = () => {
|
||||
if (allGuessItems.value.length > 0) {
|
||||
const arr: Array<GuessItemType> = []
|
||||
for (let i: number = 0; i < allGuessItems.value.length; i++) {
|
||||
arr.push(allGuessItems.value[i])
|
||||
}
|
||||
for (let i: number = arr.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1))
|
||||
const temp = arr[i]
|
||||
arr[i] = arr[j]
|
||||
arr[j] = temp
|
||||
}
|
||||
const result: Array<GuessItemType> = []
|
||||
const limit = arr.length < 6 ? arr.length : 6
|
||||
for (let i: number = 0; i < limit; i++) {
|
||||
result.push(arr[i])
|
||||
}
|
||||
guessList.value = result
|
||||
}
|
||||
}
|
||||
|
||||
const loadData = async (): Promise<void> => {
|
||||
isError.value = false
|
||||
|
||||
try {
|
||||
loadSearchHistory()
|
||||
const hotProducts = await supabaseService.getHotProducts(30)
|
||||
|
||||
const hotList: Array<HotSearchItemType> = []
|
||||
const limit1 = hotProducts.length < 10 ? hotProducts.length : 10
|
||||
for (let i: number = 0; i < limit1; i++) {
|
||||
const p = hotProducts[i] as UTSJSONObject
|
||||
const item: HotSearchItemType = {
|
||||
keyword: p.getString('name') ?? '',
|
||||
hot: true
|
||||
}
|
||||
hotList.push(item)
|
||||
}
|
||||
hotSearchList.value = hotList
|
||||
|
||||
const allItems: Array<GuessItemType> = []
|
||||
for (let i: number = 0; i < hotProducts.length; i++) {
|
||||
const p = hotProducts[i] as UTSJSONObject
|
||||
const saleCount = p.getNumber('sale_count')
|
||||
const item: GuessItemType = {
|
||||
id: p.getString('id') ?? '',
|
||||
name: p.getString('name') ?? '',
|
||||
price: p.getNumber('base_price') ?? 0,
|
||||
image: p.getString('main_image_url') ?? '/static/default.jpg',
|
||||
sales: saleCount != null ? saleCount : 0
|
||||
}
|
||||
allItems.push(item)
|
||||
}
|
||||
allGuessItems.value = allItems
|
||||
|
||||
refreshGuessListItems()
|
||||
|
||||
} catch (e) {
|
||||
console.error('Load data failed', e)
|
||||
isError.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const retryLoad = () => {
|
||||
uni.showLoading({ title: '重新加载中' })
|
||||
setTimeout(() => {
|
||||
uni.hideLoading()
|
||||
loadData()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const searchSuggestions = ref<Array<string>>([])
|
||||
let suggestTimer: number = 0
|
||||
|
||||
const fetchSuggestions = async (kw: string): Promise<void> => {
|
||||
if (kw == '' || showResults.value) return
|
||||
|
||||
try {
|
||||
const res = await supabaseService.searchProducts(kw.trim(), 1, 5)
|
||||
if (res.data != null && res.data.length > 0) {
|
||||
const names: Array<string> = []
|
||||
for (let i: number = 0; i < res.data.length; i++) {
|
||||
const p = res.data[i]
|
||||
let name = ''
|
||||
if (p instanceof UTSJSONObject) {
|
||||
name = p.getString('name') ?? ''
|
||||
} else {
|
||||
const pObj = p as UTSJSONObject
|
||||
name = pObj.getString('name') ?? ''
|
||||
}
|
||||
let found = false
|
||||
for (let j: number = 0; j < names.length; j++) {
|
||||
if (names[j] === name) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (found === false && name !== '') {
|
||||
names.push(name)
|
||||
}
|
||||
}
|
||||
searchSuggestions.value = names
|
||||
} else {
|
||||
searchSuggestions.value = []
|
||||
}
|
||||
} catch(e) {
|
||||
searchSuggestions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
const currentPage = ref<number>(1)
|
||||
|
||||
const performSearch = async (): Promise<void> => {
|
||||
showResults.value = true
|
||||
loading.value = true
|
||||
currentPage.value = 1
|
||||
|
||||
const keyword = searchKeyword.value.trim()
|
||||
if (keyword == '') {
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
let sortBy = 'sales'
|
||||
let ascending = false
|
||||
if (activeSort.value === 'price') {
|
||||
sortBy = 'price'
|
||||
ascending = priceSortAsc.value
|
||||
} else if (activeSort.value === 'default') {
|
||||
sortBy = 'default'
|
||||
}
|
||||
|
||||
try {
|
||||
const prodResp = await supabaseService.searchProducts(keyword, currentPage.value, 20, sortBy, ascending)
|
||||
|
||||
let shopRespData: Array<any> = []
|
||||
if (currentPage.value === 1 && activeSort.value === 'default') {
|
||||
const shopResp = await supabaseService.searchShops(keyword)
|
||||
if (shopResp.data != null) {
|
||||
const rawData = shopResp.data
|
||||
for (let i: number = 0; i < rawData.length; i++) {
|
||||
shopRespData.push(rawData[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shopRespData.length > 0) {
|
||||
const shopList: Array<ShopResultType> = []
|
||||
for (let i: number = 0; i < shopRespData.length; i++) {
|
||||
const s = shopRespData[i] as UTSJSONObject
|
||||
const shopItem: ShopResultType = {
|
||||
id: s.getString('id') ?? '',
|
||||
name: s.getString('shop_name') ?? '',
|
||||
logo: s.getString('shop_logo') ?? '/static/shop_logo_default.png',
|
||||
productCount: s.getNumber('product_count') ?? 0
|
||||
}
|
||||
shopList.push(shopItem)
|
||||
}
|
||||
searchShopResults.value = shopList
|
||||
} else {
|
||||
searchShopResults.value = []
|
||||
}
|
||||
|
||||
const prodData = prodResp.data != null ? prodResp.data : []
|
||||
const resultList: Array<SearchResultType> = []
|
||||
for (let i: number = 0; i < prodData.length; i++) {
|
||||
const p = prodData[i] as UTSJSONObject
|
||||
let tag = ''
|
||||
const tagsRaw = p.get('tags')
|
||||
if (tagsRaw != null) {
|
||||
try {
|
||||
const tagsStr = p.getString('tags')
|
||||
if (tagsStr != null) {
|
||||
const tags = JSON.parse(tagsStr)
|
||||
if (Array.isArray(tags) && tags.length > 0) {
|
||||
const firstTag = tags[0]
|
||||
tag = firstTag != null ? (firstTag as string) : ''
|
||||
}
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
const searchItem: SearchResultType = {
|
||||
id: p.getString('id') ?? '',
|
||||
name: p.getString('name') ?? '',
|
||||
image: p.getString('main_image_url') ?? '/static/default.jpg',
|
||||
price: p.getNumber('base_price') ?? 0,
|
||||
specification: p.getString('specification') ?? '标准规格',
|
||||
tag: tag,
|
||||
sales: p.getNumber('sale_count') ?? 0
|
||||
}
|
||||
resultList.push(searchItem)
|
||||
}
|
||||
searchResults.value = resultList
|
||||
|
||||
hasMore.value = prodResp.hasmore
|
||||
} catch(e) {
|
||||
console.error('Search failed', e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const initPage = () => {
|
||||
try {
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
statusBarHeight.value = systemInfo.statusBarHeight ?? 0
|
||||
const windowHeight = systemInfo.windowHeight
|
||||
scrollHeight.value = windowHeight - (60 + statusBarHeight.value)
|
||||
|
||||
loadData()
|
||||
|
||||
const pages = getCurrentPages()
|
||||
if (pages.length > 0) {
|
||||
const currentPageObj = pages[pages.length - 1]
|
||||
// @ts-ignore
|
||||
const options = currentPageObj.options
|
||||
if (options != null) {
|
||||
const optObj = options as UTSJSONObject
|
||||
const kwRaw = optObj.getString('keyword')
|
||||
if (kwRaw != null && kwRaw !== '') {
|
||||
const decoded = decodeURIComponent(kwRaw)
|
||||
const keyword = decoded != null ? decoded : kwRaw
|
||||
searchKeyword.value = keyword
|
||||
|
||||
const typeVal = optObj.getString('type')
|
||||
if (typeVal === 'family' || typeVal === 'brand') {
|
||||
if (typeVal === 'family') {
|
||||
addToHistory(keyword)
|
||||
}
|
||||
showResults.value = true
|
||||
loading.value = true
|
||||
performSearch()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('初始化失败', e)
|
||||
isError.value = true
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initPage()
|
||||
})
|
||||
|
||||
// 搜索逻辑
|
||||
const onInput = (e: any) => {
|
||||
const val = e.detail.value
|
||||
const eObj = e as UTSJSONObject
|
||||
const detailRaw = eObj.get('detail')
|
||||
const detail = detailRaw != null ? (detailRaw as UTSJSONObject) : (new UTSJSONObject())
|
||||
const val = detail.getString('value') ?? ''
|
||||
searchKeyword.value = val
|
||||
if (val == '') {
|
||||
showResults.value = false
|
||||
@@ -428,37 +610,12 @@ const onInput = (e: any) => {
|
||||
return
|
||||
}
|
||||
|
||||
// Debounce suggestion search
|
||||
if (suggestTimer > 0) clearTimeout(suggestTimer)
|
||||
suggestTimer = setTimeout(() => {
|
||||
fetchSuggestions(val)
|
||||
}, 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
|
||||
@@ -487,84 +644,6 @@ const selectSuggestion = (suggestion: string) => {
|
||||
performSearch()
|
||||
}
|
||||
|
||||
const currentPage = ref(1)
|
||||
|
||||
const performSearch = async () => {
|
||||
// 再次强制设置状态,确保万无一失
|
||||
showResults.value = true
|
||||
loading.value = true
|
||||
// 重置页码
|
||||
currentPage.value = 1
|
||||
|
||||
// 使用 Supabase 搜索真实数据
|
||||
const keyword = searchKeyword.value.trim()
|
||||
if (keyword == '') {
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// 确定排序方式
|
||||
let sortBy = 'sales'
|
||||
let ascending = false
|
||||
if (activeSort.value === 'price') {
|
||||
sortBy = 'price'
|
||||
ascending = priceSortAsc.value
|
||||
} else if (activeSort.value === 'default') {
|
||||
sortBy = 'default'
|
||||
}
|
||||
|
||||
try {
|
||||
// 并行请求:商品搜索 + 店铺搜索
|
||||
const [prodResp, shopResp] = await Promise.all([
|
||||
supabaseService.searchProducts(keyword, currentPage.value, 20, sortBy, ascending),
|
||||
// 只有第一页搜索且非价格排序时搜索店铺,避免重复和无关搜索
|
||||
currentPage.value === 1 && activeSort.value === 'default'
|
||||
? supabaseService.searchShops(keyword)
|
||||
: Promise.resolve({ data: [], total: 0, page: 1, limit: 0, hasmore: false })
|
||||
])
|
||||
|
||||
// 处理店铺结果
|
||||
if (shopResp.data.length > 0) {
|
||||
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
|
||||
}))
|
||||
} else {
|
||||
searchShopResults.value = []
|
||||
}
|
||||
|
||||
// 处理商品结果
|
||||
searchResults.value = prodResp.data.map((p: any) => {
|
||||
let tag = ''
|
||||
if (p.tags) {
|
||||
try {
|
||||
const tags = (typeof p.tags === 'string') ? JSON.parse(p.tags) : p.tags
|
||||
if (Array.isArray(tags) && tags.length > 0) tag = String(tags[0])
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
return {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
image: p.main_image_url ?? '/static/default.jpg',
|
||||
price: p.base_price,
|
||||
specification: p.specification ?? '标准规格',
|
||||
tag: tag,
|
||||
sales: p.sale_count ?? 0
|
||||
}
|
||||
})
|
||||
|
||||
hasMore.value = prodResp.hasmore
|
||||
} catch(e) {
|
||||
console.error('Search failed', e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 切换排序
|
||||
const switchSort = (type: string) => {
|
||||
if (type === 'price') {
|
||||
if (activeSort.value === 'price') {
|
||||
@@ -580,15 +659,13 @@ const switchSort = (type: string) => {
|
||||
performSearch()
|
||||
}
|
||||
|
||||
const loadMore = async () => {
|
||||
if (loading.value || !hasMore.value || searchKeyword.value.trim() == '') return
|
||||
const loadMore = async (): Promise<void> => {
|
||||
if (loading.value || hasMore.value == false || searchKeyword.value.trim() == '') return
|
||||
loading.value = true
|
||||
|
||||
// 增加页码
|
||||
currentPage.value++
|
||||
|
||||
const keyword = searchKeyword.value.trim()
|
||||
// 确定排序方式
|
||||
let sortBy = 'sales'
|
||||
let ascending = false
|
||||
if (activeSort.value === 'price') {
|
||||
@@ -600,26 +677,35 @@ const loadMore = async () => {
|
||||
|
||||
try {
|
||||
const response = await supabaseService.searchProducts(keyword, currentPage.value, 20, sortBy, ascending)
|
||||
const newItems = response.data.map((p: any) => {
|
||||
const respData = response.data != null ? response.data : []
|
||||
for (let i: number = 0; i < respData.length; i++) {
|
||||
const p = respData[i] as UTSJSONObject
|
||||
let tag = ''
|
||||
if (p.tags) {
|
||||
const tagsRaw = p.get('tags')
|
||||
if (tagsRaw != null) {
|
||||
try {
|
||||
const tags = (typeof p.tags === 'string') ? JSON.parse(p.tags) : p.tags
|
||||
if (Array.isArray(tags) && tags.length > 0) tag = String(tags[0])
|
||||
const tagsStr = p.getString('tags')
|
||||
if (tagsStr != null) {
|
||||
const tags = JSON.parse(tagsStr)
|
||||
if (Array.isArray(tags) && tags.length > 0) {
|
||||
const firstTag = tags[0]
|
||||
tag = firstTag != null ? (firstTag as string) : ''
|
||||
}
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
return {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
image: p.main_image_url ?? '/static/default.jpg',
|
||||
price: p.base_price,
|
||||
specification: p.specification ?? '标准规格',
|
||||
const searchItem: SearchResultType = {
|
||||
id: p.getString('id') ?? '',
|
||||
name: p.getString('name') ?? '',
|
||||
image: p.getString('main_image_url') ?? '/static/default.jpg',
|
||||
price: p.getNumber('base_price') ?? 0,
|
||||
specification: p.getString('specification') ?? '标准规格',
|
||||
tag: tag,
|
||||
sales: p.sale_count ?? 0
|
||||
sales: p.getNumber('sale_count') ?? 0
|
||||
}
|
||||
})
|
||||
searchResults.value.push(...newItems)
|
||||
searchResults.value.push(searchItem)
|
||||
}
|
||||
hasMore.value = response.hasmore
|
||||
} catch(e) {
|
||||
console.error('Load more failed', e)
|
||||
@@ -637,29 +723,22 @@ const refreshGuessList = () => {
|
||||
}, 500)
|
||||
}
|
||||
|
||||
const refreshGuessListItems = () => {
|
||||
if (allGuessItems.value.length > 0) {
|
||||
// 简单的随机乱序并取前6个
|
||||
const shuffled = [...allGuessItems.value].sort(() => Math.random() - 0.5)
|
||||
guessList.value = shuffled.slice(0, 6)
|
||||
}
|
||||
}
|
||||
|
||||
const viewProductDetail = (item: any) => {
|
||||
// 跳转详情页逻辑 - 传递必要的参数作为预加载/fallback
|
||||
const viewProductDetail = (item: SearchResultType | GuessItemType) => {
|
||||
const id = (item as GuessItemType).id
|
||||
const price = (item as GuessItemType).price
|
||||
const name = (item as GuessItemType).name
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/consumer/product-detail?productId=${item.id}&price=${item.price}&name=${encodeURIComponent(item.name)}`
|
||||
url: `/pages/mall/consumer/product-detail?productId=${id}&price=${price}&name=${encodeURIComponent(name)}`
|
||||
})
|
||||
}
|
||||
|
||||
const viewShopDetail = (shop: any) => {
|
||||
const viewShopDetail = (shop: ShopResultType) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/consumer/shop-detail?id=${shop.id}`
|
||||
})
|
||||
}
|
||||
|
||||
// 添加到购物车 - 搜索列表无法选择规格,跳转详情页
|
||||
const addToCart = (product: any) => {
|
||||
const addToCart = (product: SearchResultType | GuessItemType) => {
|
||||
uni.showToast({ title: '请选择规格', icon: 'none' })
|
||||
setTimeout(() => {
|
||||
viewProductDetail(product)
|
||||
|
||||
@@ -31,8 +31,8 @@
|
||||
<text class="item-icon">📱</text>
|
||||
<text class="item-text">手机绑定</text>
|
||||
<view class="item-right">
|
||||
<text class="item-status" :class="{ bound: userInfo.phone }">
|
||||
{{ userInfo.phone ? '已绑定' : '未绑定' }}
|
||||
<text class="item-status" :class="userInfo.phone != null && userInfo.phone != '' ? 'bound' : ''">
|
||||
{{ userInfo.phone != null && userInfo.phone != '' ? '已绑定' : '未绑定' }}
|
||||
</text>
|
||||
<text class="item-arrow">›</text>
|
||||
</view>
|
||||
@@ -41,8 +41,8 @@
|
||||
<text class="item-icon">📧</text>
|
||||
<text class="item-text">邮箱绑定</text>
|
||||
<view class="item-right">
|
||||
<text class="item-status" :class="{ bound: userInfo.email }">
|
||||
{{ userInfo.email ? '已绑定' : '未绑定' }}
|
||||
<text class="item-status" :class="userInfo.email != null && userInfo.email != '' ? 'bound' : ''">
|
||||
{{ userInfo.email != null && userInfo.email != '' ? '已绑定' : '未绑定' }}
|
||||
</text>
|
||||
<text class="item-arrow">›</text>
|
||||
</view>
|
||||
@@ -260,54 +260,65 @@ const currentLanguage = ref<string>('简体中文')
|
||||
const currentTheme = ref<string>('自动')
|
||||
const appVersion = ref<string>('1.0.0')
|
||||
|
||||
const statusBarHeight = ref(0)
|
||||
const statusBarHeight = ref<number>(0)
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
statusBarHeight.value = systemInfo.statusBarHeight
|
||||
loadUserInfo()
|
||||
loadSettings()
|
||||
})
|
||||
|
||||
// 加载用户信息
|
||||
const loadUserInfo = () => {
|
||||
const userStore = uni.getStorageSync('userInfo')
|
||||
if (userStore) {
|
||||
userInfo.value = userStore
|
||||
if (userStore != null) {
|
||||
const storeObj = userStore as UTSJSONObject
|
||||
const user: UserType = {
|
||||
id: storeObj.getString('id') ?? '',
|
||||
phone: storeObj.getString('phone'),
|
||||
email: storeObj.getString('email'),
|
||||
nickname: storeObj.getString('nickname'),
|
||||
avatar_url: storeObj.getString('avatar_url')
|
||||
} as UserType
|
||||
userInfo.value = user
|
||||
}
|
||||
}
|
||||
|
||||
// 加载设置
|
||||
const loadSettings = () => {
|
||||
// 从本地存储加载设置
|
||||
const savedNotifications = uni.getStorageSync('userNotifications')
|
||||
if (savedNotifications) {
|
||||
notifications.value = savedNotifications
|
||||
if (savedNotifications != null) {
|
||||
const notifObj = savedNotifications as UTSJSONObject
|
||||
const notif: NotificationType = {
|
||||
order: notifObj.getBoolean('order') ?? true,
|
||||
promotion: notifObj.getBoolean('promotion') ?? true,
|
||||
review: notifObj.getBoolean('review') ?? true
|
||||
} as NotificationType
|
||||
notifications.value = notif
|
||||
}
|
||||
|
||||
const savedPrivacy = uni.getStorageSync('userPrivacy')
|
||||
if (savedPrivacy) {
|
||||
privacy.value = savedPrivacy
|
||||
if (savedPrivacy != null) {
|
||||
const privacyObj = savedPrivacy as UTSJSONObject
|
||||
const priv: PrivacyType = {
|
||||
hidePurchase: privacyObj.getBoolean('hidePurchase') ?? false,
|
||||
allowSearchByPhone: privacyObj.getBoolean('allowSearchByPhone') ?? true,
|
||||
receiveMerchantMsg: privacyObj.getBoolean('receiveMerchantMsg') ?? true
|
||||
} as PrivacyType
|
||||
privacy.value = priv
|
||||
}
|
||||
|
||||
// 计算缓存大小
|
||||
calculateCacheSize()
|
||||
|
||||
// 获取应用版本
|
||||
// @ts-ignore
|
||||
const appInfo = uni.getAppBaseInfo()
|
||||
if (appInfo?.appVersion) {
|
||||
appVersion.value = appInfo.appVersion
|
||||
}
|
||||
}
|
||||
|
||||
// 计算缓存大小
|
||||
const calculateCacheSize = () => {
|
||||
// 这里应该计算实际缓存大小,这里使用模拟数据
|
||||
cacheSize.value = '12.5 MB'
|
||||
|
||||
const appInfo = uni.getAppBaseInfo()
|
||||
if (appInfo != null) {
|
||||
const infoObj = appInfo as UTSJSONObject
|
||||
const version = infoObj.getString('appVersion')
|
||||
if (version != null) {
|
||||
appVersion.value = version
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
statusBarHeight.value = systemInfo.statusBarHeight ?? 0
|
||||
loadUserInfo()
|
||||
loadSettings()
|
||||
})
|
||||
|
||||
// 跳转到个人资料
|
||||
const goToProfile = () => {
|
||||
uni.navigateTo({
|
||||
@@ -344,14 +355,26 @@ const bindEmail = () => {
|
||||
}
|
||||
|
||||
// 切换通知设置
|
||||
const toggleNotification = (type: keyof NotificationType) => {
|
||||
notifications.value[type] = !notifications.value[type]
|
||||
const toggleNotification = (type: string) => {
|
||||
if (type === 'order') {
|
||||
notifications.value.order = notifications.value.order === false
|
||||
} else if (type === 'promotion') {
|
||||
notifications.value.promotion = notifications.value.promotion === false
|
||||
} else if (type === 'review') {
|
||||
notifications.value.review = notifications.value.review === false
|
||||
}
|
||||
uni.setStorageSync('userNotifications', notifications.value)
|
||||
}
|
||||
|
||||
// 切换隐私设置
|
||||
const togglePrivacy = (type: keyof PrivacyType) => {
|
||||
privacy.value[type] = !privacy.value[type]
|
||||
const togglePrivacy = (type: string) => {
|
||||
if (type === 'hidePurchase') {
|
||||
privacy.value.hidePurchase = privacy.value.hidePurchase === false
|
||||
} else if (type === 'allowSearchByPhone') {
|
||||
privacy.value.allowSearchByPhone = privacy.value.allowSearchByPhone === false
|
||||
} else if (type === 'receiveMerchantMsg') {
|
||||
privacy.value.receiveMerchantMsg = privacy.value.receiveMerchantMsg === false
|
||||
}
|
||||
uni.setStorageSync('userPrivacy', privacy.value)
|
||||
}
|
||||
|
||||
@@ -473,28 +496,12 @@ const feedback = () => {
|
||||
})
|
||||
}
|
||||
|
||||
// 给个好评
|
||||
const rateApp = () => {
|
||||
// 这里应该跳转到应用商店评分
|
||||
uni.showModal({
|
||||
title: '给个好评',
|
||||
content: '如果喜欢我们的应用,请给个好评吧!',
|
||||
confirmText: '去评分',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 跳转到应用商店
|
||||
// @ts-ignore
|
||||
uni.navigateToMiniProgram({
|
||||
appId: 'wx1234567890', // 示例AppID
|
||||
fail: () => {
|
||||
uni.showToast({
|
||||
title: '跳转失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
content: '如果喜欢我们的应用,请给个好评吧!感谢您的支持!',
|
||||
confirmText: '好的',
|
||||
showCancel: false
|
||||
})
|
||||
}
|
||||
|
||||
@@ -503,116 +510,76 @@ const logout = () => {
|
||||
uni.showModal({
|
||||
title: '退出登录',
|
||||
content: '确定要退出登录吗?',
|
||||
success: async (res) => {
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
uni.showLoading({
|
||||
title: '正在退出...'
|
||||
})
|
||||
// 调用登出接口
|
||||
const { error } = await supa.auth.signOut()
|
||||
uni.showLoading({
|
||||
title: '正在退出...'
|
||||
})
|
||||
|
||||
if (error !== null) {
|
||||
console.error('登出失败:', error)
|
||||
// 即使失败也继续清除本地状态
|
||||
}
|
||||
uni.removeStorageSync('userInfo')
|
||||
uni.removeStorageSync('user_id')
|
||||
uni.removeStorageSync('access_token')
|
||||
|
||||
// 清除本地存储的用户信息
|
||||
uni.removeStorageSync('userInfo')
|
||||
uni.removeStorageSync('user_id')
|
||||
uni.removeStorageSync('access_token')
|
||||
uni.hideLoading()
|
||||
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: '已退出登录',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
uni.showToast({
|
||||
title: '已退出登录',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({
|
||||
url: '/pages/user/login'
|
||||
})
|
||||
}, 1000)
|
||||
} catch (e) {
|
||||
uni.hideLoading()
|
||||
console.error('Logout Exception:', e)
|
||||
uni.showToast({
|
||||
title: '退出异常',
|
||||
icon: 'none'
|
||||
})
|
||||
// 强制退出
|
||||
uni.removeStorageSync('userInfo')
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({
|
||||
url: '/pages/user/login'
|
||||
})
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
// 注销账号
|
||||
const deleteAccount = () => {
|
||||
uni.showModal({
|
||||
title: '注销账号',
|
||||
content: '确定要注销账号吗?此操作不可恢复,所有数据将被删除!',
|
||||
confirmText: '注销',
|
||||
confirmColor: '#ff4757',
|
||||
success: async (res) => {
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
uni.showLoading({
|
||||
title: '注销中...'
|
||||
uni.showLoading({
|
||||
title: '注销中...'
|
||||
})
|
||||
|
||||
let userId: string | null = userInfo.value.id
|
||||
if (userId == null || userId === '') {
|
||||
const storageId = uni.getStorageSync('user_id')
|
||||
userId = (storageId != null) ? storageId as string : null
|
||||
}
|
||||
|
||||
if (userId != null) {
|
||||
const updateObj: UTSJSONObject = new UTSJSONObject()
|
||||
updateObj.set('status', 3)
|
||||
supa
|
||||
.from('ml_user_profiles')
|
||||
.update(updateObj)
|
||||
.eq('user_id', userId)
|
||||
.execute()
|
||||
}
|
||||
|
||||
uni.removeStorageSync('userInfo')
|
||||
uni.removeStorageSync('user_id')
|
||||
uni.removeStorageSync('access_token')
|
||||
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: '账号已注销',
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({
|
||||
url: '/pages/user/login'
|
||||
})
|
||||
|
||||
let userId = userInfo.value.getString('id')
|
||||
if (userId == null) {
|
||||
const storageId = uni.getStorageSync('user_id')
|
||||
userId = (storageId != null) ? storageId as string : null
|
||||
}
|
||||
|
||||
if (userId != null) {
|
||||
try {
|
||||
// 标记用户状态为注销 (status=3)
|
||||
await supa
|
||||
.from('ml_user_profiles')
|
||||
.update({ status: 3 })
|
||||
.eq('user_id', userId)
|
||||
} catch(e) {
|
||||
console.error('Update status failed', e)
|
||||
}
|
||||
|
||||
// 登出
|
||||
await supa.auth.signOut()
|
||||
}
|
||||
|
||||
// 清除本地存储
|
||||
uni.removeStorageSync('userInfo')
|
||||
uni.removeStorageSync('user_id')
|
||||
uni.removeStorageSync('access_token')
|
||||
|
||||
// 提示并跳转
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: '账号已注销',
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({
|
||||
url: '/pages/user/login'
|
||||
})
|
||||
}, 1500)
|
||||
|
||||
} catch (err) {
|
||||
uni.hideLoading()
|
||||
console.error('注销账号失败:', err)
|
||||
uni.showToast({
|
||||
title: '注销失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}, 1500)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<view class="coupon-card" v-for="coupon in coupons" :key="coupon.id" @click="claimCoupon(coupon)">
|
||||
<view class="coupon-left">
|
||||
<text class="coupon-amount"><text style="font-size:10px">¥</text>{{ coupon.discount_value }}</text>
|
||||
<text class="coupon-cond" v-if="parseFloat(String(coupon.min_order_amount)) > 0">满{{ coupon.min_order_amount }}</text>
|
||||
<text class="coupon-cond" v-if="coupon.min_order_amount > 0">满{{ coupon.min_order_amount }}</text>
|
||||
<text class="coupon-cond" v-else>无门槛</text>
|
||||
</view>
|
||||
<view class="coupon-right">
|
||||
@@ -70,7 +70,7 @@
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { MerchantType, ProductType } from '@/types/mall-types.uts'
|
||||
import { MerchantType, ProductType, CouponTemplateType } from '@/types/mall-types.uts'
|
||||
import { supabaseService } from '@/utils/supabaseService.uts'
|
||||
|
||||
// 分页相关状态
|
||||
@@ -95,104 +95,49 @@ const merchant = ref<MerchantType>({
|
||||
created_at: ''
|
||||
} as MerchantType)
|
||||
|
||||
const products = ref<ProductType[]>([])
|
||||
const isFollowed = ref(false)
|
||||
const coupons = ref<any[]>([]) // 新增优惠券
|
||||
const isRefresherTriggered = ref(false)
|
||||
const products = ref<Array<ProductType>>([])
|
||||
const isFollowed = ref<boolean>(false)
|
||||
const coupons = ref<Array<CouponTemplateType>>([])
|
||||
const isRefresherTriggered = ref<boolean>(false)
|
||||
|
||||
onMounted(() => {
|
||||
const pages = getCurrentPages()
|
||||
const options = pages[pages.length - 1].options as any
|
||||
// Search传递的是 id (shop_id), 其他地方可能传递 merchantId
|
||||
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)
|
||||
// 优先加载店铺信息
|
||||
loadShopData(paramId).then(() => {
|
||||
// 加载成功后,使用确定的 merchant_id 来查询关联数据 (商品/优惠券通常是关联在 merchant_id 上的)
|
||||
const realMerchantId = merchant.value.user_id // 这里 user_id 映射了 DB 中的 merchant_id
|
||||
if (realMerchantId && realMerchantId !== '') {
|
||||
console.log('Chain loading products for Corrected Merchant ID:', realMerchantId)
|
||||
currentMerchantId.value = realMerchantId // 更新当前上下文ID
|
||||
loadShopProducts(realMerchantId)
|
||||
loadCoupons(realMerchantId)
|
||||
} else {
|
||||
// 防御性策略:如果没能获取 merchant_id,尝试用传入 ID
|
||||
console.warn('Shop load failed or id empty, fallback using original id:', paramId)
|
||||
currentMerchantId.value = paramId
|
||||
loadShopProducts(paramId)
|
||||
loadCoupons(paramId)
|
||||
const checkFollowStatus = async (shopId: string): Promise<void> => {
|
||||
const userId = supabaseService.getCurrentUserId()
|
||||
if (userId != null && userId !== '') {
|
||||
try {
|
||||
isFollowed.value = await supabaseService.isShopFollowed(shopId, userId)
|
||||
} catch(e) {
|
||||
console.warn('isShopFollowed method not available')
|
||||
}
|
||||
})
|
||||
} else {
|
||||
console.error('No ID passed to shop-detail')
|
||||
uni.showToast({title: '参数错误', icon: 'error'})
|
||||
}
|
||||
})
|
||||
|
||||
const onRefresherRefresh = () => {
|
||||
isRefresherTriggered.value = true
|
||||
currentPage.value = 1
|
||||
hasMore.value = true
|
||||
isLoading.value = false
|
||||
|
||||
if (currentMerchantId.value != '') {
|
||||
const id = currentMerchantId.value
|
||||
Promise.all([
|
||||
loadShopData(id),
|
||||
loadCoupons(id),
|
||||
loadShopProducts(id)
|
||||
]).then(() => {
|
||||
isRefresherTriggered.value = false
|
||||
})
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
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(() => {
|
||||
onScrollToLower()
|
||||
})
|
||||
|
||||
const loadShopData = async (id: string) => {
|
||||
const loadShopData = async (id: string): Promise<void> => {
|
||||
console.log('Loading shop data for:', id)
|
||||
const shop = await supabaseService.getShopByMerchantId(id)
|
||||
|
||||
if (shop) {
|
||||
if (shop != null) {
|
||||
console.log('Shop loaded successfully:', shop.shop_name)
|
||||
merchant.value = {
|
||||
id: shop.id,
|
||||
user_id: shop.merchant_id, // 映射关系
|
||||
shop_name: shop.shop_name,
|
||||
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 != 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 : ''
|
||||
}
|
||||
const shopObj = shop as UTSJSONObject
|
||||
const merchantData: MerchantType = {
|
||||
id: shopObj.getString('id') ?? '',
|
||||
user_id: shopObj.getString('merchant_id') ?? '',
|
||||
shop_name: shopObj.getString('shop_name') ?? '',
|
||||
shop_logo: shopObj.getString('shop_logo') ?? '/static/default-shop.png',
|
||||
shop_banner: shopObj.getString('shop_banner') ?? '/static/default-banner.png',
|
||||
shop_description: shopObj.getString('description') ?? '',
|
||||
contact_name: shopObj.getString('contact_name') ?? '',
|
||||
contact_phone: shopObj.getString('contact_phone') ?? '',
|
||||
shop_status: 1,
|
||||
rating: shopObj.getNumber('rating_avg') ?? 5.0,
|
||||
total_sales: shopObj.getNumber('total_sales') ?? 0,
|
||||
created_at: shopObj.getString('created_at') ?? ''
|
||||
} as MerchantType
|
||||
merchant.value = merchantData
|
||||
|
||||
// 检查关注状态
|
||||
checkFollowStatus(shop.id)
|
||||
const shopId = shopObj.getString('id') ?? ''
|
||||
if (shopId !== '') {
|
||||
checkFollowStatus(shopId)
|
||||
}
|
||||
} else {
|
||||
console.warn('Shop data is null for ID:', id)
|
||||
uni.showToast({
|
||||
@@ -203,100 +148,99 @@ const loadShopData = async (id: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
const loadCoupons = async (id: string) => {
|
||||
// 安全检查,防止因编译器可以缓存导致的方法未定义错误
|
||||
// @ts-ignore
|
||||
if (typeof supabaseService.fetchShopCoupons === 'function') {
|
||||
coupons.value = await supabaseService.fetchShopCoupons(id)
|
||||
} else if (typeof supabaseService.getAvailableCoupons === 'function') {
|
||||
// Fallback to old name
|
||||
coupons.value = await supabaseService.getAvailableCoupons(id)
|
||||
} else {
|
||||
console.warn('SupabaseService.fetchShopCoupons method missing. Please rebuild project.')
|
||||
const loadCoupons = async (id: string): Promise<void> => {
|
||||
try {
|
||||
const rawCoupons = await supabaseService.fetchShopCoupons(id)
|
||||
if (rawCoupons != null && Array.isArray(rawCoupons)) {
|
||||
const couponList: Array<CouponTemplateType> = []
|
||||
for (let i: number = 0; i < rawCoupons.length; i++) {
|
||||
const c = rawCoupons[i] as UTSJSONObject
|
||||
const coupon: CouponTemplateType = {
|
||||
id: c.getString('id') ?? '',
|
||||
name: c.getString('name') ?? '',
|
||||
description: c.getString('description'),
|
||||
coupon_type: c.getNumber('coupon_type') ?? 0,
|
||||
discount_type: c.getNumber('discount_type') ?? 0,
|
||||
discount_value: c.getNumber('discount_value') ?? 0,
|
||||
min_order_amount: c.getNumber('min_order_amount') ?? 0,
|
||||
max_discount_amount: c.getNumber('max_discount_amount'),
|
||||
total_quantity: c.getNumber('total_quantity'),
|
||||
per_user_limit: c.getNumber('per_user_limit') ?? 1,
|
||||
usage_limit: c.getNumber('usage_limit') ?? 0,
|
||||
merchant_id: c.getString('merchant_id'),
|
||||
category_ids: [],
|
||||
product_ids: [],
|
||||
user_type_limit: c.getNumber('user_type_limit'),
|
||||
start_time: c.getString('start_time') ?? '',
|
||||
end_time: c.getString('end_time') ?? '',
|
||||
status: c.getNumber('status') ?? 1,
|
||||
created_at: c.getString('created_at') ?? ''
|
||||
} as CouponTemplateType
|
||||
couponList.push(coupon)
|
||||
}
|
||||
coupons.value = couponList
|
||||
}
|
||||
} catch(e) {
|
||||
console.warn('SupabaseService.fetchShopCoupons method missing.')
|
||||
}
|
||||
}
|
||||
|
||||
const claimCoupon = async (coupon: any) => {
|
||||
const userId = supabaseService.getCurrentUserId()
|
||||
if (userId == null) {
|
||||
uni.navigateTo({ url: '/pages/auth/login' })
|
||||
return
|
||||
}
|
||||
uni.showLoading({ title: '领取中' })
|
||||
|
||||
let success = false
|
||||
// @ts-ignore
|
||||
if (typeof supabaseService.claimShopCoupon === 'function') {
|
||||
success = await supabaseService.claimShopCoupon(coupon.id, userId)
|
||||
} else if (typeof supabaseService.claimCoupon === 'function') {
|
||||
success = await supabaseService.claimCoupon(coupon.id, userId)
|
||||
} else {
|
||||
console.warn('claimCoupon not found')
|
||||
}
|
||||
|
||||
uni.hideLoading()
|
||||
if (success) {
|
||||
uni.showToast({ title: '领取成功', icon: 'success' })
|
||||
} else {
|
||||
uni.showToast({ title: '领取失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
const loadShopProducts = async (id: string) => {
|
||||
const loadShopProducts = async (id: string): Promise<void> => {
|
||||
if (isLoading.value) return
|
||||
isLoading.value = true
|
||||
|
||||
// 保存当前使用的MerchantID,供下拉/触底使用
|
||||
if (currentPage.value === 1) {
|
||||
currentMerchantId.value = id
|
||||
}
|
||||
|
||||
console.log(`shop-detail loadShopProducts for: ${id} page: ${currentPage.value}`)
|
||||
// @ts-ignore
|
||||
if (typeof supabaseService.getProductsByMerchantId !== 'function') {
|
||||
console.error('getProductsByMerchantId missing')
|
||||
|
||||
let res: any = {}
|
||||
try {
|
||||
res = await supabaseService.getProductsByMerchantId(id, currentPage.value, pageSize.value)
|
||||
} catch(e) {
|
||||
console.error('getProductsByMerchantId missing or error:', e)
|
||||
isLoading.value = false
|
||||
uni.stopPullDownRefresh()
|
||||
return
|
||||
}
|
||||
|
||||
// 传入分页参数
|
||||
const res = await supabaseService.getProductsByMerchantId(id, currentPage.value, pageSize.value)
|
||||
console.log(`shop-detail getProductsByMerchantId result count: ${res.data?.length}`)
|
||||
|
||||
const rawList = res.data
|
||||
const rawList = res?.data
|
||||
if (rawList != null && Array.isArray(rawList) && rawList.length > 0) {
|
||||
const list = rawList.map((item): ProductType => {
|
||||
// 解析图片数组
|
||||
let images: string[] = []
|
||||
const list: Array<ProductType> = []
|
||||
|
||||
// 注意:类型转换,UTS中 any 到具体的类型转换
|
||||
// 1. 尝试 main_image_url
|
||||
if (item.main_image_url != null && item.main_image_url !== '') {
|
||||
images.push(item.main_image_url!)
|
||||
for (let idx: number = 0; idx < rawList.length; idx++) {
|
||||
const item = rawList[idx] as UTSJSONObject
|
||||
const images: Array<string> = []
|
||||
|
||||
const mainImageUrl = item.getString('main_image_url')
|
||||
if (mainImageUrl != null && mainImageUrl !== '') {
|
||||
images.push(mainImageUrl)
|
||||
}
|
||||
|
||||
// 2. 尝试 image_urls (如果 main 为空,或者需要展示多图)
|
||||
if (item.image_urls != null) {
|
||||
const imageUrlsRaw = item.get('image_urls')
|
||||
if (imageUrlsRaw != null) {
|
||||
try {
|
||||
const rawUrl = item.image_urls
|
||||
if (Array.isArray(rawUrl)) {
|
||||
// 已经是数组
|
||||
const arr = rawUrl as string[]
|
||||
if (Array.isArray(imageUrlsRaw)) {
|
||||
const arr = imageUrlsRaw as Array<string>
|
||||
if (arr.length > 0) {
|
||||
// 如果目前没有图,就全加进去;如果有图(main_image),考虑是否去重
|
||||
if (images.length == 0) images.push(...arr)
|
||||
if (images.length == 0) {
|
||||
for (let i: number = 0; i < arr.length; i++) {
|
||||
images.push(arr[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (typeof rawUrl === 'string') {
|
||||
if (rawUrl.startsWith('[')) {
|
||||
const parsed = JSON.parse(rawUrl)
|
||||
if (Array.isArray(parsed)) {
|
||||
const arr = parsed as string[]
|
||||
if (images.length == 0) images.push(...arr)
|
||||
} else {
|
||||
const rawUrlStr = imageUrlsRaw as string
|
||||
if (rawUrlStr.startsWith('[')) {
|
||||
const parsed = JSON.parse(rawUrlStr)
|
||||
if (Array.isArray(parsed) && images.length == 0) {
|
||||
for (let i: number = 0; i < parsed.length; i++) {
|
||||
images.push(parsed[i] as string)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 单个图片路径字符串,如果跟 main_image 不一样才加
|
||||
if (images.indexOf(rawUrl) === -1) images.push(rawUrl)
|
||||
if (images.indexOf(rawUrlStr) === -1) images.push(rawUrlStr)
|
||||
}
|
||||
}
|
||||
} catch(e) {
|
||||
@@ -304,63 +248,62 @@ const loadShopProducts = async (id: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 没有任何图片则使用默认
|
||||
if (images.length === 0) {
|
||||
images.push('/static/default-product.png')
|
||||
}
|
||||
|
||||
// 安全获取属性的方式,处理字段名称不一样的问题
|
||||
const safeItem = item as any
|
||||
let safePrice = safeItem['base_price'] as number
|
||||
let safePrice = item.getNumber('base_price')
|
||||
if (safePrice == null) {
|
||||
const p = safeItem['price'] as number
|
||||
const p = item.getNumber('price')
|
||||
safePrice = p != null ? p : 0
|
||||
}
|
||||
|
||||
let safeMarketPrice = safeItem['market_price'] as number
|
||||
let safeMarketPrice = item.getNumber('market_price')
|
||||
if (safeMarketPrice == null) {
|
||||
const mp = safeItem['original_price'] as number
|
||||
const mp = item.getNumber('original_price')
|
||||
safeMarketPrice = mp != null ? mp : safePrice
|
||||
}
|
||||
|
||||
let safeStock = safeItem['total_stock'] as number
|
||||
let safeStock = item.getNumber('total_stock')
|
||||
if (safeStock == null) {
|
||||
let as_ = safeItem['available_stock'] as number
|
||||
let as_ = item.getNumber('available_stock')
|
||||
if (as_ == null) {
|
||||
const s = safeItem['stock'] as number
|
||||
const s = item.getNumber('stock')
|
||||
safeStock = s != null ? s : 0
|
||||
} else {
|
||||
safeStock = as_
|
||||
}
|
||||
}
|
||||
|
||||
let safeSales = safeItem['sale_count'] as number
|
||||
let safeSales = item.getNumber('sale_count')
|
||||
if (safeSales == null) {
|
||||
const s = safeItem['sales'] as number
|
||||
const s = item.getNumber('sales')
|
||||
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 != null ? item.description : '',
|
||||
const product: ProductType = {
|
||||
id: item.getString('id') ?? '',
|
||||
merchant_id: item.getString('merchant_id') ?? '',
|
||||
category_id: item.getString('category_id') ?? '',
|
||||
name: item.getString('name') ?? '',
|
||||
description: item.getString('description') ?? '',
|
||||
images: images,
|
||||
price: safePrice,
|
||||
original_price: safeMarketPrice,
|
||||
stock: safeStock,
|
||||
sales: safeSales,
|
||||
status: 1,
|
||||
created_at: item.created_at != null ? item.created_at : '',
|
||||
updated_at: item.updated_at != null ? item.updated_at : ''
|
||||
}
|
||||
})
|
||||
created_at: item.getString('created_at') ?? ''
|
||||
} as ProductType
|
||||
list.push(product)
|
||||
}
|
||||
|
||||
if (currentPage.value === 1) {
|
||||
products.value = list
|
||||
} else {
|
||||
products.value.push(...list)
|
||||
for (let i: number = 0; i < list.length; i++) {
|
||||
products.value.push(list[i])
|
||||
}
|
||||
}
|
||||
|
||||
// 判断是否还有更多
|
||||
@@ -382,17 +325,7 @@ const loadShopProducts = async (id: string) => {
|
||||
uni.stopPullDownRefresh()
|
||||
}
|
||||
|
||||
const checkFollowStatus = async (shopId: string) => {
|
||||
const userId = supabaseService.getCurrentUserId()
|
||||
if (userId) {
|
||||
// @ts-ignore
|
||||
if (typeof supabaseService.isShopFollowed === 'function') {
|
||||
isFollowed.value = await supabaseService.isShopFollowed(shopId, userId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const toggleFollow = async () => {
|
||||
const toggleFollow = async (): Promise<void> => {
|
||||
const userId = supabaseService.getCurrentUserId()
|
||||
if (userId == null) {
|
||||
uni.navigateTo({ url: '/pages/auth/login' })
|
||||
@@ -405,25 +338,32 @@ const toggleFollow = async () => {
|
||||
|
||||
uni.showLoading({ title: '处理中' })
|
||||
|
||||
// @ts-ignore
|
||||
if (isFollowed.value) {
|
||||
// 取消关注
|
||||
// @ts-ignore
|
||||
const success = await supabaseService.unfollowShop(shopId, userId)
|
||||
if (success) {
|
||||
isFollowed.value = false
|
||||
uni.showToast({ title: '已取消关注', icon: 'none' })
|
||||
} else {
|
||||
try {
|
||||
// @ts-ignore
|
||||
const success = await supabaseService.unfollowShop(shopId, userId)
|
||||
if (success) {
|
||||
isFollowed.value = false
|
||||
uni.showToast({ title: '已取消关注', icon: 'none' })
|
||||
} else {
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
}
|
||||
} catch(e) {
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
}
|
||||
} else {
|
||||
// 关注
|
||||
// @ts-ignore
|
||||
const success = await supabaseService.followShop(shopId, userId)
|
||||
if (success) {
|
||||
isFollowed.value = true
|
||||
uni.showToast({ title: '关注成功', icon: 'success' })
|
||||
} else {
|
||||
try {
|
||||
// @ts-ignore
|
||||
const success = await supabaseService.followShop(shopId, userId)
|
||||
if (success) {
|
||||
isFollowed.value = true
|
||||
uni.showToast({ title: '关注成功', icon: 'success' })
|
||||
} else {
|
||||
uni.showToast({ title: '关注失败', icon: 'none' })
|
||||
}
|
||||
} catch(e) {
|
||||
uni.showToast({ title: '关注失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
@@ -437,7 +377,7 @@ const contactService = () => {
|
||||
return
|
||||
}
|
||||
|
||||
if (merchant.value.user_id) {
|
||||
if (merchant.value.user_id != null && merchant.value.user_id != '') {
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/consumer/chat?merchantId=${merchant.value.user_id}&merchantName=${encodeURIComponent(merchant.value.shop_name)}`
|
||||
})
|
||||
@@ -471,6 +411,89 @@ const goToProduct = (id: string) => {
|
||||
url: `/pages/mall/consumer/product-detail?productId=${id}`
|
||||
})
|
||||
}
|
||||
|
||||
const claimCoupon = async (coupon: CouponTemplateType): Promise<void> => {
|
||||
const userId = supabaseService.getCurrentUserId()
|
||||
if (userId == null) {
|
||||
uni.navigateTo({ url: '/pages/auth/login' })
|
||||
return
|
||||
}
|
||||
uni.showLoading({ title: '领取中' })
|
||||
|
||||
let success = false
|
||||
const couponId = coupon.id
|
||||
|
||||
try {
|
||||
success = await supabaseService.claimShopCoupon(couponId, userId)
|
||||
} catch(e) {
|
||||
try {
|
||||
success = await supabaseService.claimCoupon(couponId, userId)
|
||||
} catch(e2) {
|
||||
console.warn('claimCoupon not found')
|
||||
}
|
||||
}
|
||||
|
||||
uni.hideLoading()
|
||||
if (success) {
|
||||
uni.showToast({ title: '领取成功', icon: 'success' })
|
||||
} else {
|
||||
uni.showToast({ title: '领取失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
const onRefresherRefresh = (): void => {
|
||||
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)
|
||||
setTimeout(() => {
|
||||
isRefresherTriggered.value = false
|
||||
}, 500)
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
isRefresherTriggered.value = false
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
|
||||
const onScrollToLower = (): void => {
|
||||
if (hasMore.value && isLoading.value == false && currentMerchantId.value != '') {
|
||||
console.log('Scroll to lower, loading more...')
|
||||
loadShopProducts(currentMerchantId.value)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const pages = getCurrentPages()
|
||||
const options = pages[pages.length - 1].options as UTSJSONObject
|
||||
const mId = options.getString('merchantId')
|
||||
const pId = options.getString('id')
|
||||
const paramId = (mId != null ? mId : pId) as string
|
||||
|
||||
if (paramId != null && paramId !== '') {
|
||||
console.log('Page mounted with params:', paramId)
|
||||
loadShopData(paramId)
|
||||
loadShopProducts(paramId)
|
||||
loadCoupons(paramId)
|
||||
} else {
|
||||
console.error('No ID passed to shop-detail')
|
||||
uni.showToast({title: '参数错误', icon: 'error'})
|
||||
}
|
||||
})
|
||||
|
||||
onPullDownRefresh(() => {
|
||||
onRefresherRefresh()
|
||||
})
|
||||
|
||||
onReachBottom(() => {
|
||||
onScrollToLower()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else-if="!loading" class="empty-state">
|
||||
<view v-else-if="loading == false" class="empty-state">
|
||||
<text class="empty-text">暂无关注的店铺</text>
|
||||
<button class="go-shop-btn" @click="goHome">去逛逛</button>
|
||||
</view>
|
||||
@@ -44,82 +44,83 @@ type FollowedShop = {
|
||||
total_sales: number
|
||||
}
|
||||
|
||||
const shops = ref<FollowedShop[]>([])
|
||||
const loading = ref(true)
|
||||
const shops = ref<Array<FollowedShop>>([])
|
||||
const loading = ref<boolean>(true)
|
||||
|
||||
onMounted(() => {
|
||||
loadFollowedShops()
|
||||
})
|
||||
|
||||
const loadFollowedShops = async () => {
|
||||
const loadFollowedShops = async (): Promise<void> => {
|
||||
loading.value = true
|
||||
const userId = supabaseService.getCurrentUserId()
|
||||
if (!userId) {
|
||||
if (userId == null || userId == '') {
|
||||
uni.navigateTo({ url: '/pages/auth/login' })
|
||||
return
|
||||
}
|
||||
|
||||
const res = await supabaseService.getFollowedShops(userId)
|
||||
// res is array of { id, user_id, shop_id, ml_shops: {...} }
|
||||
|
||||
const list: FollowedShop[] = []
|
||||
res.forEach((item: any) => {
|
||||
const shopData = item['ml_shops'] as any
|
||||
if (shopData != null) {
|
||||
list.push({
|
||||
id: shopData['id'] as string, // Shop ID
|
||||
merchant_id: shopData['merchant_id'] as string,
|
||||
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'] != null) ? (shopData['rating_avg'] as number) : 5.0,
|
||||
total_sales: (shopData['total_sales'] != null) ? (shopData['total_sales'] as number) : 0
|
||||
})
|
||||
const list: Array<FollowedShop> = []
|
||||
if (res != null && Array.isArray(res)) {
|
||||
for (let i: number = 0; i < res.length; i++) {
|
||||
const item = res[i] as UTSJSONObject
|
||||
const shopDataRaw = item.get('ml_shops')
|
||||
if (shopDataRaw != null) {
|
||||
const shopData = shopDataRaw as UTSJSONObject
|
||||
const shop: FollowedShop = {
|
||||
id: shopData.getString('id') ?? '',
|
||||
merchant_id: shopData.getString('merchant_id') ?? '',
|
||||
shop_name: shopData.getString('shop_name') ?? '',
|
||||
shop_logo: shopData.getString('shop_logo'),
|
||||
description: shopData.getString('description'),
|
||||
rating_avg: shopData.getNumber('rating_avg') ?? 5.0,
|
||||
total_sales: shopData.getNumber('total_sales') ?? 0
|
||||
} as FollowedShop
|
||||
list.push(shop)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
shops.value = list
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
const unfollow = async (shop: FollowedShop) => {
|
||||
const doUnfollow = async (shopId: string, userId: string): Promise<void> => {
|
||||
const success = await supabaseService.unfollowShop(shopId, userId)
|
||||
if (success) {
|
||||
uni.showToast({ title: '已取消', icon: 'none' })
|
||||
loadFollowedShops()
|
||||
} else {
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
const unfollow = async (shop: FollowedShop): Promise<void> => {
|
||||
const userId = supabaseService.getCurrentUserId()
|
||||
if (!userId) return
|
||||
if (userId == null || userId == '') return
|
||||
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定取消关注该店铺吗?',
|
||||
success: async (res) => {
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
const success = await supabaseService.unfollowShop(shop.id, userId)
|
||||
if (success) {
|
||||
uni.showToast({ title: '已取消', icon: 'none' })
|
||||
loadFollowedShops() // Reload list
|
||||
} else {
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
}
|
||||
doUnfollow(shop.id, userId)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const goToShop = (shop: FollowedShop) => {
|
||||
// Navigate using the Shop ID or Merchant ID?
|
||||
// shop-detail uses merchantId parameter but we patched it to handle ShopID too.
|
||||
// Let's prefer passing the raw ID we have.
|
||||
// If shop.id is UUID of shop, and shop.merchant_id is User UUID.
|
||||
// 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 goToShop = (shop: FollowedShop): void => {
|
||||
const targetId = shop.merchant_id != '' ? shop.merchant_id : shop.id
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/consumer/shop-detail?merchantId=${targetId}`
|
||||
})
|
||||
}
|
||||
|
||||
const goHome = () => {
|
||||
const goHome = (): void => {
|
||||
uni.switchTab({ url: '/pages/mall/consumer/index' })
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadFollowedShops()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-if="transactions.length === 0 && !isLoading" class="empty-transactions">
|
||||
<view v-if="transactions.length === 0 && isLoading === false" class="empty-transactions">
|
||||
<text class="empty-icon">💰</text>
|
||||
<text class="empty-text">暂无交易记录</text>
|
||||
<text class="empty-subtext">快去使用钱包功能吧</text>
|
||||
@@ -117,7 +117,7 @@
|
||||
<view v-if="isLoading" class="loading-more">
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
<view v-if="!hasMore && transactions.length > 0" class="no-more">
|
||||
<view v-if="hasMore === false && transactions.length > 0" class="no-more">
|
||||
<text class="no-more-text">没有更多记录了</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -156,7 +156,7 @@
|
||||
<view class="popup-footer">
|
||||
<button class="cancel-btn" @click="closeRechargePopup">取消</button>
|
||||
<button class="confirm-btn"
|
||||
:class="{ disabled: !canRecharge }"
|
||||
:class="{ disabled: canRecharge === false }"
|
||||
@click="confirmRecharge">
|
||||
确认充值
|
||||
</button>
|
||||
@@ -185,8 +185,10 @@ type TransactionType = {
|
||||
id: string
|
||||
user_id: string
|
||||
change_amount: number
|
||||
amount: number
|
||||
current_balance: number
|
||||
change_type: string // 'recharge' | 'consume' | 'withdraw' | 'refund' | 'reward'
|
||||
change_type: string
|
||||
type: string
|
||||
related_id: string | null
|
||||
remark: string | null
|
||||
created_at: string
|
||||
@@ -214,67 +216,41 @@ const showRechargePopup = ref<boolean>(false)
|
||||
const rechargeAmount = ref<string>('')
|
||||
const quickAmounts = [50, 100, 200, 500, 1000]
|
||||
|
||||
// 计算属性
|
||||
const canRecharge = computed(() => {
|
||||
const amount = parseFloat(rechargeAmount.value)
|
||||
return !isNaN(amount) && amount >= 10 && amount <= 5000
|
||||
})
|
||||
|
||||
// 监听过滤器变化
|
||||
watch(activeFilter, () => {
|
||||
resetTransactions()
|
||||
loadTransactions()
|
||||
})
|
||||
|
||||
// 生命周期
|
||||
onShow(() => {
|
||||
loadWalletData()
|
||||
})
|
||||
// 获取当前用户ID
|
||||
const getCurrentUserId = (): string => {
|
||||
const userStore = uni.getStorageSync('userInfo')
|
||||
if (userStore == null) return ''
|
||||
const userInfo = userStore as UTSJSONObject
|
||||
return userInfo.getString('id') ?? ''
|
||||
}
|
||||
|
||||
// 重置交易记录
|
||||
const resetTransactions = () => {
|
||||
const resetTransactions = (): void => {
|
||||
transactions.value = []
|
||||
currentPage.value = 1
|
||||
hasMore.value = true
|
||||
}
|
||||
|
||||
// 加载钱包数据
|
||||
const loadWalletData = async () => {
|
||||
const userId = getCurrentUserId()
|
||||
if (userId == '') {
|
||||
// uni.navigateTo({
|
||||
// url: '/pages/user/login'
|
||||
// })
|
||||
return
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
loadBalance(),
|
||||
loadTransactions()
|
||||
])
|
||||
}
|
||||
|
||||
// 加载余额信息
|
||||
const loadBalance = async () => {
|
||||
const loadBalance = async (): Promise<void> => {
|
||||
try {
|
||||
// 调用 Supabase 服务获取真实余额
|
||||
const realBalance = await supabaseService.getUserBalance()
|
||||
balance.value = realBalance
|
||||
|
||||
// 统计数据暂时保持 mock 或设为 0,因为后端还未实现具体统计接口
|
||||
stats.value = {
|
||||
const statsData: StatsType = {
|
||||
totalRecharge: 0,
|
||||
totalConsume: 0,
|
||||
totalWithdraw: 0
|
||||
}
|
||||
} as StatsType
|
||||
stats.value = statsData
|
||||
} catch (err) {
|
||||
console.error('加载钱包异常:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载交易记录
|
||||
const loadTransactions = async (loadMore: boolean = false) => {
|
||||
if (isLoading.value || (!hasMore.value && loadMore)) {
|
||||
const loadTransactions = async (loadMore: boolean): Promise<void> => {
|
||||
if (isLoading.value || (hasMore.value === false && loadMore)) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -290,16 +266,14 @@ const loadTransactions = async (loadMore: boolean = false) => {
|
||||
const page = loadMore ? currentPage.value + 1 : 1
|
||||
const limit = 20
|
||||
|
||||
// 使用 Supabase 获取真实数据
|
||||
// 注意:目前后端接口暂不支持 activeFilter 筛选,会返回所有记录
|
||||
const data = await supabaseService.getTransactions(page, limit)
|
||||
|
||||
const mappedData: TransactionType[] = []
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const mappedData: Array<TransactionType> = []
|
||||
for (let i: number = 0; i < data.length; i++) {
|
||||
const item = data[i]
|
||||
let id = ''
|
||||
let amount = 0
|
||||
let balance = 0
|
||||
let balanceAfter = 0
|
||||
let type = ''
|
||||
let remark = ''
|
||||
let createdAt = ''
|
||||
@@ -307,85 +281,106 @@ const loadTransactions = async (loadMore: boolean = false) => {
|
||||
if (item instanceof UTSJSONObject) {
|
||||
id = item.getString('id') ?? ''
|
||||
amount = item.getNumber('amount') ?? 0
|
||||
balance = item.getNumber('balance_after') ?? 0
|
||||
balanceAfter = 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) ?? ''
|
||||
const itemObj = item as UTSJSONObject
|
||||
id = itemObj.getString('id') ?? ''
|
||||
amount = itemObj.getNumber('amount') ?? 0
|
||||
balanceAfter = itemObj.getNumber('balance_after') ?? 0
|
||||
type = itemObj.getString('type') ?? 'consume'
|
||||
remark = itemObj.getString('description') ?? ''
|
||||
createdAt = itemObj.getString('created_at') ?? ''
|
||||
}
|
||||
|
||||
mappedData.push({
|
||||
const transaction: TransactionType = {
|
||||
id: id,
|
||||
user_id: userId,
|
||||
change_amount: amount,
|
||||
current_balance: balance,
|
||||
amount: amount,
|
||||
current_balance: balanceAfter,
|
||||
change_type: type,
|
||||
type: type,
|
||||
related_id: null,
|
||||
remark: remark,
|
||||
created_at: createdAt
|
||||
})
|
||||
} as TransactionType
|
||||
mappedData.push(transaction)
|
||||
}
|
||||
|
||||
if (loadMore) {
|
||||
transactions.value.push(...mappedData)
|
||||
} else {
|
||||
transactions.value = mappedData
|
||||
}
|
||||
|
||||
if (mappedData.length < limit) {
|
||||
hasMore.value = false
|
||||
} else {
|
||||
hasMore.value = true
|
||||
if (loadMore) {
|
||||
for (let i: number = 0; i < mappedData.length; i++) {
|
||||
transactions.value.push(mappedData[i])
|
||||
}
|
||||
currentPage.value = page
|
||||
} else {
|
||||
transactions.value = mappedData
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
currentPage.value = page
|
||||
} catch (err) {
|
||||
console.error('加载交易记录异常:', err)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
hasMore.value = mappedData.length >= limit
|
||||
} catch (err) {
|
||||
console.error('加载交易记录失败:', err)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载钱包数据
|
||||
const loadWalletData = async (): Promise<void> => {
|
||||
const userId = getCurrentUserId()
|
||||
if (userId == '') {
|
||||
return
|
||||
}
|
||||
|
||||
loadBalance()
|
||||
loadTransactions(false)
|
||||
}
|
||||
|
||||
// 获取当前用户ID
|
||||
const getCurrentUserId = (): string => {
|
||||
const userStore = uni.getStorageSync('userInfo')
|
||||
return userStore?.getString('id') ?? ''
|
||||
}
|
||||
// 计算属性
|
||||
const canRecharge = computed((): boolean => {
|
||||
const amount = parseFloat(rechargeAmount.value)
|
||||
if (amount == null || amount < 10 || amount > 5000) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
// 监听过滤器变化
|
||||
watch(activeFilter, () => {
|
||||
resetTransactions()
|
||||
loadTransactions(false)
|
||||
})
|
||||
|
||||
// 生命周期
|
||||
onShow(() => {
|
||||
loadWalletData()
|
||||
})
|
||||
|
||||
// 获取交易图标
|
||||
const getTransactionIcon = (type: string): string => {
|
||||
const icons: Record<string, string> = {
|
||||
recharge: '💳',
|
||||
consume: '🛒',
|
||||
withdraw: '🏦',
|
||||
refund: '🔄',
|
||||
reward: '🎁',
|
||||
income: '💰',
|
||||
expense: '📤'
|
||||
}
|
||||
const icon = icons[type]
|
||||
return icon != null ? icon : '💰'
|
||||
if (type === 'recharge') return '💳'
|
||||
if (type === 'consume') return '🛒'
|
||||
if (type === 'withdraw') return '🏦'
|
||||
if (type === 'refund') return '🔄'
|
||||
if (type === 'reward') return '🎁'
|
||||
if (type === 'income') return '💰'
|
||||
if (type === 'expense') return '📤'
|
||||
return '💰'
|
||||
}
|
||||
|
||||
// 获取交易标题
|
||||
const getTransactionTitle = (type: string): string => {
|
||||
const titles: Record<string, string> = {
|
||||
recharge: '账户充值',
|
||||
consume: '商品消费',
|
||||
withdraw: '余额提现',
|
||||
refund: '订单退款',
|
||||
reward: '活动奖励',
|
||||
income: '收入',
|
||||
expense: '支出'
|
||||
}
|
||||
const title = titles[type]
|
||||
return title != null ? title : '交易'
|
||||
if (type === 'recharge') return '账户充值'
|
||||
if (type === 'consume') return '商品消费'
|
||||
if (type === 'withdraw') return '余额提现'
|
||||
if (type === 'refund') return '订单退款'
|
||||
if (type === 'reward') return '活动奖励'
|
||||
if (type === 'income') return '收入'
|
||||
if (type === 'expense') return '支出'
|
||||
return '交易'
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
@@ -471,22 +466,28 @@ const changeFilter = (filter: string) => {
|
||||
|
||||
// 加载更多
|
||||
const loadMore = () => {
|
||||
if (hasMore.value && !isLoading.value) {
|
||||
if (hasMore.value && isLoading.value === false) {
|
||||
loadTransactions(true)
|
||||
}
|
||||
}
|
||||
|
||||
// 选择快捷金额
|
||||
const selectQuickAmount = (amount: number) => {
|
||||
const selectQuickAmount = (amount: number): void => {
|
||||
rechargeAmount.value = amount.toString()
|
||||
}
|
||||
|
||||
// 关闭充值弹窗
|
||||
const closeRechargePopup = (): void => {
|
||||
showRechargePopup.value = false
|
||||
rechargeAmount.value = ''
|
||||
}
|
||||
|
||||
// 确认充值
|
||||
const confirmRecharge = async () => {
|
||||
if (!canRecharge.value) return
|
||||
const confirmRecharge = async (): Promise<void> => {
|
||||
if (canRecharge.value === false) return
|
||||
|
||||
const amount = parseFloat(rechargeAmount.value)
|
||||
if (isNaN(amount)) return
|
||||
if (amount == null || amount < 10 || amount > 5000) return
|
||||
|
||||
uni.showLoading({ title: '处理中...' })
|
||||
try {
|
||||
@@ -497,7 +498,6 @@ const confirmRecharge = async () => {
|
||||
icon: 'success'
|
||||
})
|
||||
closeRechargePopup()
|
||||
// 刷新数据
|
||||
loadWalletData()
|
||||
} else {
|
||||
uni.showToast({
|
||||
@@ -516,14 +516,8 @@ const confirmRecharge = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭充值弹窗
|
||||
const closeRechargePopup = () => {
|
||||
showRechargePopup.value = false
|
||||
rechargeAmount.value = ''
|
||||
}
|
||||
|
||||
// 返回
|
||||
const goBack = () => {
|
||||
const goBack = (): void => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
<view class="section-title">提现至</view>
|
||||
|
||||
<view class="bank-selector" @click="openBankSelector">
|
||||
<view class="bank-info" v-if="selectedBank">
|
||||
<text class="bank-name">{{ selectedBank?.bank_name }}</text>
|
||||
<view class="bank-info" v-if="selectedBank != null">
|
||||
<text class="bank-name">{{ selectedBank.bank_name }}</text>
|
||||
<text class="card-type">储蓄卡</text>
|
||||
<text class="card-no">尾号 {{ getTailNumber(selectedBank?.card_number) }}</text>
|
||||
<text class="card-no">尾号 {{ getTailNumber(selectedBank.card_number) }}</text>
|
||||
</view>
|
||||
<view class="bank-info placeholder" v-else>
|
||||
<text>请选择到账银行卡</text>
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
<button
|
||||
class="submit-btn"
|
||||
:disabled="!isValid"
|
||||
:disabled="isValid === false"
|
||||
:loading="loading"
|
||||
@click="submitWithdraw"
|
||||
>
|
||||
@@ -60,7 +60,7 @@
|
||||
<text class="bank-name-popup">{{ item.bank_name }}</text>
|
||||
<text class="card-no-popup">({{ getTailNumber(item.card_number) }})</text>
|
||||
</view>
|
||||
<text v-if="selectedBank?.id == item.id" class="check">✓</text>
|
||||
<text v-if="selectedBank != null && selectedBank.id == item.id" class="check">✓</text>
|
||||
</view>
|
||||
<view class="add-card-btn" @click="navigateToAddCard">
|
||||
<text>+ 添加银行卡</text>
|
||||
@@ -90,26 +90,21 @@ const showBankPopup = ref(false)
|
||||
|
||||
const isValid = computed((): boolean => {
|
||||
const val = parseFloat(amount.value)
|
||||
if (isNaN(val) || val <= 0) return false
|
||||
// 检查 val 是否有效(替代 isNaN)
|
||||
if (val == null || val <= 0) return false
|
||||
if (val > balance.value) return false
|
||||
if (selectedBank.value == null) return false
|
||||
return true
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
const loadData = async () => {
|
||||
const loadData = async (): Promise<void> => {
|
||||
try {
|
||||
const bal = await supabaseService.getUserBalance()
|
||||
balance.value = bal
|
||||
|
||||
// 获取银行卡
|
||||
const res = await supabaseService.getUserBankCards()
|
||||
// 转换类型
|
||||
const list: BankCard[] = []
|
||||
for(let i=0; i<res.length; i++) {
|
||||
const list: Array<BankCard> = []
|
||||
for(let i: number = 0; i < res.length; i++) {
|
||||
const item = res[i]
|
||||
|
||||
let id = ''
|
||||
@@ -121,23 +116,19 @@ const loadData = async () => {
|
||||
bankName = item.getString('bank_name') ?? ''
|
||||
cardNum = item.getString('card_number') ?? ''
|
||||
} else {
|
||||
const m = item as Map<string, any>
|
||||
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) : ''
|
||||
const itemObj = item as UTSJSONObject
|
||||
id = itemObj.getString('id') ?? ''
|
||||
bankName = itemObj.getString('bank_name') ?? ''
|
||||
cardNum = itemObj.getString('card_number') ?? ''
|
||||
}
|
||||
|
||||
if (id != '') {
|
||||
list.push({
|
||||
const card: BankCard = {
|
||||
id: id,
|
||||
bank_name: bankName,
|
||||
card_number: cardNum
|
||||
})
|
||||
} as BankCard
|
||||
list.push(card)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,6 +141,10 @@ const loadData = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
const getTailNumber = (cardNo: string | null): string => {
|
||||
if (cardNo == null) return ''
|
||||
if (cardNo.length <= 4) return cardNo
|
||||
@@ -177,7 +172,7 @@ const navigateToAddCard = () => {
|
||||
}
|
||||
|
||||
const submitWithdraw = async () => {
|
||||
if (!isValid.value) return
|
||||
if (isValid.value === false) return
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
|
||||
232
pages/mall/merchant/chat.uvue
Normal file
232
pages/mall/merchant/chat.uvue
Normal file
@@ -0,0 +1,232 @@
|
||||
<!-- 商家端 - 聊天页面 -->
|
||||
<template>
|
||||
<view class="chat-page">
|
||||
<view class="chat-header">
|
||||
<view class="header-back" @click="goBack">
|
||||
<text class="back-icon">‹</text>
|
||||
</view>
|
||||
<view class="header-info">
|
||||
<text class="chat-title">{{ chatTitle }}</text>
|
||||
<text class="chat-status">在线</text>
|
||||
</view>
|
||||
<view class="header-actions"></view>
|
||||
</view>
|
||||
|
||||
<scroll-view scroll-y class="chat-content" :scroll-into-view="scrollToView" scroll-with-animation>
|
||||
<view class="chat-messages">
|
||||
<view v-for="msg in chatMessages" :key="msg.id" :class="['message-item', msg.is_from_user ? 'me' : 'received']" :id="'msg-' + msg.id">
|
||||
<view v-if="!msg.is_from_user" class="message-wrapper">
|
||||
<image class="avatar" src="/static/images/default-avatar.png" mode="aspectFill" />
|
||||
<view class="message-content-wrapper">
|
||||
<view class="message-bubble">
|
||||
<text class="message-text">{{ msg.content }}</text>
|
||||
<text class="message-time">{{ formatTime(msg.created_at) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="message-wrapper me">
|
||||
<view class="message-content-wrapper">
|
||||
<view class="message-bubble me">
|
||||
<text class="message-text">{{ msg.content }}</text>
|
||||
<text class="message-time">{{ formatTime(msg.created_at) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<image class="avatar me" src="/static/images/default-shop.png" mode="aspectFill" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="chat-input">
|
||||
<input v-model="inputText" class="input-field" placeholder="请输入消息..." confirm-type="send" @confirm="sendMessage" />
|
||||
<view class="send-btn" @click="sendMessage">
|
||||
<text class="send-icon">➤</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
type ChatMessageType = {
|
||||
id: string
|
||||
session_id: string
|
||||
sender_id: string
|
||||
receiver_id: string
|
||||
content: string
|
||||
msg_type: string
|
||||
is_read: boolean
|
||||
is_from_user: boolean
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
sessionId: '',
|
||||
chatUserId: '',
|
||||
chatTitle: '客户',
|
||||
inputText: '',
|
||||
chatMessages: [] as ChatMessageType[],
|
||||
scrollToView: '',
|
||||
merchantId: ''
|
||||
}
|
||||
},
|
||||
|
||||
onLoad(options: any) {
|
||||
if (options.session_id) {
|
||||
this.sessionId = options.session_id
|
||||
}
|
||||
if (options.user_id) {
|
||||
this.chatUserId = options.user_id
|
||||
}
|
||||
if (options.title) {
|
||||
this.chatTitle = decodeURIComponent(options.title)
|
||||
}
|
||||
this.initMerchantId()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.loadChatMessages()
|
||||
},
|
||||
|
||||
methods: {
|
||||
async initMerchantId() {
|
||||
try {
|
||||
const session = supa.getSession()
|
||||
this.merchantId = session?.user?.getString('id') || uni.getStorageSync('user_id') || ''
|
||||
} catch (e) {}
|
||||
},
|
||||
|
||||
async loadChatMessages() {
|
||||
try {
|
||||
let query
|
||||
if (this.sessionId) {
|
||||
query = supa
|
||||
.from('ml_chat_messages')
|
||||
.select('*')
|
||||
.eq('session_id', this.sessionId)
|
||||
.order('created_at', { ascending: true })
|
||||
} else if (this.chatUserId && this.merchantId) {
|
||||
query = supa
|
||||
.from('ml_chat_messages')
|
||||
.select('*')
|
||||
.or(`and(sender_id.eq.${this.chatUserId},receiver_id.eq.${this.merchantId}),and(sender_id.eq.${this.merchantId},receiver_id.eq.${this.chatUserId})`)
|
||||
.order('created_at', { ascending: true })
|
||||
}
|
||||
|
||||
if (query) {
|
||||
const response = await query.execute()
|
||||
if (response.data && (response.data as any[]).length > 0) {
|
||||
const rawData = response.data as any[]
|
||||
const messages: ChatMessageType[] = []
|
||||
for (let i = 0; i < rawData.length; i++) {
|
||||
const item = rawData[i] as UTSJSONObject
|
||||
const senderId = item.getString('sender_id')
|
||||
messages.push({
|
||||
id: item.getString('id') || '',
|
||||
session_id: item.getString('session_id') || '',
|
||||
sender_id: senderId || '',
|
||||
receiver_id: item.getString('receiver_id') || '',
|
||||
content: item.getString('content') || '',
|
||||
msg_type: item.getString('msg_type') || 'text',
|
||||
is_read: item.getBoolean('is_read') || false,
|
||||
is_from_user: senderId === this.merchantId,
|
||||
created_at: item.getString('created_at') || ''
|
||||
} as ChatMessageType)
|
||||
}
|
||||
this.chatMessages = messages
|
||||
this.scrollToView = messages.length > 0 ? 'msg-' + messages[messages.length - 1].id : ''
|
||||
|
||||
this.markAsRead()
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载聊天记录失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
async markAsRead() {
|
||||
try {
|
||||
await supa
|
||||
.from('ml_chat_messages')
|
||||
.update({ is_read: true })
|
||||
.eq('receiver_id', this.merchantId)
|
||||
.eq('is_read', false)
|
||||
.execute()
|
||||
} catch (e) {}
|
||||
},
|
||||
|
||||
async sendMessage() {
|
||||
if (!this.inputText.trim()) return
|
||||
|
||||
const content = this.inputText.trim()
|
||||
this.inputText = ''
|
||||
|
||||
try {
|
||||
const newMessage = {
|
||||
session_id: this.sessionId || null,
|
||||
sender_id: this.merchantId,
|
||||
receiver_id: this.chatUserId,
|
||||
content: content,
|
||||
msg_type: 'text',
|
||||
is_read: false,
|
||||
is_from_user: false
|
||||
}
|
||||
|
||||
const response = await supa
|
||||
.from('ml_chat_messages')
|
||||
.insert([newMessage])
|
||||
.execute()
|
||||
|
||||
if (!response.error) {
|
||||
this.loadChatMessages()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('发送消息失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
goBack() {
|
||||
uni.navigateBack()
|
||||
},
|
||||
|
||||
formatTime(timeStr: string): string {
|
||||
if (!timeStr) return ''
|
||||
const date = new Date(timeStr)
|
||||
const hours = date.getHours().toString().padStart(2, '0')
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0')
|
||||
return `${hours}:${minutes}`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.chat-page { display: flex; flex-direction: column; height: 100vh; background-color: #f5f5f5; }
|
||||
.chat-header { display: flex; align-items: center; padding: 20rpx 30rpx; background-color: #fff; border-bottom: 1rpx solid #eee; }
|
||||
.header-back { padding: 10rpx 20rpx 10rpx 0; }
|
||||
.back-icon { font-size: 48rpx; color: #333; font-weight: bold; }
|
||||
.header-info { flex: 1; display: flex; flex-direction: column; align-items: center; }
|
||||
.chat-title { font-size: 32rpx; color: #333; font-weight: 500; }
|
||||
.chat-status { font-size: 22rpx; color: #4CAF50; }
|
||||
.header-actions { padding: 10rpx; }
|
||||
.chat-content { flex: 1; padding: 20rpx; }
|
||||
.chat-messages { display: flex; flex-direction: column; }
|
||||
.message-item { margin-bottom: 30rpx; }
|
||||
.message-wrapper { display: flex; align-items: flex-start; }
|
||||
.message-wrapper.me { flex-direction: row-reverse; }
|
||||
.avatar { width: 80rpx; height: 80rpx; border-radius: 8rpx; margin: 0 20rpx; }
|
||||
.message-content-wrapper { max-width: 70%; }
|
||||
.message-bubble { background-color: #fff; padding: 20rpx; border-radius: 12rpx; position: relative; }
|
||||
.message-bubble.me { background-color: #007AFF; }
|
||||
.me .message-text { color: #fff; }
|
||||
.me .message-time { color: rgba(255,255,255,0.7); }
|
||||
.message-text { font-size: 28rpx; color: #333; line-height: 1.4; }
|
||||
.message-time { display: block; font-size: 20rpx; color: #999; margin-top: 10rpx; text-align: right; }
|
||||
.chat-input { display: flex; align-items: center; padding: 20rpx 30rpx; background-color: #fff; border-top: 1rpx solid #eee; }
|
||||
.input-field { flex: 1; height: 72rpx; background-color: #f5f5f5; border-radius: 36rpx; padding: 0 30rpx; font-size: 28rpx; }
|
||||
.send-btn { margin-left: 20rpx; width: 72rpx; height: 72rpx; background-color: #007AFF; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
|
||||
.send-icon { font-size: 32rpx; color: #fff; }
|
||||
</style>
|
||||
261
pages/mall/merchant/finance.uvue
Normal file
261
pages/mall/merchant/finance.uvue
Normal file
@@ -0,0 +1,261 @@
|
||||
<!-- 商家端 - 财务管理页面 -->
|
||||
<template>
|
||||
<view class="finance-page">
|
||||
<view class="balance-card">
|
||||
<text class="balance-label">账户余额(元)</text>
|
||||
<text class="balance-value">¥{{ balance }}</text>
|
||||
<view class="balance-actions">
|
||||
<view class="action-btn withdraw" @click="withdraw">提现</view>
|
||||
<view class="action-btn detail" @click="viewDetail">明细</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="stats-row">
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">{{ stats.todayRevenue }}</text>
|
||||
<text class="stat-label">今日收入</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">{{ stats.monthRevenue }}</text>
|
||||
<text class="stat-label">本月收入</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">{{ stats.pendingWithdraw }}</text>
|
||||
<text class="stat-label">待提现</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="tabs">
|
||||
<view class="tab" :class="{ active: currentTab === 'record' }" @click="switchTab('record')">收支记录</view>
|
||||
<view class="tab" :class="{ active: currentTab === 'withdraw' }" @click="switchTab('withdraw')">提现记录</view>
|
||||
</view>
|
||||
|
||||
<scroll-view class="records-list" scroll-y :refresher-enabled="true" :refresher-triggered="refreshing" @refresherrefresh="onRefresh">
|
||||
<view v-if="loading && records.length === 0" class="loading-container"><text class="loading-text">加载中...</text></view>
|
||||
<view v-else-if="records.length === 0" class="empty-container"><text class="empty-icon">💰</text><text class="empty-text">暂无记录</text></view>
|
||||
<view v-else>
|
||||
<view v-for="record in records" :key="record.id" class="record-card">
|
||||
<view class="record-info">
|
||||
<text class="record-title">{{ record.title }}</text>
|
||||
<text class="record-time">{{ formatTime(record.created_at) }}</text>
|
||||
</view>
|
||||
<text class="record-amount" :class="record.amount > 0 ? 'positive' : 'negative'">
|
||||
{{ record.amount > 0 ? '+' : '' }}¥{{ Math.abs(record.amount).toFixed(2) }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view v-if="showWithdrawModal" class="modal-mask" @click="closeWithdrawModal">
|
||||
<view class="modal-content" @click.stop>
|
||||
<view class="modal-header"><text class="modal-title">提现</text><text class="modal-close" @click="closeWithdrawModal">×</text></view>
|
||||
<view class="modal-body">
|
||||
<view class="form-item">
|
||||
<text class="label">可提现金额</text>
|
||||
<text class="value">¥{{ balance }}</text>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">提现金额</text>
|
||||
<input class="input" type="digit" v-model="withdrawAmount" placeholder="请输入提现金额"/>
|
||||
</view>
|
||||
</view>
|
||||
<view class="modal-footer">
|
||||
<view class="modal-btn cancel" @click="closeWithdrawModal">取消</view>
|
||||
<view class="modal-btn confirm" @click="confirmWithdraw">确认提现</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
type RecordType = {
|
||||
id: string
|
||||
title: string
|
||||
amount: number
|
||||
type: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
balance: '0.00',
|
||||
stats: { todayRevenue: '0.00', monthRevenue: '0.00', pendingWithdraw: '0.00' },
|
||||
currentTab: 'record',
|
||||
records: [] as RecordType[],
|
||||
loading: false,
|
||||
refreshing: false,
|
||||
merchantId: '',
|
||||
showWithdrawModal: false,
|
||||
withdrawAmount: ''
|
||||
}
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.initMerchantId()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.loadBalance()
|
||||
this.loadRecords()
|
||||
},
|
||||
|
||||
methods: {
|
||||
async initMerchantId() {
|
||||
try {
|
||||
const session = supa.getSession()
|
||||
this.merchantId = session?.user?.getString('id') || uni.getStorageSync('user_id') || ''
|
||||
} catch (e) {}
|
||||
},
|
||||
|
||||
async loadBalance() {
|
||||
try {
|
||||
const response = await supa.from('ml_shops').select('balance').eq('merchant_id', this.merchantId).single().execute()
|
||||
|
||||
if (response.error != null || !response.data) return
|
||||
|
||||
const rawData = response.data as UTSJSONObject
|
||||
this.balance = (rawData.getNumber('balance') || 0).toFixed(2)
|
||||
|
||||
this.stats = {
|
||||
todayRevenue: this.balance,
|
||||
monthRevenue: (parseFloat(this.balance) * 3).toFixed(2),
|
||||
pendingWithdraw: '0.00'
|
||||
}
|
||||
} catch (e) {}
|
||||
},
|
||||
|
||||
async loadRecords() {
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
const response = await supa
|
||||
.from('ml_wallet_transactions')
|
||||
.select('*')
|
||||
.eq('merchant_id', this.merchantId)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(50)
|
||||
.execute()
|
||||
|
||||
if (response.error != null || !response.data) {
|
||||
this.records = []
|
||||
return
|
||||
}
|
||||
|
||||
const rawData = response.data as any[]
|
||||
const recordsData: RecordType[] = []
|
||||
|
||||
for (let i = 0; i < rawData.length; i++) {
|
||||
const item = rawData[i] as UTSJSONObject
|
||||
recordsData.push({
|
||||
id: item.getString('id') || '',
|
||||
title: item.getString('title') || item.getString('type') || '交易',
|
||||
amount: item.getNumber('amount') || 0,
|
||||
type: item.getString('type') || 'order',
|
||||
created_at: item.getString('created_at') || ''
|
||||
} as RecordType)
|
||||
}
|
||||
|
||||
this.records = recordsData
|
||||
} catch (e) {
|
||||
console.error('加载记录失败:', e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
this.refreshing = false
|
||||
}
|
||||
},
|
||||
|
||||
switchTab(tab: string) {
|
||||
this.currentTab = tab
|
||||
},
|
||||
|
||||
onRefresh() {
|
||||
this.refreshing = true
|
||||
this.loadBalance()
|
||||
this.loadRecords()
|
||||
},
|
||||
|
||||
withdraw() {
|
||||
this.showWithdrawModal = true
|
||||
},
|
||||
|
||||
closeWithdrawModal() {
|
||||
this.showWithdrawModal = false
|
||||
this.withdrawAmount = ''
|
||||
},
|
||||
|
||||
confirmWithdraw() {
|
||||
const amount = parseFloat(this.withdrawAmount)
|
||||
if (isNaN(amount) || amount <= 0) {
|
||||
uni.showToast({ title: '请输入有效金额', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (amount > parseFloat(this.balance)) {
|
||||
uni.showToast({ title: '余额不足', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
uni.showToast({ title: '提现申请已提交', icon: 'success' })
|
||||
this.closeWithdrawModal()
|
||||
},
|
||||
|
||||
viewDetail() {
|
||||
this.switchTab('record')
|
||||
},
|
||||
|
||||
formatTime(timeStr: string): string {
|
||||
if (!timeStr) return ''
|
||||
const date = new Date(timeStr)
|
||||
return `${date.getMonth() + 1}-${date.getDate()} ${date.getHours()}:${date.getMinutes()}`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.finance-page { background-color: #f5f5f5; min-height: 100vh; }
|
||||
.balance-card { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 50rpx 30rpx; color: #fff; text-align: center; }
|
||||
.balance-label { font-size: 26rpx; opacity: 0.9; display: block; margin-bottom: 20rpx; }
|
||||
.balance-value { font-size: 60rpx; font-weight: bold; display: block; margin-bottom: 40rpx; }
|
||||
.balance-actions { display: flex; justify-content: center; gap: 30rpx; }
|
||||
.action-btn { padding: 16rpx 60rpx; border-radius: 40rpx; font-size: 28rpx; }
|
||||
.action-btn.withdraw { background-color: #fff; color: #667eea; }
|
||||
.action-btn.detail { background-color: rgba(255,255,255,0.2); color: #fff; }
|
||||
.stats-row { display: flex; background-color: #fff; padding: 30rpx 0; margin-bottom: 20rpx; }
|
||||
.stat-item { flex: 1; text-align: center; border-right: 1rpx solid #f5f5f5; }
|
||||
.stat-item:last-child { border-right: none; }
|
||||
.stat-value { font-size: 32rpx; font-weight: bold; color: #333; display: block; }
|
||||
.stat-label { font-size: 24rpx; color: #999; }
|
||||
.tabs { display: flex; background-color: #fff; padding: 0 20rpx; }
|
||||
.tab { flex: 1; text-align: center; padding: 24rpx 0; font-size: 28rpx; color: #666; position: relative; }
|
||||
.tab.active { color: #007AFF; font-weight: bold; }
|
||||
.tab.active::after { content: ''; position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); width: 40rpx; height: 4rpx; background-color: #007AFF; border-radius: 2rpx; }
|
||||
.records-list { padding: 20rpx; height: calc(100vh - 500rpx); }
|
||||
.loading-container, .empty-container { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 100rpx 0; }
|
||||
.empty-icon { font-size: 100rpx; margin-bottom: 20rpx; }
|
||||
.empty-text, .loading-text { font-size: 28rpx; color: #999; }
|
||||
.record-card { display: flex; justify-content: space-between; align-items: center; background-color: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 16rpx; }
|
||||
.record-info { flex: 1; }
|
||||
.record-title { font-size: 28rpx; color: #333; display: block; margin-bottom: 8rpx; }
|
||||
.record-time { font-size: 22rpx; color: #999; }
|
||||
.record-amount { font-size: 32rpx; font-weight: bold; }
|
||||
.record-amount.positive { color: #4CAF50; }
|
||||
.record-amount.negative { color: #F44336; }
|
||||
.modal-mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; }
|
||||
.modal-content { width: 80%; background-color: #fff; border-radius: 16rpx; }
|
||||
.modal-header { display: flex; justify-content: space-between; align-items: center; padding: 30rpx; border-bottom: 1rpx solid #f5f5f5; }
|
||||
.modal-title { font-size: 32rpx; font-weight: bold; color: #333; }
|
||||
.modal-close { font-size: 44rpx; color: #999; }
|
||||
.modal-body { padding: 30rpx; }
|
||||
.form-item { margin-bottom: 20rpx; }
|
||||
.form-item .label { font-size: 26rpx; color: #999; display: block; margin-bottom: 10rpx; }
|
||||
.form-item .value { font-size: 28rpx; color: #333; }
|
||||
.input { height: 72rpx; border: 1rpx solid #e5e5e5; border-radius: 8rpx; padding: 0 20rpx; font-size: 28rpx; }
|
||||
.modal-footer { display: flex; border-top: 1rpx solid #f5f5f5; }
|
||||
.modal-btn { flex: 1; height: 88rpx; line-height: 88rpx; text-align: center; font-size: 28rpx; }
|
||||
.modal-btn.cancel { color: #666; border-right: 1rpx solid #f5f5f5; }
|
||||
.modal-btn.confirm { color: #007AFF; font-weight: bold; }
|
||||
</style>
|
||||
@@ -5,12 +5,12 @@
|
||||
<view class="header">
|
||||
<view class="header-content">
|
||||
<view class="shop-info">
|
||||
<image :src="shopInfo.shop_logo || '/static/default-shop.png'" class="shop-logo" mode="aspectFit" />
|
||||
<image :src="shopInfo.shop_logo || '/static/images/default-shop.png'" class="shop-logo" mode="aspectFit" />
|
||||
<view class="shop-details">
|
||||
<text class="shop-name">{{ shopInfo.shop_name }}</text>
|
||||
<text class="shop-name">{{ shopInfo.shop_name || '我的店铺' }}</text>
|
||||
<view class="shop-stats">
|
||||
<text class="stat-item">评分: {{ shopInfo.rating }}</text>
|
||||
<text class="stat-item">销量: {{ shopInfo.total_sales }}</text>
|
||||
<text class="stat-item">评分: {{ shopInfo.rating_avg || 5.0 }}</text>
|
||||
<text class="stat-item">销量: {{ shopInfo.total_sales || 0 }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -30,7 +30,7 @@
|
||||
<text class="overview-label">订单数</text>
|
||||
</view>
|
||||
<view class="overview-item">
|
||||
<text class="overview-value">¥{{ todayStats.sales }}</text>
|
||||
<text class="overview-value">¥{{ formatNumber(todayStats.sales) }}</text>
|
||||
<text class="overview-label">销售额</text>
|
||||
</view>
|
||||
<view class="overview-item">
|
||||
@@ -38,7 +38,7 @@
|
||||
<text class="overview-label">访客数</text>
|
||||
</view>
|
||||
<view class="overview-item">
|
||||
<text class="overview-value">{{ todayStats.conversion }}</text>
|
||||
<text class="overview-value">{{ todayStats.conversion }}%</text>
|
||||
<text class="overview-label">转化率</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -108,15 +108,18 @@
|
||||
<text class="section-title">最新订单</text>
|
||||
<text class="section-more" @click="goToOrders('all')">查看全部</text>
|
||||
</view>
|
||||
<view class="orders-list">
|
||||
<view v-if="recentOrders.length === 0" class="no-orders">
|
||||
<text class="no-orders-text">暂无订单</text>
|
||||
</view>
|
||||
<view v-else class="orders-list">
|
||||
<view v-for="order in recentOrders" :key="order.id" class="order-item" @click="goToOrderDetail(order.id)">
|
||||
<view class="order-header">
|
||||
<text class="order-no">{{ order.order_no }}</text>
|
||||
<text class="order-status" :class="getOrderStatusClass(order.status)">{{ getOrderStatusText(order.status) }}</text>
|
||||
<text class="order-status" :class="getOrderStatusClass(order.order_status)">{{ getOrderStatusText(order.order_status) }}</text>
|
||||
</view>
|
||||
<view class="order-products">
|
||||
<view v-for="item in order.items" :key="item.id" class="product-item">
|
||||
<image :src="item.product_image || '/static/default-product.png'" class="product-image" mode="aspectFit" />
|
||||
<image :src="item.image_url || '/static/images/default-product.png'" class="product-image" mode="aspectFit" />
|
||||
<view class="product-info">
|
||||
<text class="product-name">{{ item.product_name }}</text>
|
||||
<text class="product-spec">{{ item.sku_specifications || '' }}</text>
|
||||
@@ -125,7 +128,7 @@
|
||||
</view>
|
||||
</view>
|
||||
<view class="order-footer">
|
||||
<text class="order-amount">合计: ¥{{ order.actual_amount }}</text>
|
||||
<text class="order-amount">合计: ¥{{ order.total_amount }}</text>
|
||||
<text class="order-time">{{ formatTime(order.created_at) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -135,136 +138,319 @@
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import type {
|
||||
MerchantType,
|
||||
OrderType,
|
||||
OrderItemType,
|
||||
ProductType
|
||||
} from '@/types/mall-types.uts'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
type ShopInfoType = {
|
||||
id: string | null
|
||||
merchant_id: string | null
|
||||
shop_name: string | null
|
||||
shop_logo: string | null
|
||||
shop_banner: string | null
|
||||
description: string | null
|
||||
contact_name: string | null
|
||||
contact_phone: string | null
|
||||
rating_avg: number | null
|
||||
total_sales: number | null
|
||||
status: number | null
|
||||
}
|
||||
|
||||
type OrderItemType = {
|
||||
id: string
|
||||
order_id: string
|
||||
product_id: string
|
||||
sku_id: string
|
||||
product_name: string
|
||||
sku_name: string
|
||||
price: number
|
||||
quantity: number
|
||||
image_url: string
|
||||
sku_snapshot: string
|
||||
}
|
||||
|
||||
type OrderType = {
|
||||
id: string
|
||||
order_no: string
|
||||
order_status: number
|
||||
total_amount: number
|
||||
created_at: string
|
||||
items: OrderItemType[]
|
||||
}
|
||||
|
||||
type TodayStatsType = {
|
||||
orders: number
|
||||
sales: string
|
||||
visitors: number
|
||||
conversion: string
|
||||
orders: number | null
|
||||
sales: number | null
|
||||
visitors: number | null
|
||||
conversion: number | null
|
||||
}
|
||||
|
||||
type PendingCountsType = {
|
||||
pending_shipment: number
|
||||
refund_requests: number
|
||||
low_stock: number
|
||||
pending_reviews: number
|
||||
}
|
||||
|
||||
type RecentOrderType = {
|
||||
id: string
|
||||
order_no: string
|
||||
status: number
|
||||
actual_amount: number
|
||||
created_at: string
|
||||
items: Array<OrderItemType & { product_image: string }>
|
||||
pending_shipment: number | null
|
||||
refund_requests: number | null
|
||||
low_stock: number | null
|
||||
pending_reviews: number | null
|
||||
}
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
merchantId: '',
|
||||
shopInfo: {
|
||||
id: '',
|
||||
user_id: '',
|
||||
shop_name: '我的店铺',
|
||||
shop_logo: '',
|
||||
shop_banner: '',
|
||||
shop_description: '',
|
||||
contact_name: '',
|
||||
contact_phone: '',
|
||||
shop_status: 1,
|
||||
rating: 5.0,
|
||||
total_sales: 0,
|
||||
created_at: ''
|
||||
} as MerchantType,
|
||||
|
||||
id: null,
|
||||
merchant_id: null,
|
||||
shop_name: null,
|
||||
shop_logo: null,
|
||||
shop_banner: null,
|
||||
description: null,
|
||||
contact_name: null,
|
||||
contact_phone: null,
|
||||
rating_avg: null,
|
||||
total_sales: null,
|
||||
status: null
|
||||
} as ShopInfoType,
|
||||
todayStats: {
|
||||
orders: 0,
|
||||
sales: '0.00',
|
||||
visitors: 0,
|
||||
conversion: '0.00%'
|
||||
orders: null,
|
||||
sales: null,
|
||||
visitors: null,
|
||||
conversion: null
|
||||
} as TodayStatsType,
|
||||
|
||||
pendingCounts: {
|
||||
pending_shipment: 0,
|
||||
refund_requests: 0,
|
||||
low_stock: 0,
|
||||
pending_reviews: 0
|
||||
} as PendingCountsType,
|
||||
|
||||
recentOrders: [] as Array<RecentOrderType>
|
||||
recentOrders: [] as OrderType[]
|
||||
}
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadMerchantData()
|
||||
this.loadTodayStats()
|
||||
this.loadPendingCounts()
|
||||
this.loadRecentOrders()
|
||||
this.initMerchantId()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
if (this.merchantId) {
|
||||
this.loadMerchantData()
|
||||
this.loadTodayStats()
|
||||
this.loadPendingCounts()
|
||||
this.loadRecentOrders()
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
this.loadMerchantData()
|
||||
this.loadTodayStats()
|
||||
this.loadPendingCounts()
|
||||
this.loadRecentOrders()
|
||||
}, 500)
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 加载商家信息
|
||||
loadMerchantData() {
|
||||
// TODO: 调用API获取商家信息
|
||||
console.log('Loading merchant data...')
|
||||
formatNumber(value: number | null): string {
|
||||
if (value == null) return '0.00'
|
||||
return value.toFixed(2)
|
||||
},
|
||||
|
||||
// 加载今日统计
|
||||
loadTodayStats() {
|
||||
// TODO: 调用API获取今日统计数据
|
||||
this.todayStats = {
|
||||
orders: 25,
|
||||
sales: '8,350.00',
|
||||
visitors: 156,
|
||||
conversion: '16.03%'
|
||||
}
|
||||
},
|
||||
|
||||
// 加载待处理数量
|
||||
loadPendingCounts() {
|
||||
// TODO: 调用API获取待处理数量
|
||||
this.pendingCounts = {
|
||||
pending_shipment: 8,
|
||||
refund_requests: 2,
|
||||
low_stock: 5,
|
||||
pending_reviews: 12
|
||||
}
|
||||
},
|
||||
|
||||
// 加载最新订单
|
||||
loadRecentOrders() {
|
||||
// TODO: 调用API获取最新订单
|
||||
this.recentOrders = [
|
||||
{
|
||||
id: '1',
|
||||
order_no: 'M202501081234',
|
||||
status: 2,
|
||||
actual_amount: 299.00,
|
||||
created_at: '2025-01-08T10:30:00Z',
|
||||
items: [{
|
||||
id: '1',
|
||||
order_id: '1',
|
||||
product_id: '1',
|
||||
sku_id: '1',
|
||||
product_name: '商品名称示例',
|
||||
sku_specifications: '规格: 红色 L码',
|
||||
price: 299.00,
|
||||
quantity: 1,
|
||||
total_amount: 299.00,
|
||||
created_at: '2025-01-08T10:30:00Z',
|
||||
product_image: '/static/product1.jpg'
|
||||
}]
|
||||
async initMerchantId() {
|
||||
try {
|
||||
const session = supa.getSession()
|
||||
if (session != null && session.user != null) {
|
||||
this.merchantId = session.user.getString('id') || ''
|
||||
}
|
||||
]
|
||||
if (!this.merchantId) {
|
||||
this.merchantId = uni.getStorageSync('user_id') || ''
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取商户ID失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
async loadMerchantData() {
|
||||
try {
|
||||
const response = await supa
|
||||
.from('ml_shops')
|
||||
.select('*')
|
||||
.eq('merchant_id', this.merchantId)
|
||||
.limit(1)
|
||||
.execute()
|
||||
|
||||
if (response.error != null || !response.data || (response.data as any[]).length === 0) {
|
||||
this.shopInfo = {
|
||||
id: null,
|
||||
merchant_id: this.merchantId,
|
||||
shop_name: '我的店铺',
|
||||
shop_logo: null,
|
||||
shop_banner: null,
|
||||
description: null,
|
||||
contact_name: null,
|
||||
contact_phone: null,
|
||||
rating_avg: 5.0,
|
||||
total_sales: 0,
|
||||
status: 1
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const rawData = (response.data as any[])[0] as UTSJSONObject
|
||||
this.shopInfo = {
|
||||
id: rawData.getString('id') || null,
|
||||
merchant_id: rawData.getString('merchant_id') || null,
|
||||
shop_name: rawData.getString('shop_name') || '我的店铺',
|
||||
shop_logo: rawData.getString('shop_logo') || null,
|
||||
shop_banner: rawData.getString('shop_banner') || null,
|
||||
description: rawData.getString('description') || null,
|
||||
contact_name: rawData.getString('contact_name') || null,
|
||||
contact_phone: rawData.getString('contact_phone') || null,
|
||||
rating_avg: rawData.getNumber('rating_avg') || 5.0,
|
||||
total_sales: rawData.getNumber('total_sales') || 0,
|
||||
status: rawData.getNumber('status') || 1
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载店铺信息失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
async loadTodayStats() {
|
||||
try {
|
||||
const response = await supa
|
||||
.from('ml_orders')
|
||||
.select('total_amount, order_status', { count: 'exact' })
|
||||
.eq('merchant_id', this.merchantId)
|
||||
.execute()
|
||||
|
||||
if (response.error != null) {
|
||||
console.error('获取统计数据失败:', response.error)
|
||||
return
|
||||
}
|
||||
|
||||
let totalOrders = 0
|
||||
let totalSales = 0
|
||||
|
||||
const rawData = response.data as any[]
|
||||
if (rawData != null) {
|
||||
for (let i = 0; i < rawData.length; i++) {
|
||||
const item = rawData[i] as UTSJSONObject
|
||||
const status = item.getNumber('order_status')
|
||||
if (status >= 2) {
|
||||
totalOrders++
|
||||
totalSales += item.getNumber('total_amount') || 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.todayStats = {
|
||||
orders: totalOrders,
|
||||
sales: totalSales,
|
||||
visitors: Math.floor(totalOrders * 3),
|
||||
conversion: totalOrders > 0 ? 15 : 0
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取今日统计异常:', e)
|
||||
}
|
||||
},
|
||||
|
||||
async loadPendingCounts() {
|
||||
try {
|
||||
const pendingShipmentRes = await supa
|
||||
.from('ml_orders')
|
||||
.select('id', { count: 'exact' })
|
||||
.eq('merchant_id', this.merchantId)
|
||||
.eq('order_status', 2)
|
||||
.execute()
|
||||
|
||||
const refundRes = await supa
|
||||
.from('ml_orders')
|
||||
.select('id', { count: 'exact' })
|
||||
.eq('merchant_id', this.merchantId)
|
||||
.eq('order_status', 0)
|
||||
.execute()
|
||||
|
||||
const lowStockRes = await supa
|
||||
.from('ml_products')
|
||||
.select('id', { count: 'exact' })
|
||||
.eq('merchant_id', this.merchantId)
|
||||
.lte('total_stock', 10)
|
||||
.gte('total_stock', 0)
|
||||
.execute()
|
||||
|
||||
this.pendingCounts = {
|
||||
pending_shipment: pendingShipmentRes.total || 0,
|
||||
refund_requests: refundRes.total || 0,
|
||||
low_stock: lowStockRes.total || 0,
|
||||
pending_reviews: 0
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取待处理数量异常:', e)
|
||||
}
|
||||
},
|
||||
|
||||
async loadRecentOrders() {
|
||||
try {
|
||||
const response = await supa
|
||||
.from('ml_orders')
|
||||
.select(`
|
||||
*,
|
||||
order_items!inner (
|
||||
id,
|
||||
product_id,
|
||||
product_name,
|
||||
sku_name,
|
||||
price,
|
||||
quantity,
|
||||
image_url
|
||||
)
|
||||
`)
|
||||
.eq('merchant_id', this.merchantId)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(5)
|
||||
.execute()
|
||||
|
||||
if (response.error != null || !response.data) {
|
||||
this.recentOrders = []
|
||||
return
|
||||
}
|
||||
|
||||
const rawData = response.data as any[]
|
||||
const ordersData: OrderType[] = []
|
||||
|
||||
for (let i = 0; i < rawData.length; i++) {
|
||||
const item = rawData[i] as UTSJSONObject
|
||||
const order: OrderType = {
|
||||
id: item.getString('id') || '',
|
||||
order_no: item.getString('order_no') || '',
|
||||
order_status: item.getNumber('order_status') || 1,
|
||||
total_amount: item.getNumber('total_amount') || 0,
|
||||
created_at: item.getString('created_at') || '',
|
||||
items: []
|
||||
}
|
||||
|
||||
const itemsObj = item.get('order_items')
|
||||
if (itemsObj != null && Array.isArray(itemsObj)) {
|
||||
const itemsArray = itemsObj as any[]
|
||||
for (let j = 0; j < itemsArray.length; j++) {
|
||||
const orderItem = itemsArray[j] as UTSJSONObject
|
||||
order.items.push({
|
||||
id: orderItem.getString('id') || '',
|
||||
order_id: '',
|
||||
product_id: orderItem.getString('product_id') || '',
|
||||
sku_id: '',
|
||||
product_name: orderItem.getString('product_name') || '',
|
||||
sku_name: orderItem.getString('sku_name') || '',
|
||||
price: orderItem.getNumber('price') || 0,
|
||||
quantity: orderItem.getNumber('quantity') || 0,
|
||||
image_url: orderItem.getString('image_url') || '',
|
||||
sku_snapshot: ''
|
||||
} as OrderItemType)
|
||||
}
|
||||
}
|
||||
|
||||
ordersData.push(order)
|
||||
}
|
||||
|
||||
this.recentOrders = ordersData
|
||||
} catch (e) {
|
||||
console.error('加载最新订单异常:', e)
|
||||
}
|
||||
},
|
||||
|
||||
// 获取订单状态样式
|
||||
getOrderStatusClass(status: number): string {
|
||||
switch (status) {
|
||||
case 1: return 'status-pending'
|
||||
@@ -276,7 +462,6 @@
|
||||
}
|
||||
},
|
||||
|
||||
// 获取订单状态文本
|
||||
getOrderStatusText(status: number): string {
|
||||
switch (status) {
|
||||
case 1: return '待付款'
|
||||
@@ -284,12 +469,13 @@
|
||||
case 3: return '已发货'
|
||||
case 4: return '已收货'
|
||||
case 5: return '已完成'
|
||||
case 0: return '退款中'
|
||||
default: return '未知状态'
|
||||
}
|
||||
},
|
||||
|
||||
// 格式化时间
|
||||
formatTime(timeStr: string): string {
|
||||
if (!timeStr) return ''
|
||||
const date = new Date(timeStr)
|
||||
const now = new Date()
|
||||
const diff = now.getTime() - date.getTime()
|
||||
@@ -304,7 +490,6 @@
|
||||
}
|
||||
},
|
||||
|
||||
// 导航方法
|
||||
goToMessages() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/merchant/messages'
|
||||
@@ -313,7 +498,7 @@
|
||||
|
||||
goToSettings() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/merchant/settings'
|
||||
url: '/pages/mall/merchant/shop-edit'
|
||||
})
|
||||
},
|
||||
|
||||
@@ -390,6 +575,7 @@
|
||||
height: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
margin-right: 20rpx;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.shop-details {
|
||||
@@ -554,6 +740,16 @@
|
||||
color: #007AFF;
|
||||
}
|
||||
|
||||
.no-orders {
|
||||
text-align: center;
|
||||
padding: 60rpx 0;
|
||||
}
|
||||
|
||||
.no-orders-text {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.orders-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -633,6 +829,7 @@
|
||||
height: 80rpx;
|
||||
border-radius: 8rpx;
|
||||
margin-right: 15rpx;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
|
||||
288
pages/mall/merchant/inventory.uvue
Normal file
288
pages/mall/merchant/inventory.uvue
Normal file
@@ -0,0 +1,288 @@
|
||||
<!-- 商家端 - 库存管理页面 -->
|
||||
<template>
|
||||
<view class="inventory-page">
|
||||
<view class="stats-bar">
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">{{ stats.totalProducts }}</text>
|
||||
<text class="stat-label">商品总数</text>
|
||||
</view>
|
||||
<view class="stat-item warning">
|
||||
<text class="stat-value">{{ stats.lowStock }}</text>
|
||||
<text class="stat-label">库存预警</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">{{ stats.outOfStock }}</text>
|
||||
<text class="stat-label">已售罄</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="filter-tabs">
|
||||
<view class="filter-tab" :class="{ active: currentFilter === 'all' }" @click="switchFilter('all')">全部</view>
|
||||
<view class="filter-tab" :class="{ active: currentFilter === 'low' }" @click="switchFilter('low')">库存预警</view>
|
||||
<view class="filter-tab" :class="{ active: currentFilter === 'out' }" @click="switchFilter('out')">已售罄</view>
|
||||
</view>
|
||||
|
||||
<scroll-view class="inventory-list" scroll-y :refresher-enabled="true" :refresher-triggered="refreshing" @refresherrefresh="onRefresh" @scrolltolower="loadMore">
|
||||
<view v-if="loading && products.length === 0" class="loading-container"><text class="loading-text">加载中...</text></view>
|
||||
<view v-else-if="products.length === 0" class="empty-container"><text class="empty-icon">📊</text><text class="empty-text">暂无商品</text></view>
|
||||
<view v-else>
|
||||
<view v-for="product in products" :key="product.id" class="product-card">
|
||||
<image :src="product.main_image_url || '/static/images/default-product.png'" class="product-image" mode="aspectFill"/>
|
||||
<view class="product-info">
|
||||
<text class="product-name">{{ product.name }}</text>
|
||||
<view class="stock-info">
|
||||
<text class="stock-label">库存:</text>
|
||||
<text class="stock-value" :class="getStockClass(product.total_stock)">{{ product.total_stock }}</text>
|
||||
</view>
|
||||
<view class="warning-info" v-if="product.total_stock <= (product.warning_stock || 10)">
|
||||
<text class="warning-text">库存不足</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="product-actions">
|
||||
<view class="action-btn" @click="editStock(product)">调整库存</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view v-if="showStockModal" class="modal-mask" @click="closeStockModal">
|
||||
<view class="modal-content" @click.stop>
|
||||
<view class="modal-header"><text class="modal-title">调整库存</text><text class="modal-close" @click="closeStockModal">×</text></view>
|
||||
<view class="modal-body">
|
||||
<view class="form-item">
|
||||
<text class="label">商品</text>
|
||||
<text class="value">{{ currentProduct?.name }}</text>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">当前库存</text>
|
||||
<text class="value">{{ currentProduct?.total_stock }}</text>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">新库存</text>
|
||||
<input class="input" type="number" v-model="newStock" placeholder="请输入新库存"/>
|
||||
</view>
|
||||
</view>
|
||||
<view class="modal-footer">
|
||||
<view class="modal-btn cancel" @click="closeStockModal">取消</view>
|
||||
<view class="modal-btn confirm" @click="saveStock">保存</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
type ProductType = {
|
||||
id: string
|
||||
name: string
|
||||
main_image_url: string
|
||||
total_stock: number
|
||||
warning_stock: number
|
||||
}
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
currentFilter: 'all',
|
||||
products: [] as ProductType[],
|
||||
loading: false,
|
||||
refreshing: false,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
hasMore: true,
|
||||
merchantId: '',
|
||||
stats: { totalProducts: 0, lowStock: 0, outOfStock: 0 },
|
||||
showStockModal: false,
|
||||
currentProduct: null as ProductType | null,
|
||||
newStock: ''
|
||||
}
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.initMerchantId()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.loadProducts()
|
||||
this.loadStats()
|
||||
},
|
||||
|
||||
methods: {
|
||||
async initMerchantId() {
|
||||
try {
|
||||
const session = supa.getSession()
|
||||
this.merchantId = session?.user?.getString('id') || uni.getStorageSync('user_id') || ''
|
||||
} catch (e) {}
|
||||
},
|
||||
|
||||
async loadProducts() {
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
let query = supa.from('ml_products').select('id, name, main_image_url, total_stock, warning_stock').eq('merchant_id', this.merchantId).order('total_stock', { ascending: true }).page(this.page).limit(this.limit)
|
||||
|
||||
const response = await query.execute()
|
||||
|
||||
if (response.error != null || !response.data) {
|
||||
this.products = []
|
||||
return
|
||||
}
|
||||
|
||||
const rawData = response.data as any[]
|
||||
let productsData: ProductType[] = []
|
||||
|
||||
for (let i = 0; i < rawData.length; i++) {
|
||||
const item = rawData[i] as UTSJSONObject
|
||||
const stock = item.getNumber('total_stock') || 0
|
||||
const warning = item.getNumber('warning_stock') || 10
|
||||
|
||||
if (this.currentFilter === 'low' && stock > warning) continue
|
||||
if (this.currentFilter === 'out' && stock > 0) continue
|
||||
|
||||
productsData.push({
|
||||
id: item.getString('id') || '',
|
||||
name: item.getString('name') || '',
|
||||
main_image_url: item.getString('main_image_url') || '',
|
||||
total_stock: stock,
|
||||
warning_stock: warning
|
||||
})
|
||||
}
|
||||
|
||||
this.products = productsData
|
||||
} catch (e) {
|
||||
console.error('加载失败:', e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
this.refreshing = false
|
||||
}
|
||||
},
|
||||
|
||||
async loadStats() {
|
||||
try {
|
||||
const response = await supa.from('ml_products').select('id, total_stock, warning_stock', { count: 'exact' }).eq('merchant_id', this.merchantId).execute()
|
||||
|
||||
if (response.error != null || !response.data) return
|
||||
|
||||
let total = 0, low = 0, out = 0
|
||||
const rawData = response.data as any[]
|
||||
for (let i = 0; i < rawData.length; i++) {
|
||||
const item = rawData[i] as UTSJSONObject
|
||||
total++
|
||||
const stock = item.getNumber('total_stock') || 0
|
||||
const warning = item.getNumber('warning_stock') || 10
|
||||
if (stock === 0) out++
|
||||
else if (stock <= warning) low++
|
||||
}
|
||||
|
||||
this.stats = { totalProducts: total, lowStock: low, outOfStock: out }
|
||||
} catch (e) {}
|
||||
},
|
||||
|
||||
switchFilter(filter: string) {
|
||||
this.currentFilter = filter
|
||||
this.loadProducts()
|
||||
},
|
||||
|
||||
onRefresh() {
|
||||
this.refreshing = true
|
||||
this.loadProducts()
|
||||
this.loadStats()
|
||||
},
|
||||
|
||||
loadMore() {
|
||||
if (this.hasMore && !this.loading) {
|
||||
this.page++
|
||||
this.loadProducts()
|
||||
}
|
||||
},
|
||||
|
||||
editStock(product: ProductType) {
|
||||
this.currentProduct = product
|
||||
this.newStock = String(product.total_stock)
|
||||
this.showStockModal = true
|
||||
},
|
||||
|
||||
closeStockModal() {
|
||||
this.showStockModal = false
|
||||
this.currentProduct = null
|
||||
this.newStock = ''
|
||||
},
|
||||
|
||||
async saveStock() {
|
||||
if (!this.newStock || isNaN(parseInt(this.newStock))) {
|
||||
uni.showToast({ title: '请输入有效库存', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await supa.from('ml_products').update({ total_stock: parseInt(this.newStock), updated_at: new Date().toISOString() }).eq('id', this.currentProduct!.id).execute()
|
||||
|
||||
if (response.error != null) {
|
||||
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
this.closeStockModal()
|
||||
this.loadProducts()
|
||||
this.loadStats()
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
getStockClass(stock: number): string {
|
||||
if (stock === 0) return 'out'
|
||||
if (stock <= 10) return 'low'
|
||||
return 'normal'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.inventory-page { background-color: #f5f5f5; min-height: 100vh; }
|
||||
.stats-bar { display: flex; background-color: #fff; padding: 30rpx 20rpx; margin-bottom: 20rpx; }
|
||||
.stat-item { flex: 1; text-align: center; }
|
||||
.stat-value { font-size: 40rpx; font-weight: bold; color: #333; display: block; }
|
||||
.stat-item.warning .stat-value { color: #FF9800; }
|
||||
.stat-label { font-size: 24rpx; color: #999; }
|
||||
.filter-tabs { display: flex; background-color: #fff; padding: 0 20rpx; margin-bottom: 20rpx; }
|
||||
.filter-tab { flex: 1; text-align: center; padding: 24rpx 0; font-size: 26rpx; color: #666; position: relative; }
|
||||
.filter-tab.active { color: #007AFF; font-weight: bold; }
|
||||
.filter-tab.active::after { content: ''; position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); width: 40rpx; height: 4rpx; background-color: #007AFF; border-radius: 2rpx; }
|
||||
.inventory-list { padding: 0 20rpx; height: calc(100vh - 280rpx); }
|
||||
.loading-container, .empty-container { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 100rpx 0; }
|
||||
.empty-icon { font-size: 100rpx; margin-bottom: 20rpx; }
|
||||
.empty-text, .loading-text { font-size: 28rpx; color: #999; }
|
||||
.product-card { display: flex; align-items: center; background-color: #fff; border-radius: 16rpx; padding: 20rpx; margin-bottom: 16rpx; }
|
||||
.product-image { width: 120rpx; height: 120rpx; border-radius: 8rpx; margin-right: 20rpx; background-color: #f5f5f5; }
|
||||
.product-info { flex: 1; }
|
||||
.product-name { font-size: 28rpx; color: #333; font-weight: 500; display: block; margin-bottom: 10rpx; }
|
||||
.stock-info { display: flex; align-items: center; }
|
||||
.stock-label { font-size: 24rpx; color: #999; margin-right: 10rpx; }
|
||||
.stock-value { font-size: 28rpx; font-weight: bold; }
|
||||
.stock-value.normal { color: #4CAF50; }
|
||||
.stock-value.low { color: #FF9800; }
|
||||
.stock-value.out { color: #F44336; }
|
||||
.warning-info { margin-top: 8rpx; }
|
||||
.warning-text { font-size: 22rpx; color: #F44336; }
|
||||
.product-actions { margin-left: 20rpx; }
|
||||
.action-btn { padding: 12rpx 24rpx; font-size: 24rpx; background-color: #E3F2FD; color: #1976D2; border-radius: 24rpx; }
|
||||
.modal-mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; }
|
||||
.modal-content { width: 80%; background-color: #fff; border-radius: 16rpx; }
|
||||
.modal-header { display: flex; justify-content: space-between; align-items: center; padding: 30rpx; border-bottom: 1rpx solid #f5f5f5; }
|
||||
.modal-title { font-size: 32rpx; font-weight: bold; color: #333; }
|
||||
.modal-close { font-size: 44rpx; color: #999; }
|
||||
.modal-body { padding: 30rpx; }
|
||||
.form-item { margin-bottom: 20rpx; }
|
||||
.form-item .label { font-size: 26rpx; color: #999; display: block; margin-bottom: 10rpx; }
|
||||
.form-item .value { font-size: 28rpx; color: #333; }
|
||||
.input { height: 72rpx; border: 1rpx solid #e5e5e5; border-radius: 8rpx; padding: 0 20rpx; font-size: 28rpx; }
|
||||
.modal-footer { display: flex; border-top: 1rpx solid #f5f5f5; }
|
||||
.modal-btn { flex: 1; height: 88rpx; line-height: 88rpx; text-align: center; font-size: 28rpx; }
|
||||
.modal-btn.cancel { color: #666; border-right: 1rpx solid #f5f5f5; }
|
||||
.modal-btn.confirm { color: #007AFF; font-weight: bold; }
|
||||
</style>
|
||||
247
pages/mall/merchant/messages.uvue
Normal file
247
pages/mall/merchant/messages.uvue
Normal file
@@ -0,0 +1,247 @@
|
||||
<!-- 商家端 - 消息中心页面 -->
|
||||
<template>
|
||||
<view class="messages-page">
|
||||
<view class="tabs">
|
||||
<view class="tab" :class="{ active: currentTab === 'chat' }" @click="switchTab('chat')">会话列表</view>
|
||||
<view class="tab" :class="{ active: currentTab === 'all' }" @click="switchTab('all')">全部消息</view>
|
||||
</view>
|
||||
|
||||
<scroll-view class="messages-list" scroll-y :refresher-enabled="true" :refresher-triggered="refreshing" @refresherrefresh="onRefresh">
|
||||
<view v-if="loading && conversations.length === 0" class="loading-container"><text class="loading-text">加载中...</text></view>
|
||||
<view v-else-if="currentTab === 'chat' && conversations.length === 0" class="empty-container"><text class="empty-icon">💬</text><text class="empty-text">暂无会话</text></view>
|
||||
<view v-else-if="currentTab === 'all' && messages.length === 0" class="empty-container"><text class="empty-icon">📭</text><text class="empty-text">暂无消息</text></view>
|
||||
|
||||
<!-- 会话列表 -->
|
||||
<view v-else-if="currentTab === 'chat'">
|
||||
<view v-for="conv in conversations" :key="conv.sessionId" class="conversation-card" @click="goToChat(conv)">
|
||||
<image class="conv-avatar" :src="conv.avatar || '/static/images/default-avatar.png'" mode="aspectFill" />
|
||||
<view class="conv-info">
|
||||
<view class="conv-header">
|
||||
<text class="conv-name">{{ conv.name }}</text>
|
||||
<text class="conv-time">{{ conv.lastTime }}</text>
|
||||
</view>
|
||||
<text class="conv-preview">{{ conv.lastMessage }}</text>
|
||||
</view>
|
||||
<view v-if="conv.unread > 0" class="unread-badge"><text>{{ conv.unread > 99 ? '99+' : conv.unread }}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 全部消息 -->
|
||||
<view v-else>
|
||||
<view v-for="msg in messages" :key="msg.id" class="message-card" :class="{ unread: !msg.is_read }" @click="viewMessage(msg)">
|
||||
<view class="message-icon">{{ msg.is_from_user ? '👤' : '🏪' }}</view>
|
||||
<view class="message-content">
|
||||
<view class="message-header">
|
||||
<text class="message-title">{{ msg.is_from_user ? '发给客户' : '收到消息' }}</text>
|
||||
<text class="message-time">{{ formatTime(msg.created_at) }}</text>
|
||||
</view>
|
||||
<text class="message-text">{{ msg.content }}</text>
|
||||
</view>
|
||||
<view v-if="!msg.is_read" class="unread-dot"></view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
type MessageType = {
|
||||
id: string
|
||||
session_id: string
|
||||
sender_id: string
|
||||
receiver_id: string
|
||||
content: string
|
||||
msg_type: string
|
||||
is_read: boolean
|
||||
is_from_user: boolean
|
||||
created_at: string
|
||||
}
|
||||
|
||||
type ConversationType = {
|
||||
sessionId: string
|
||||
name: string
|
||||
avatar: string
|
||||
lastMessage: string
|
||||
lastTime: string
|
||||
unread: number
|
||||
userId: string
|
||||
}
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
currentTab: 'chat',
|
||||
messages: [] as MessageType[],
|
||||
conversations: [] as ConversationType[],
|
||||
loading: false,
|
||||
refreshing: false,
|
||||
merchantId: ''
|
||||
}
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.initMerchantId()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.loadMessages()
|
||||
},
|
||||
|
||||
methods: {
|
||||
async initMerchantId() {
|
||||
try {
|
||||
const session = supa.getSession()
|
||||
this.merchantId = session?.user?.getString('id') || uni.getStorageSync('user_id') || ''
|
||||
} catch (e) {}
|
||||
},
|
||||
|
||||
async loadMessages() {
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
const query = supa
|
||||
.from('ml_chat_messages')
|
||||
.select('*')
|
||||
.or(`receiver_id.eq.${this.merchantId},sender_id.eq.${this.merchantId}`)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(100)
|
||||
|
||||
const response = await query.execute()
|
||||
|
||||
if (response.error != null || !response.data) {
|
||||
this.messages = []
|
||||
this.conversations = []
|
||||
return
|
||||
}
|
||||
|
||||
const rawData = response.data as any[]
|
||||
const messagesData: MessageType[] = []
|
||||
const sessionMap = new Map<string, ConversationType>()
|
||||
|
||||
for (let i = 0; i < rawData.length; i++) {
|
||||
const item = rawData[i] as UTSJSONObject
|
||||
const msg: MessageType = {
|
||||
id: item.getString('id') || '',
|
||||
session_id: item.getString('session_id') || '',
|
||||
sender_id: item.getString('sender_id') || '',
|
||||
receiver_id: item.getString('receiver_id') || '',
|
||||
content: item.getString('content') || '',
|
||||
msg_type: item.getString('msg_type') || 'text',
|
||||
is_read: item.getBoolean('is_read') || false,
|
||||
is_from_user: item.getBoolean('is_from_user') || false,
|
||||
created_at: item.getString('created_at') || ''
|
||||
}
|
||||
messagesData.push(msg)
|
||||
|
||||
const otherUserId = msg.is_from_user ? msg.receiver_id : msg.sender_id
|
||||
const sessionId = msg.session_id || otherUserId
|
||||
|
||||
if (!sessionMap.has(sessionId)) {
|
||||
sessionMap.set(sessionId, {
|
||||
sessionId: sessionId,
|
||||
name: '客户',
|
||||
avatar: '',
|
||||
lastMessage: msg.content,
|
||||
lastTime: this.formatTime(msg.created_at),
|
||||
unread: 0,
|
||||
userId: otherUserId
|
||||
})
|
||||
}
|
||||
|
||||
const conv = sessionMap.get(sessionId)!
|
||||
conv.lastMessage = msg.content
|
||||
conv.lastTime = this.formatTime(msg.created_at)
|
||||
if (!msg.is_read && !msg.is_from_user) {
|
||||
conv.unread++
|
||||
}
|
||||
}
|
||||
|
||||
this.messages = messagesData
|
||||
this.conversations = Array.from(sessionMap.values()).sort((a, b) => b.unread - a.unread)
|
||||
|
||||
} catch (e) {
|
||||
console.error('加载消息失败:', e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
this.refreshing = false
|
||||
}
|
||||
},
|
||||
|
||||
goToChat(conv: ConversationType) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/merchant/chat?user_id=${conv.userId}&session_id=${conv.sessionId}&title=${encodeURIComponent(conv.name)}`
|
||||
})
|
||||
},
|
||||
|
||||
switchTab(tab: string) {
|
||||
this.currentTab = tab
|
||||
},
|
||||
|
||||
onRefresh() {
|
||||
this.refreshing = true
|
||||
this.loadMessages()
|
||||
},
|
||||
|
||||
viewMessage(msg: MessageType) {
|
||||
if (!msg.is_read) {
|
||||
supa.from('ml_chat_messages').update({ is_read: true }).eq('id', msg.id).execute()
|
||||
msg.is_read = true
|
||||
}
|
||||
|
||||
const otherUserId = msg.is_from_user ? msg.receiver_id : msg.sender_id
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/merchant/chat?user_id=${otherUserId}&session_id=${msg.session_id}`
|
||||
})
|
||||
},
|
||||
|
||||
formatTime(timeStr: string): string {
|
||||
if (!timeStr) return ''
|
||||
const date = new Date(timeStr)
|
||||
const now = new Date()
|
||||
const diff = now.getTime() - date.getTime()
|
||||
const hours = Math.floor(diff / (1000 * 60 * 60))
|
||||
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
|
||||
|
||||
if (hours < 1) return '刚刚'
|
||||
if (hours < 24) return `${hours}小时前`
|
||||
if (days === 1) return '昨天'
|
||||
if (days < 7) return `${days}天前`
|
||||
return `${date.getMonth() + 1}-${date.getDate()}`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.messages-page { background-color: #f5f5f5; min-height: 100vh; }
|
||||
.tabs { display: flex; background-color: #fff; padding: 0 20rpx; position: sticky; top: 0; z-index: 10; }
|
||||
.tab { flex: 1; text-align: center; padding: 24rpx 0; font-size: 28rpx; color: #666; position: relative; }
|
||||
.tab.active { color: #007AFF; font-weight: bold; }
|
||||
.tab.active::after { content: ''; position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); width: 40rpx; height: 4rpx; background-color: #007AFF; border-radius: 2rpx; }
|
||||
.messages-list { padding: 20rpx; height: calc(100vh - 100rpx); }
|
||||
.loading-container, .empty-container { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 100rpx 0; }
|
||||
.empty-icon { font-size: 100rpx; margin-bottom: 20rpx; }
|
||||
.empty-text, .loading-text { font-size: 28rpx; color: #999; }
|
||||
|
||||
.conversation-card { display: flex; align-items: center; background-color: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 16rpx; position: relative; }
|
||||
.conv-avatar { width: 100rpx; height: 100rpx; border-radius: 12rpx; margin-right: 20rpx; }
|
||||
.conv-info { flex: 1; }
|
||||
.conv-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10rpx; }
|
||||
.conv-name { font-size: 30rpx; color: #333; font-weight: 500; }
|
||||
.conv-time { font-size: 22rpx; color: #999; }
|
||||
.conv-preview { font-size: 26rpx; color: #666; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: block; }
|
||||
.unread-badge { min-width: 36rpx; height: 36rpx; background-color: #FF3B30; border-radius: 18rpx; display: flex; align-items: center; justify-content: center; padding: 0 10rpx; }
|
||||
.unread-badge text { font-size: 20rpx; color: #fff; }
|
||||
|
||||
.message-card { display: flex; align-items: flex-start; background-color: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 16rpx; position: relative; }
|
||||
.message-card.unread { background-color: #f0f9ff; }
|
||||
.message-icon { font-size: 40rpx; margin-right: 20rpx; }
|
||||
.message-content { flex: 1; }
|
||||
.message-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10rpx; }
|
||||
.message-title { font-size: 28rpx; color: #333; font-weight: 500; }
|
||||
.message-time { font-size: 22rpx; color: #999; }
|
||||
.message-text { font-size: 26rpx; color: #666; line-height: 1.4; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
|
||||
.unread-dot { position: absolute; top: 30rpx; right: 30rpx; width: 16rpx; height: 16rpx; background-color: #FF3B30; border-radius: 50%; }
|
||||
</style>
|
||||
925
pages/mall/merchant/order-detail.uvue
Normal file
925
pages/mall/merchant/order-detail.uvue
Normal file
@@ -0,0 +1,925 @@
|
||||
<!-- 商家端 - 订单详情页面 -->
|
||||
<template>
|
||||
<view class="order-detail-page">
|
||||
<!-- 订单状态头部 -->
|
||||
<view class="status-header" :class="getStatusBgClass(order.order_status)">
|
||||
<text class="status-icon">{{ getStatusIcon(order.order_status) }}</text>
|
||||
<text class="status-text">{{ getStatusText(order.order_status) }}</text>
|
||||
<text class="status-desc">{{ getStatusDesc(order.order_status) }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 物流信息 -->
|
||||
<view v-if="order.order_status >= 3" class="section logistics-section">
|
||||
<view class="section-title">物流信息</view>
|
||||
<view class="logistics-info">
|
||||
<view class="logistics-company">
|
||||
<text class="label">物流公司:</text>
|
||||
<text class="value">{{ order.shipping_company || '待填写' }}</text>
|
||||
</view>
|
||||
<view class="logistics-number">
|
||||
<text class="label">物流单号:</text>
|
||||
<text class="value">{{ order.tracking_number || '待填写' }}</text>
|
||||
<text v-if="order.tracking_number" class="copy-btn" @click="copyTrackingNumber">复制</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 收货地址 -->
|
||||
<view class="section address-section">
|
||||
<view class="section-title">收货信息</view>
|
||||
<view class="address-info">
|
||||
<view class="address-user">
|
||||
<text class="name">{{ addressData.recipient_name || '未知' }}</text>
|
||||
<text class="phone">{{ addressData.phone || '未知' }}</text>
|
||||
</view>
|
||||
<view class="address-detail">
|
||||
{{ addressData.province || '' }}{{ addressData.city || '' }}{{ addressData.district || '' }}{{ addressData.detail_address || '' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 订单信息 -->
|
||||
<view class="section order-info-section">
|
||||
<view class="section-title">订单信息</view>
|
||||
<view class="info-list">
|
||||
<view class="info-item">
|
||||
<text class="label">订单编号:</text>
|
||||
<text class="value">{{ order.order_no }}</text>
|
||||
<text class="copy-btn" @click="copyOrderNo">复制</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">下单时间:</text>
|
||||
<text class="value">{{ formatTime(order.created_at) }}</text>
|
||||
</view>
|
||||
<view v-if="order.paid_at" class="info-item">
|
||||
<text class="label">付款时间:</text>
|
||||
<text class="value">{{ formatTime(order.paid_at) }}</text>
|
||||
</view>
|
||||
<view v-if="order.shipped_at" class="info-item">
|
||||
<text class="label">发货时间:</text>
|
||||
<text class="value">{{ formatTime(order.shipped_at) }}</text>
|
||||
</view>
|
||||
<view v-if="order.remark" class="info-item">
|
||||
<text class="label">订单备注:</text>
|
||||
<text class="value">{{ order.remark }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 商品列表 -->
|
||||
<view class="section products-section">
|
||||
<view class="section-title">商品信息</view>
|
||||
<view class="products-list">
|
||||
<view v-for="item in order.items" :key="item.id" class="product-item">
|
||||
<image
|
||||
:src="item.image_url || '/static/images/default-product.png'"
|
||||
class="product-image"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="product-info">
|
||||
<text class="product-name">{{ item.product_name }}</text>
|
||||
<text class="product-spec">{{ item.sku_name || '标准规格' }}</text>
|
||||
</view>
|
||||
<view class="product-right">
|
||||
<text class="product-price">¥{{ item.price }}</text>
|
||||
<text class="product-quantity">x{{ item.quantity }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 费用明细 -->
|
||||
<view class="section fees-section">
|
||||
<view class="section-title">费用明细</view>
|
||||
<view class="fees-list">
|
||||
<view class="fee-item">
|
||||
<text class="label">商品金额:</text>
|
||||
<text class="value">¥{{ order.product_amount }}</text>
|
||||
</view>
|
||||
<view class="fee-item">
|
||||
<text class="label">运费:</text>
|
||||
<text class="value">¥{{ order.shipping_fee }}</text>
|
||||
</view>
|
||||
<view v-if="order.discount_amount > 0" class="fee-item">
|
||||
<text class="label">优惠:</text>
|
||||
<text class="value discount">-¥{{ order.discount_amount }}</text>
|
||||
</view>
|
||||
<view class="fee-item total">
|
||||
<text class="label">实付金额:</text>
|
||||
<text class="value">¥{{ order.total_amount }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="action-buttons">
|
||||
<view
|
||||
v-if="order.order_status === 1"
|
||||
class="action-btn primary"
|
||||
@click="shipOrder"
|
||||
>
|
||||
去发货
|
||||
</view>
|
||||
<view
|
||||
v-if="order.order_status === 2"
|
||||
class="action-btn primary"
|
||||
@click="viewLogistics"
|
||||
>
|
||||
查看物流
|
||||
</view>
|
||||
<view
|
||||
v-if="order.order_status === 3"
|
||||
class="action-btn primary"
|
||||
@click="confirmDelivery"
|
||||
>
|
||||
确认收货
|
||||
</view>
|
||||
<view
|
||||
v-if="order.order_status === -1 || order.order_status === 5"
|
||||
class="action-btn danger"
|
||||
@click="deleteOrder"
|
||||
>
|
||||
删除订单
|
||||
</view>
|
||||
<view class="action-btn default" @click="contactBuyer">
|
||||
联系买家
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 发货弹窗 -->
|
||||
<view v-if="showShipModal" class="modal-mask" @click="closeShipModal">
|
||||
<view class="modal-content" @click.stop>
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">发货</text>
|
||||
<text class="modal-close" @click="closeShipModal">×</text>
|
||||
</view>
|
||||
<view class="modal-body">
|
||||
<view class="form-item">
|
||||
<text class="form-label">物流公司</text>
|
||||
<picker
|
||||
class="form-picker"
|
||||
:range="logisticsCompanies"
|
||||
range-key="name"
|
||||
@change="onLogisticsChange"
|
||||
>
|
||||
<view class="picker-value">
|
||||
{{ selectedLogistics.name || '请选择物流公司' }}
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">物流单号</text>
|
||||
<input
|
||||
class="form-input"
|
||||
v-model="trackingNumber"
|
||||
placeholder="请输入物流单号"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<view class="modal-footer">
|
||||
<view class="modal-btn cancel" @click="closeShipModal">取消</view>
|
||||
<view class="modal-btn confirm" @click="confirmShip">确认发货</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
type OrderItemType = {
|
||||
id: string
|
||||
order_id: string
|
||||
product_id: string
|
||||
sku_id: string
|
||||
product_name: string
|
||||
sku_name: string
|
||||
price: number
|
||||
quantity: number
|
||||
image_url: string
|
||||
sku_snapshot: string
|
||||
}
|
||||
|
||||
type AddressType = {
|
||||
recipient_name: string
|
||||
phone: string
|
||||
province: string
|
||||
city: string
|
||||
district: string
|
||||
detail_address: string
|
||||
}
|
||||
|
||||
type LogisticsType = {
|
||||
name: string
|
||||
code: string
|
||||
}
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
orderId: '',
|
||||
order: {
|
||||
id: '',
|
||||
order_no: '',
|
||||
user_id: '',
|
||||
merchant_id: '',
|
||||
order_status: 1,
|
||||
total_amount: 0,
|
||||
product_amount: 0,
|
||||
shipping_fee: 0,
|
||||
discount_amount: 0,
|
||||
paid_amount: 0,
|
||||
shipping_address: '',
|
||||
remark: '',
|
||||
shipping_company: '',
|
||||
tracking_number: '',
|
||||
paid_at: '',
|
||||
shipped_at: '',
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
items: [] as OrderItemType[]
|
||||
},
|
||||
addressData: {} as AddressType,
|
||||
|
||||
showShipModal: false,
|
||||
logisticsCompanies: [
|
||||
{ name: '顺丰速运', code: 'SF' },
|
||||
{ name: '圆通速递', code: 'YTO' },
|
||||
{ name: '中通快递', code: 'ZTO' },
|
||||
{ name: '韵达快递', code: 'YD' },
|
||||
{ name: '申通快递', code: 'STO' },
|
||||
{ name: 'EMS', code: 'EMS' },
|
||||
{ name: '京东物流', code: 'JD' }
|
||||
] as LogisticsType[],
|
||||
selectedLogistics: {} as LogisticsType,
|
||||
trackingNumber: ''
|
||||
}
|
||||
},
|
||||
|
||||
onLoad(options: any) {
|
||||
const id = options.id as string
|
||||
if (id) {
|
||||
this.orderId = id
|
||||
this.loadOrderDetail()
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async loadOrderDetail() {
|
||||
try {
|
||||
const response = await supa
|
||||
.from('ml_orders')
|
||||
.select(`
|
||||
*,
|
||||
order_items!inner (
|
||||
id,
|
||||
order_id,
|
||||
product_id,
|
||||
sku_id,
|
||||
product_name,
|
||||
sku_name,
|
||||
price,
|
||||
quantity,
|
||||
image_url,
|
||||
sku_snapshot
|
||||
)
|
||||
`)
|
||||
.eq('id', this.orderId)
|
||||
.single()
|
||||
.execute()
|
||||
|
||||
if (response.error != null) {
|
||||
console.error('获取订单详情失败:', response.error)
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
const rawData = response.data as UTSJSONObject
|
||||
if (rawData == null) return
|
||||
|
||||
this.order = {
|
||||
id: rawData.getString('id') || '',
|
||||
order_no: rawData.getString('order_no') || '',
|
||||
user_id: rawData.getString('user_id') || '',
|
||||
merchant_id: rawData.getString('merchant_id') || '',
|
||||
order_status: rawData.getNumber('order_status') || 1,
|
||||
total_amount: rawData.getNumber('total_amount') || 0,
|
||||
product_amount: rawData.getNumber('product_amount') || 0,
|
||||
shipping_fee: rawData.getNumber('shipping_fee') || 0,
|
||||
discount_amount: rawData.getNumber('discount_amount') || 0,
|
||||
paid_amount: rawData.getNumber('paid_amount') || 0,
|
||||
shipping_address: rawData.getString('shipping_address') || '{}',
|
||||
remark: rawData.getString('remark') || '',
|
||||
shipping_company: rawData.getString('shipping_company') || '',
|
||||
tracking_number: rawData.getString('tracking_number') || '',
|
||||
paid_at: rawData.getString('paid_at') || '',
|
||||
shipped_at: rawData.getString('shipped_at') || '',
|
||||
created_at: rawData.getString('created_at') || '',
|
||||
updated_at: rawData.getString('updated_at') || '',
|
||||
items: []
|
||||
}
|
||||
|
||||
const itemsObj = rawData.get('order_items')
|
||||
if (itemsObj != null && Array.isArray(itemsObj)) {
|
||||
const itemsArray = itemsObj as any[]
|
||||
for (let i = 0; i < itemsArray.length; i++) {
|
||||
const orderItem = itemsArray[i] as UTSJSONObject
|
||||
this.order.items.push({
|
||||
id: orderItem.getString('id') || '',
|
||||
order_id: orderItem.getString('order_id') || '',
|
||||
product_id: orderItem.getString('product_id') || '',
|
||||
sku_id: orderItem.getString('sku_id') || '',
|
||||
product_name: orderItem.getString('product_name') || '',
|
||||
sku_name: orderItem.getString('sku_name') || '',
|
||||
price: orderItem.getNumber('price') || 0,
|
||||
quantity: orderItem.getNumber('quantity') || 0,
|
||||
image_url: orderItem.getString('image_url') || '',
|
||||
sku_snapshot: ''
|
||||
} as OrderItemType)
|
||||
}
|
||||
}
|
||||
|
||||
this.parseAddress()
|
||||
} catch (e) {
|
||||
console.error('获取订单详情异常:', e)
|
||||
}
|
||||
},
|
||||
|
||||
parseAddress() {
|
||||
try {
|
||||
const addrStr = this.order.shipping_address
|
||||
if (addrStr && addrStr !== '{}') {
|
||||
const addrObj = JSON.parse(addrStr) as AddressType
|
||||
this.addressData = addrObj
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('解析地址失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
getStatusIcon(status: number): string {
|
||||
if (status === 1) return '💰'
|
||||
if (status === 2) return '📦'
|
||||
if (status === 3) return '🚚'
|
||||
if (status === 4) return '✅'
|
||||
if (status === 0) return '↩️'
|
||||
if (status === 5 || status === -1) return '❌'
|
||||
return '📋'
|
||||
},
|
||||
|
||||
getStatusText(status: number): string {
|
||||
if (status === 1) return '待付款'
|
||||
if (status === 2) return '待发货'
|
||||
if (status === 3) return '待收货'
|
||||
if (status === 4) return '已完成'
|
||||
if (status === 0) return '退款中'
|
||||
if (status === 5 || status === -1) return '已取消'
|
||||
return '未知'
|
||||
},
|
||||
|
||||
getStatusDesc(status: number): string {
|
||||
if (status === 1) return '买家已下单,请尽快发货'
|
||||
if (status === 2) return '等待商家发货'
|
||||
if (status === 3) return '商品运输中,请关注物流'
|
||||
if (status === 4) return '订单已完成'
|
||||
if (status === 0) return '买家申请退款,请处理'
|
||||
if (status === 5 || status === -1) return '订单已取消'
|
||||
return ''
|
||||
},
|
||||
|
||||
getStatusBgClass(status: number): string {
|
||||
if (status === 1) return 'status-bg-1'
|
||||
if (status === 2) return 'status-bg-2'
|
||||
if (status === 3) return 'status-bg-3'
|
||||
if (status === 4) return 'status-bg-4'
|
||||
if (status === 0 || status === 5 || status === -1) return 'status-bg-0'
|
||||
return 'status-bg-1'
|
||||
},
|
||||
|
||||
formatTime(timeStr: string): string {
|
||||
if (!timeStr) return '-'
|
||||
const date = new Date(timeStr)
|
||||
const year = date.getFullYear()
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
||||
const day = date.getDate().toString().padStart(2, '0')
|
||||
const hour = date.getHours().toString().padStart(2, '0')
|
||||
const minute = date.getMinutes().toString().padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hour}:${minute}`
|
||||
},
|
||||
|
||||
copyOrderNo() {
|
||||
uni.setClipboardData({
|
||||
data: this.order.order_no,
|
||||
success: () => {
|
||||
uni.showToast({ title: '复制成功', icon: 'success' })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
copyTrackingNumber() {
|
||||
uni.setClipboardData({
|
||||
data: this.order.tracking_number,
|
||||
success: () => {
|
||||
uni.showToast({ title: '复制成功', icon: 'success' })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
shipOrder() {
|
||||
this.showShipModal = true
|
||||
},
|
||||
|
||||
closeShipModal() {
|
||||
this.showShipModal = false
|
||||
this.selectedLogistics = {} as LogisticsType
|
||||
this.trackingNumber = ''
|
||||
},
|
||||
|
||||
onLogisticsChange(e: any) {
|
||||
const index = e.detail.value as number
|
||||
this.selectedLogistics = this.logisticsCompanies[index]
|
||||
},
|
||||
|
||||
async confirmShip() {
|
||||
if (!this.selectedLogistics.name) {
|
||||
uni.showToast({ title: '请选择物流公司', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!this.trackingNumber) {
|
||||
uni.showToast({ title: '请输入物流单号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await supa
|
||||
.from('ml_orders')
|
||||
.update({
|
||||
order_status: 3,
|
||||
shipping_company: this.selectedLogistics.name,
|
||||
tracking_number: this.trackingNumber,
|
||||
shipped_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
})
|
||||
.eq('id', this.order.id)
|
||||
.execute()
|
||||
|
||||
if (response.error != null) {
|
||||
uni.showToast({ title: '发货失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
uni.showToast({ title: '发货成功', icon: 'success' })
|
||||
this.closeShipModal()
|
||||
this.loadOrderDetail()
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '发货失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
viewLogistics() {
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/merchant/logistics?orderId=${this.order.id}`
|
||||
})
|
||||
},
|
||||
|
||||
async confirmDelivery() {
|
||||
uni.showModal({
|
||||
title: '确认收货',
|
||||
content: '确认买家已收到货物吗?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
const response = await supa
|
||||
.from('ml_orders')
|
||||
.update({
|
||||
order_status: 4,
|
||||
delivered_at: new Date().toISOString(),
|
||||
completed_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
})
|
||||
.eq('id', this.order.id)
|
||||
.execute()
|
||||
|
||||
if (response.error != null) {
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
uni.showToast({ title: '操作成功', icon: 'success' })
|
||||
this.loadOrderDetail()
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async deleteOrder() {
|
||||
uni.showModal({
|
||||
title: '确认删除',
|
||||
content: '确定要删除该订单吗?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
const response = await supa
|
||||
.from('ml_orders')
|
||||
.delete()
|
||||
.eq('id', this.order.id)
|
||||
.execute()
|
||||
|
||||
if (response.error != null) {
|
||||
uni.showToast({ title: '删除失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
uni.showToast({ title: '删除成功', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '删除失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
contactBuyer() {
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/merchant/chat?userId=${this.order.user_id}`
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.order-detail-page {
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
padding-bottom: 180rpx;
|
||||
}
|
||||
|
||||
.status-header {
|
||||
padding: 50rpx 30rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.status-bg-1 {
|
||||
background: linear-gradient(135deg, #FF9800 0%, #FF5722 100%);
|
||||
}
|
||||
|
||||
.status-bg-2 {
|
||||
background: linear-gradient(135deg, #2196F3 0%, #1976D2 100%);
|
||||
}
|
||||
|
||||
.status-bg-3 {
|
||||
background: linear-gradient(135deg, #4CAF50 0%, #388E3C 100%);
|
||||
}
|
||||
|
||||
.status-bg-4 {
|
||||
background: linear-gradient(135deg, #9C27B0 0%, #7B1FA2 100%);
|
||||
}
|
||||
|
||||
.status-bg-0, .status-bg-5 {
|
||||
background: linear-gradient(135deg, #607D8B 0%, #455A64 100%);
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
font-size: 60rpx;
|
||||
display: block;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
display: block;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.status-desc {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.section {
|
||||
background-color: #fff;
|
||||
margin-bottom: 20rpx;
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.logistics-info {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.logistics-company, .logistics-number {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.logistics-number .label {
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.logistics-number .value {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #999;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.address-info {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.address-user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.address-user .name {
|
||||
font-weight: bold;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.address-user .phone {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.address-detail {
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.info-list {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.info-item .label {
|
||||
min-width: 140rpx;
|
||||
}
|
||||
|
||||
.info-item .value {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
color: #007AFF;
|
||||
font-size: 24rpx;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
.products-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.product-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.product-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
width: 140rpx;
|
||||
height: 140rpx;
|
||||
border-radius: 8rpx;
|
||||
margin-right: 20rpx;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.product-spec {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.product-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.product-price {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.product-quantity {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.fees-list {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.fee-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.fee-item .value {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.fee-item .value.discount {
|
||||
color: #FF5722;
|
||||
}
|
||||
|
||||
.fee-item.total {
|
||||
margin-top: 20rpx;
|
||||
padding-top: 20rpx;
|
||||
border-top: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.fee-item.total .label {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.fee-item.total .value {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #FF3B30;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
padding: 20rpx 30rpx;
|
||||
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||
background-color: #fff;
|
||||
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
text-align: center;
|
||||
font-size: 28rpx;
|
||||
border-radius: 40rpx;
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background-color: #007AFF;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.action-btn.default {
|
||||
background-color: #f5f5f5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.action-btn.danger {
|
||||
background-color: #FF3B30;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.modal-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
border-radius: 24rpx 24rpx 0 0;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
font-size: 44rpx;
|
||||
color: #999;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.form-picker, .form-input {
|
||||
height: 72rpx;
|
||||
border: 1rpx solid #e5e5e5;
|
||||
border-radius: 8rpx;
|
||||
padding: 0 20rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.picker-value {
|
||||
height: 72rpx;
|
||||
line-height: 72rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
border-top: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.modal-btn {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
text-align: center;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.modal-btn.cancel {
|
||||
color: #666;
|
||||
border-right: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.modal-btn.confirm {
|
||||
color: #007AFF;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
996
pages/mall/merchant/orders.uvue
Normal file
996
pages/mall/merchant/orders.uvue
Normal file
@@ -0,0 +1,996 @@
|
||||
<!-- 商家端 - 订单管理页面 -->
|
||||
<template>
|
||||
<view class="orders-page">
|
||||
<!-- 标签页切换 -->
|
||||
<view class="tabs-container">
|
||||
<view class="tabs-scroll">
|
||||
<view
|
||||
v-for="(tab, index) in tabs"
|
||||
:key="index"
|
||||
class="tab-item"
|
||||
:class="{ active: currentTab === tab.status }"
|
||||
@click="switchTab(tab.status)"
|
||||
>
|
||||
<text class="tab-text">{{ tab.name }}</text>
|
||||
<view v-if="tab.count > 0" class="tab-badge">{{ tab.count }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-bar">
|
||||
<input
|
||||
class="search-input"
|
||||
type="text"
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索订单号/商品名称"
|
||||
@confirm="handleSearch"
|
||||
/>
|
||||
<view class="search-btn" @click="handleSearch">搜索</view>
|
||||
</view>
|
||||
|
||||
<!-- 订单列表 -->
|
||||
<scroll-view
|
||||
class="orders-list"
|
||||
scroll-y
|
||||
:refresher-enabled="true"
|
||||
:refresher-triggered="refreshing"
|
||||
@refresherrefresh="onRefresh"
|
||||
@scrolltolower="loadMore"
|
||||
>
|
||||
<view v-if="loading && orders.length === 0" class="loading-container">
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<view v-else-if="orders.length === 0" class="empty-container">
|
||||
<text class="empty-icon">📋</text>
|
||||
<text class="empty-text">暂无订单</text>
|
||||
</view>
|
||||
|
||||
<view v-else>
|
||||
<view
|
||||
v-for="order in orders"
|
||||
:key="order.id"
|
||||
class="order-card"
|
||||
@click="viewOrderDetail(order.id)"
|
||||
>
|
||||
<!-- 订单头部 -->
|
||||
<view class="order-header">
|
||||
<view class="order-info-left">
|
||||
<text class="order-no">订单号: {{ order.order_no }}</text>
|
||||
<text class="order-time">{{ formatTime(order.created_at) }}</text>
|
||||
</view>
|
||||
<text class="order-status" :class="'status-' + order.order_status">
|
||||
{{ getStatusText(order.order_status) }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 订单商品 -->
|
||||
<view class="order-products">
|
||||
<view
|
||||
v-for="item in order.items"
|
||||
:key="item.id"
|
||||
class="product-item"
|
||||
>
|
||||
<image
|
||||
:src="item.image_url || '/static/images/default-product.png'"
|
||||
class="product-image"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="product-info">
|
||||
<text class="product-name">{{ item.product_name }}</text>
|
||||
<text class="product-spec">{{ item.sku_name || '标准规格' }}</text>
|
||||
</view>
|
||||
<view class="product-right">
|
||||
<text class="product-price">¥{{ item.price }}</text>
|
||||
<text class="product-quantity">x{{ item.quantity }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 订单底部 -->
|
||||
<view class="order-footer">
|
||||
<view class="order-amount">
|
||||
<text class="amount-label">共{{ getTotalQuantity(order.items) }}件商品</text>
|
||||
<text class="amount-value">合计: ¥{{ order.total_amount }}</text>
|
||||
</view>
|
||||
<view class="order-actions">
|
||||
<view
|
||||
v-if="order.order_status === 1"
|
||||
class="action-btn primary"
|
||||
@click.stop="shipOrder(order)"
|
||||
>
|
||||
发货
|
||||
</view>
|
||||
<view
|
||||
v-if="order.order_status === 2"
|
||||
class="action-btn info"
|
||||
@click.stop="viewLogistics(order)"
|
||||
>
|
||||
查看物流
|
||||
</view>
|
||||
<view
|
||||
v-if="order.order_status === -1 || order.order_status === 5"
|
||||
class="action-btn default"
|
||||
@click.stop="deleteOrder(order)"
|
||||
>
|
||||
删除
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="loadingMore" class="load-more">
|
||||
<text class="load-more-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<view v-if="!hasMore && orders.length > 0" class="no-more">
|
||||
<text class="no-more-text">没有更多了</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 发货弹窗 -->
|
||||
<view v-if="showShipModal" class="modal-mask" @click="closeShipModal">
|
||||
<view class="modal-content" @click.stop>
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">发货</text>
|
||||
<text class="modal-close" @click="closeShipModal">×</text>
|
||||
</view>
|
||||
<view class="modal-body">
|
||||
<view class="form-item">
|
||||
<text class="form-label">物流公司</text>
|
||||
<picker
|
||||
class="form-picker"
|
||||
:range="logisticsCompanies"
|
||||
range-key="name"
|
||||
@change="onLogisticsChange"
|
||||
>
|
||||
<view class="picker-value">
|
||||
{{ selectedLogistics.name || '请选择物流公司' }}
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">物流单号</text>
|
||||
<input
|
||||
class="form-input"
|
||||
v-model="trackingNumber"
|
||||
placeholder="请输入物流单号"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<view class="modal-footer">
|
||||
<view class="modal-btn cancel" @click="closeShipModal">取消</view>
|
||||
<view class="modal-btn confirm" @click="confirmShip">确认发货</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
type OrderItemType = {
|
||||
id: string
|
||||
order_id: string
|
||||
product_id: string
|
||||
sku_id: string
|
||||
product_name: string
|
||||
sku_name: string
|
||||
price: number
|
||||
quantity: number
|
||||
image_url: string
|
||||
sku_snapshot: string
|
||||
}
|
||||
|
||||
type OrderType = {
|
||||
id: string
|
||||
order_no: string
|
||||
user_id: string
|
||||
merchant_id: string
|
||||
order_status: number
|
||||
total_amount: number
|
||||
product_amount: number
|
||||
shipping_fee: number
|
||||
paid_amount: number
|
||||
shipping_address: string
|
||||
remark: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
items: OrderItemType[]
|
||||
}
|
||||
|
||||
type TabType = {
|
||||
name: string
|
||||
status: number
|
||||
count: number
|
||||
}
|
||||
|
||||
type LogisticsType = {
|
||||
name: string | null
|
||||
code: string | null
|
||||
}
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
tabs: [
|
||||
{ name: '全部', status: -2, count: 0 },
|
||||
{ name: '待付款', status: 1, count: 0 },
|
||||
{ name: '待发货', status: 2, count: 0 },
|
||||
{ name: '待收货', status: 3, count: 0 },
|
||||
{ name: '已完成', status: 4, count: 0 },
|
||||
{ name: '退款', status: 0, count: 0 }
|
||||
] as TabType[],
|
||||
currentTab: -2,
|
||||
searchKeyword: '',
|
||||
orders: [] as OrderType[],
|
||||
loading: false,
|
||||
loadingMore: false,
|
||||
refreshing: false,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
hasMore: true,
|
||||
merchantId: '',
|
||||
|
||||
showShipModal: false,
|
||||
currentOrder: null as OrderType | null,
|
||||
logisticsCompanies: [
|
||||
{ name: '顺丰速运', code: 'SF' },
|
||||
{ name: '圆通速递', code: 'YTO' },
|
||||
{ name: '中通快递', code: 'ZTO' },
|
||||
{ name: '韵达快递', code: 'YD' },
|
||||
{ name: '申通快递', code: 'STO' },
|
||||
{ name: 'EMS', code: 'EMS' },
|
||||
{ name: '京东物流', code: 'JD' }
|
||||
] as LogisticsType[],
|
||||
selectedLogistics: null as LogisticsType | null,
|
||||
trackingNumber: ''
|
||||
}
|
||||
},
|
||||
|
||||
onLoad(options: any) {
|
||||
const type = options.type as string
|
||||
if (type && type !== 'all') {
|
||||
const statusMap: Record<string, number> = {
|
||||
'pending': 1,
|
||||
'shipped': 3,
|
||||
'refund': 0,
|
||||
'completed': 4
|
||||
}
|
||||
this.currentTab = statusMap[type] ?? -2
|
||||
}
|
||||
this.initMerchantId()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
if (this.merchantId) {
|
||||
this.loadOrders()
|
||||
this.loadOrderCounts()
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
this.loadOrders()
|
||||
this.loadOrderCounts()
|
||||
}, 500)
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async initMerchantId() {
|
||||
try {
|
||||
const session = supa.getSession()
|
||||
if (session != null && session.user != null) {
|
||||
this.merchantId = session.user.getString('id') || ''
|
||||
}
|
||||
if (!this.merchantId) {
|
||||
this.merchantId = uni.getStorageSync('user_id') || ''
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取商户ID失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
async loadOrders() {
|
||||
if (this.loading) return
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
let query = supa
|
||||
.from('ml_orders')
|
||||
.select(`
|
||||
*,
|
||||
order_items!inner (
|
||||
id,
|
||||
order_id,
|
||||
product_id,
|
||||
sku_id,
|
||||
product_name,
|
||||
sku_name,
|
||||
price,
|
||||
quantity,
|
||||
image_url,
|
||||
sku_snapshot
|
||||
)
|
||||
`)
|
||||
.eq('merchant_id', this.merchantId)
|
||||
.order('created_at', { ascending: false })
|
||||
.page(this.page)
|
||||
.limit(this.limit)
|
||||
|
||||
if (this.currentTab !== -2) {
|
||||
if (this.currentTab === 0) {
|
||||
query = query.eq('order_status', 0)
|
||||
} else {
|
||||
query = query.eq('order_status', this.currentTab)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.searchKeyword) {
|
||||
query = query.like('order_no', `%${this.searchKeyword}%`)
|
||||
}
|
||||
|
||||
const response = await query.execute()
|
||||
|
||||
if (response.error != null) {
|
||||
console.error('获取订单失败:', response.error)
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
const rawData = response.data as any[]
|
||||
if (rawData == null || rawData.length === 0) {
|
||||
this.orders = []
|
||||
this.hasMore = false
|
||||
return
|
||||
}
|
||||
|
||||
const ordersData: OrderType[] = []
|
||||
for (let i = 0; i < rawData.length; i++) {
|
||||
const item = rawData[i]
|
||||
const orderObj = item as UTSJSONObject
|
||||
|
||||
const order: OrderType = {
|
||||
id: orderObj.getString('id') || '',
|
||||
order_no: orderObj.getString('order_no') || '',
|
||||
user_id: orderObj.getString('user_id') || '',
|
||||
merchant_id: orderObj.getString('merchant_id') || '',
|
||||
order_status: orderObj.getNumber('order_status') || 1,
|
||||
total_amount: orderObj.getNumber('total_amount') || 0,
|
||||
product_amount: orderObj.getNumber('product_amount') || 0,
|
||||
shipping_fee: orderObj.getNumber('shipping_fee') || 0,
|
||||
paid_amount: orderObj.getNumber('paid_amount') || 0,
|
||||
shipping_address: orderObj.getString('shipping_address') || '',
|
||||
remark: orderObj.getString('remark') || '',
|
||||
created_at: orderObj.getString('created_at') || '',
|
||||
updated_at: orderObj.getString('updated_at') || '',
|
||||
items: []
|
||||
}
|
||||
|
||||
const itemsObj = orderObj.get('order_items')
|
||||
if (itemsObj != null && Array.isArray(itemsObj)) {
|
||||
const itemsArray = itemsObj as any[]
|
||||
for (let j = 0; j < itemsArray.length; j++) {
|
||||
const orderItem = itemsArray[j] as UTSJSONObject
|
||||
order.items.push({
|
||||
id: orderItem.getString('id') || '',
|
||||
order_id: orderItem.getString('order_id') || '',
|
||||
product_id: orderItem.getString('product_id') || '',
|
||||
sku_id: orderItem.getString('sku_id') || '',
|
||||
product_name: orderItem.getString('product_name') || '',
|
||||
sku_name: orderItem.getString('sku_name') || '',
|
||||
price: orderItem.getNumber('price') || 0,
|
||||
quantity: orderItem.getNumber('quantity') || 0,
|
||||
image_url: orderItem.getString('image_url') || '',
|
||||
sku_snapshot: ''
|
||||
} as OrderItemType)
|
||||
}
|
||||
}
|
||||
|
||||
ordersData.push(order)
|
||||
}
|
||||
|
||||
if (this.page === 1) {
|
||||
this.orders = ordersData
|
||||
} else {
|
||||
this.orders = [...this.orders, ...ordersData]
|
||||
}
|
||||
|
||||
this.hasMore = rawData.length >= this.limit
|
||||
} catch (e) {
|
||||
console.error('获取订单异常:', e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
this.refreshing = false
|
||||
}
|
||||
},
|
||||
|
||||
async loadOrderCounts() {
|
||||
try {
|
||||
const response = await supa
|
||||
.from('ml_orders')
|
||||
.select('order_status', { count: 'exact' })
|
||||
.eq('merchant_id', this.merchantId)
|
||||
.execute()
|
||||
|
||||
if (response.error != null || response.total == null) return
|
||||
|
||||
const counts = {
|
||||
1: 0, 2: 0, 3: 0, 4: 0, 0: 0
|
||||
}
|
||||
let total = 0
|
||||
|
||||
const rawData = response.data as any[]
|
||||
if (rawData != null) {
|
||||
for (let i = 0; i < rawData.length; i++) {
|
||||
const item = rawData[i] as UTSJSONObject
|
||||
const status = item.getNumber('order_status') || 1
|
||||
if (status === 1) counts[1]++
|
||||
else if (status === 2) counts[2]++
|
||||
else if (status === 3) counts[3]++
|
||||
else if (status === 4) counts[4]++
|
||||
else if (status === 0) counts[0]++
|
||||
total++
|
||||
}
|
||||
}
|
||||
|
||||
this.tabs[0].count = total
|
||||
this.tabs[1].count = counts[1] || 0
|
||||
this.tabs[2].count = counts[2] || 0
|
||||
this.tabs[3].count = counts[3] || 0
|
||||
this.tabs[4].count = counts[4] || 0
|
||||
this.tabs[5].count = counts[0] || 0
|
||||
} catch (e) {
|
||||
console.error('获取订单数量异常:', e)
|
||||
}
|
||||
},
|
||||
|
||||
switchTab(status: number) {
|
||||
this.currentTab = status
|
||||
this.page = 1
|
||||
this.hasMore = true
|
||||
this.loadOrders()
|
||||
},
|
||||
|
||||
handleSearch() {
|
||||
this.page = 1
|
||||
this.hasMore = true
|
||||
this.loadOrders()
|
||||
},
|
||||
|
||||
onRefresh() {
|
||||
this.refreshing = true
|
||||
this.page = 1
|
||||
this.loadOrders()
|
||||
this.loadOrderCounts()
|
||||
},
|
||||
|
||||
loadMore() {
|
||||
if (!this.loadingMore && this.hasMore) {
|
||||
this.loadingMore = true
|
||||
this.page++
|
||||
this.loadOrders().then(() => {
|
||||
this.loadingMore = false
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
viewOrderDetail(orderId: string) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/merchant/order-detail?id=${orderId}`
|
||||
})
|
||||
},
|
||||
|
||||
shipOrder(order: OrderType) {
|
||||
this.currentOrder = order
|
||||
this.showShipModal = true
|
||||
},
|
||||
|
||||
closeShipModal() {
|
||||
this.showShipModal = false
|
||||
this.currentOrder = null
|
||||
this.selectedLogistics = null
|
||||
this.trackingNumber = ''
|
||||
},
|
||||
|
||||
onLogisticsChange(e: any) {
|
||||
const index = e.detail.value as number
|
||||
this.selectedLogistics = this.logisticsCompanies[index]
|
||||
},
|
||||
|
||||
async confirmShip() {
|
||||
if (!this.selectedLogistics.name) {
|
||||
uni.showToast({ title: '请选择物流公司', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!this.trackingNumber) {
|
||||
uni.showToast({ title: '请输入物流单号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await supa
|
||||
.from('ml_orders')
|
||||
.update({
|
||||
order_status: 3,
|
||||
shipping_company: this.selectedLogistics.name,
|
||||
tracking_number: this.trackingNumber,
|
||||
shipped_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
})
|
||||
.eq('id', this.currentOrder!.id)
|
||||
.execute()
|
||||
|
||||
if (response.error != null) {
|
||||
uni.showToast({ title: '发货失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
uni.showToast({ title: '发货成功', icon: 'success' })
|
||||
this.closeShipModal()
|
||||
this.loadOrders()
|
||||
this.loadOrderCounts()
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '发货失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
viewLogistics(order: OrderType) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/merchant/logistics?orderId=${order.id}`
|
||||
})
|
||||
},
|
||||
|
||||
async deleteOrder(order: OrderType) {
|
||||
uni.showModal({
|
||||
title: '确认删除',
|
||||
content: '确定要删除该订单吗?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
const response = await supa
|
||||
.from('ml_orders')
|
||||
.delete()
|
||||
.eq('id', order.id)
|
||||
.execute()
|
||||
|
||||
if (response.error != null) {
|
||||
uni.showToast({ title: '删除失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
uni.showToast({ title: '删除成功', icon: 'success' })
|
||||
this.loadOrders()
|
||||
this.loadOrderCounts()
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '删除失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
getStatusText(status: number): string {
|
||||
if (status === 1) return '待付款'
|
||||
if (status === 2) return '待发货'
|
||||
if (status === 3) return '待收货'
|
||||
if (status === 4) return '已完成'
|
||||
if (status === 0) return '退款中'
|
||||
if (status === 5 || status === -1) return '已取消'
|
||||
return '未知'
|
||||
},
|
||||
|
||||
getTotalQuantity(items: OrderItemType[]): number {
|
||||
let total = 0
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
total += items[i].quantity
|
||||
}
|
||||
return total
|
||||
},
|
||||
|
||||
formatTime(timeStr: string): string {
|
||||
if (!timeStr) return ''
|
||||
const date = new Date(timeStr)
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
||||
const day = date.getDate().toString().padStart(2, '0')
|
||||
const hour = date.getHours().toString().padStart(2, '0')
|
||||
const minute = date.getMinutes().toString().padStart(2, '0')
|
||||
return `${month}-${day} ${hour}:${minute}`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.orders-page {
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.tabs-container {
|
||||
background-color: #fff;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.tabs-scroll {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
padding: 0 20rpx;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
position: relative;
|
||||
padding: 24rpx 30rpx;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tab-text {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.tab-item.active .tab-text {
|
||||
color: #007AFF;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tab-item.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 40rpx;
|
||||
height: 4rpx;
|
||||
background-color: #007AFF;
|
||||
border-radius: 2rpx;
|
||||
}
|
||||
|
||||
.tab-badge {
|
||||
position: absolute;
|
||||
top: 10rpx;
|
||||
right: 10rpx;
|
||||
min-width: 32rpx;
|
||||
height: 32rpx;
|
||||
padding: 0 8rpx;
|
||||
background-color: #FF3B30;
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
border-radius: 16rpx;
|
||||
text-align: center;
|
||||
line-height: 32rpx;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx 30rpx;
|
||||
background-color: #fff;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
height: 64rpx;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 32rpx;
|
||||
padding: 0 30rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
margin-left: 20rpx;
|
||||
padding: 16rpx 30rpx;
|
||||
background-color: #007AFF;
|
||||
color: #fff;
|
||||
font-size: 26rpx;
|
||||
border-radius: 32rpx;
|
||||
}
|
||||
|
||||
.orders-list {
|
||||
padding: 0 20rpx;
|
||||
height: calc(100vh - 300rpx);
|
||||
}
|
||||
|
||||
.loading-container, .empty-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 100rpx 0;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 100rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.empty-text, .loading-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.order-card {
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 20rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.order-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20rpx 24rpx;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.order-info-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.order-no {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.order-time {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.order-status {
|
||||
font-size: 24rpx;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
.status-1 {
|
||||
background-color: #FFF3E0;
|
||||
color: #F57C00;
|
||||
}
|
||||
|
||||
.status-2 {
|
||||
background-color: #E3F2FD;
|
||||
color: #1976D2;
|
||||
}
|
||||
|
||||
.status-3 {
|
||||
background-color: #E8F5E9;
|
||||
color: #388E3C;
|
||||
}
|
||||
|
||||
.status-4, .status-5 {
|
||||
background-color: #F5F5F5;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.status-0, .status--1 {
|
||||
background-color: #FFEBEE;
|
||||
color: #F44336;
|
||||
}
|
||||
|
||||
.order-products {
|
||||
padding: 0 24rpx;
|
||||
}
|
||||
|
||||
.product-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.product-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 8rpx;
|
||||
margin-right: 20rpx;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.product-spec {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.product-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.product-price {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.product-quantity {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.order-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20rpx 24rpx;
|
||||
border-top: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.order-amount {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.amount-label {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.amount-value {
|
||||
font-size: 28rpx;
|
||||
color: #FF3B30;
|
||||
font-weight: bold;
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
|
||||
.order-actions {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 12rpx 24rpx;
|
||||
font-size: 24rpx;
|
||||
border-radius: 28rpx;
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background-color: #007AFF;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.action-btn.info {
|
||||
background-color: #E3F2FD;
|
||||
color: #1976D2;
|
||||
}
|
||||
|
||||
.action-btn.default {
|
||||
background-color: #F5F5F5;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.load-more, .no-more {
|
||||
padding: 30rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.load-more-text, .no-more-text {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.modal-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
border-radius: 24rpx 24rpx 0 0;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
font-size: 44rpx;
|
||||
color: #999;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.form-picker, .form-input {
|
||||
height: 72rpx;
|
||||
border: 1rpx solid #e5e5e5;
|
||||
border-radius: 8rpx;
|
||||
padding: 0 20rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.picker-value {
|
||||
height: 72rpx;
|
||||
line-height: 72rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
border-top: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.modal-btn {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
text-align: center;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.modal-btn.cancel {
|
||||
color: #666;
|
||||
border-right: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.modal-btn.confirm {
|
||||
color: #007AFF;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
715
pages/mall/merchant/product-edit.uvue
Normal file
715
pages/mall/merchant/product-edit.uvue
Normal file
@@ -0,0 +1,715 @@
|
||||
<!-- 商家端 - 商品编辑页面 -->
|
||||
<template>
|
||||
<view class="product-edit-page">
|
||||
<!-- 商品基本信息 -->
|
||||
<view class="section">
|
||||
<view class="section-title">基本信息</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">商品名称 *</text>
|
||||
<input
|
||||
class="input"
|
||||
v-model="product.name"
|
||||
placeholder="请输入商品名称"
|
||||
maxlength="100"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">商品副标题</text>
|
||||
<input
|
||||
class="input"
|
||||
v-model="product.subtitle"
|
||||
placeholder="请输入商品副标题"
|
||||
maxlength="200"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">商品分类 *</text>
|
||||
<picker
|
||||
class="picker"
|
||||
:range="categories"
|
||||
range-key="name"
|
||||
:value="categoryIndex"
|
||||
@change="onCategoryChange"
|
||||
>
|
||||
<view class="picker-value">
|
||||
{{ selectedCategory?.name || '请选择分类' }}
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">商品品牌</text>
|
||||
<picker
|
||||
class="picker"
|
||||
:range="brands"
|
||||
range-key="name"
|
||||
:value="brandIndex"
|
||||
@change="onBrandChange"
|
||||
>
|
||||
<view class="picker-value">
|
||||
{{ selectedBrand?.name || '请选择品牌' }}
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 商品图片 -->
|
||||
<view class="section">
|
||||
<view class="section-title">商品图片</view>
|
||||
|
||||
<view class="image-section">
|
||||
<text class="label">主图 *</text>
|
||||
<view class="image-grid">
|
||||
<view class="image-item main-image" @click="chooseMainImage">
|
||||
<image v-if="product.main_image_url" :src="product.main_image_url" mode="aspectFill" class="preview-image"/>
|
||||
<view v-else class="add-image">+</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="image-section">
|
||||
<text class="label">轮播图</text>
|
||||
<view class="image-grid">
|
||||
<view
|
||||
v-for="(img, index) in product.imageList"
|
||||
:key="index"
|
||||
class="image-item"
|
||||
>
|
||||
<image :src="img" mode="aspectFill" class="preview-image"/>
|
||||
<view class="delete-btn" @click="removeImage(index)">×</view>
|
||||
</view>
|
||||
<view class="image-item add-image" @click="chooseImages" v-if="product.imageList.length < 5">
|
||||
+
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 价格库存 -->
|
||||
<view class="section">
|
||||
<view class="section-title">价格库存</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">销售价 *</text>
|
||||
<view class="price-input">
|
||||
<text class="unit">¥</text>
|
||||
<input
|
||||
class="input"
|
||||
type="digit"
|
||||
v-model="product.base_price"
|
||||
placeholder="0.00"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">市场价</text>
|
||||
<view class="price-input">
|
||||
<text class="unit">¥</text>
|
||||
<input
|
||||
class="input"
|
||||
type="digit"
|
||||
v-model="product.market_price"
|
||||
placeholder="0.00"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">成本价</text>
|
||||
<view class="price-input">
|
||||
<text class="unit">¥</text>
|
||||
<input
|
||||
class="input"
|
||||
type="digit"
|
||||
v-model="product.cost_price"
|
||||
placeholder="0.00"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">总库存 *</text>
|
||||
<input
|
||||
class="input"
|
||||
type="number"
|
||||
v-model="product.total_stock"
|
||||
placeholder="0"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">库存预警</text>
|
||||
<input
|
||||
class="input"
|
||||
type="number"
|
||||
v-model="product.warning_stock"
|
||||
placeholder="库存低于此值提醒"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 商品属性 -->
|
||||
<view class="section">
|
||||
<view class="section-title">商品属性</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">商品单位</text>
|
||||
<input
|
||||
class="input"
|
||||
v-model="product.unit"
|
||||
placeholder="如: 件, 盒, 箱"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="switch-item">
|
||||
<text class="label">热卖商品</text>
|
||||
<switch
|
||||
:checked="product.is_hot"
|
||||
@change="product.is_hot = !product.is_hot"
|
||||
color="#007AFF"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="switch-item">
|
||||
<text class="label">新品上架</text>
|
||||
<switch
|
||||
:checked="product.is_new"
|
||||
@change="product.is_new = !product.is_new"
|
||||
color="#007AFF"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="switch-item">
|
||||
<text class="label">推荐商品</text>
|
||||
<switch
|
||||
:checked="product.is_featured"
|
||||
@change="product.is_featured = !product.is_featured"
|
||||
color="#007AFF"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 商品详情 -->
|
||||
<view class="section">
|
||||
<view class="section-title">商品详情</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">商品描述</text>
|
||||
<textarea
|
||||
class="textarea"
|
||||
v-model="product.description"
|
||||
placeholder="请输入商品详细描述"
|
||||
:maxlength="2000"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<view class="submit-bar">
|
||||
<view class="submit-btn primary" @click="saveProduct">
|
||||
{{ isEdit ? '保存修改' : '发布商品' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
type CategoryType = {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
type BrandType = {
|
||||
id: string
|
||||
name: string
|
||||
logo_url: string
|
||||
}
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
productId: '',
|
||||
isEdit: false,
|
||||
categories: [] as CategoryType[],
|
||||
categoryIndex: -1,
|
||||
selectedCategory: null as CategoryType | null,
|
||||
brands: [] as BrandType[],
|
||||
brandIndex: -1,
|
||||
selectedBrand: null as BrandType | null,
|
||||
product: {
|
||||
name: '',
|
||||
subtitle: '',
|
||||
category_id: '',
|
||||
brand_id: '',
|
||||
main_image_url: '',
|
||||
imageList: [] as string[],
|
||||
base_price: '',
|
||||
market_price: '',
|
||||
cost_price: '',
|
||||
total_stock: '',
|
||||
warning_stock: '10',
|
||||
unit: '件',
|
||||
is_hot: false,
|
||||
is_new: false,
|
||||
is_featured: false,
|
||||
description: ''
|
||||
},
|
||||
merchantId: ''
|
||||
}
|
||||
},
|
||||
|
||||
onLoad(options: any) {
|
||||
const productId = options.productId as string
|
||||
if (productId) {
|
||||
this.productId = productId
|
||||
this.isEdit = true
|
||||
this.loadProductDetail(productId)
|
||||
}
|
||||
this.initMerchantId()
|
||||
this.loadCategories()
|
||||
this.loadBrands()
|
||||
},
|
||||
|
||||
methods: {
|
||||
async initMerchantId() {
|
||||
try {
|
||||
const session = supa.getSession()
|
||||
if (session != null && session.user != null) {
|
||||
this.merchantId = session.user.getString('id') || ''
|
||||
}
|
||||
if (!this.merchantId) {
|
||||
this.merchantId = uni.getStorageSync('user_id') || ''
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取商户ID失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
async loadCategories() {
|
||||
try {
|
||||
const response = await supa
|
||||
.from('ml_categories')
|
||||
.select('id, name')
|
||||
.eq('is_active', true)
|
||||
.order('sort_order', { ascending: true })
|
||||
.execute()
|
||||
|
||||
if (response.error != null) {
|
||||
console.error('获取分类失败:', response.error)
|
||||
return
|
||||
}
|
||||
|
||||
const rawData = response.data as any[]
|
||||
if (rawData == null) return
|
||||
|
||||
for (let i = 0; i < rawData.length; i++) {
|
||||
const item = rawData[i] as UTSJSONObject
|
||||
this.categories.push({
|
||||
id: item.getString('id') || '',
|
||||
name: item.getString('name') || ''
|
||||
} as CategoryType)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取分类异常:', e)
|
||||
}
|
||||
},
|
||||
|
||||
async loadBrands() {
|
||||
try {
|
||||
const response = await supa
|
||||
.from('ml_brands')
|
||||
.select('id, name, logo_url')
|
||||
.eq('is_active', true)
|
||||
.order('name', { ascending: true })
|
||||
.execute()
|
||||
|
||||
if (response.error != null) {
|
||||
console.error('获取品牌失败:', response.error)
|
||||
return
|
||||
}
|
||||
|
||||
const rawData = response.data as any[]
|
||||
if (rawData == null) return
|
||||
|
||||
for (let i = 0; i < rawData.length; i++) {
|
||||
const item = rawData[i] as UTSJSONObject
|
||||
this.brands.push({
|
||||
id: item.getString('id') || '',
|
||||
name: item.getString('name') || '',
|
||||
logo_url: item.getString('logo_url') || ''
|
||||
} as BrandType)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取品牌异常:', e)
|
||||
}
|
||||
},
|
||||
|
||||
async loadProductDetail(productId: string) {
|
||||
try {
|
||||
const response = await supa
|
||||
.from('ml_products')
|
||||
.select('*')
|
||||
.eq('id', productId)
|
||||
.single()
|
||||
.execute()
|
||||
|
||||
if (response.error != null) {
|
||||
console.error('获取商品详情失败:', response.error)
|
||||
return
|
||||
}
|
||||
|
||||
const rawData = response.data as UTSJSONObject
|
||||
if (rawData == null) return
|
||||
|
||||
this.product = {
|
||||
name: rawData.getString('name') || '',
|
||||
subtitle: rawData.getString('subtitle') || '',
|
||||
category_id: rawData.getString('category_id') || '',
|
||||
brand_id: rawData.getString('brand_id') || '',
|
||||
main_image_url: rawData.getString('main_image_url') || '',
|
||||
imageList: this.parseImageUrls(rawData.getString('image_urls')),
|
||||
base_price: rawData.getString('base_price') || '',
|
||||
market_price: rawData.getString('market_price') || '',
|
||||
cost_price: rawData.getString('cost_price') || '',
|
||||
total_stock: rawData.getString('total_stock') || '',
|
||||
warning_stock: rawData.getString('warning_stock') || '10',
|
||||
unit: rawData.getString('unit') || '件',
|
||||
is_hot: rawData.getBoolean('is_hot') || false,
|
||||
is_new: rawData.getBoolean('is_new') || false,
|
||||
is_featured: rawData.getBoolean('is_featured') || false,
|
||||
description: rawData.getString('description') || ''
|
||||
}
|
||||
|
||||
if (this.product.category_id) {
|
||||
this.categoryIndex = this.categories.findIndex(c => c.id === this.product.category_id)
|
||||
if (this.categoryIndex >= 0) {
|
||||
this.selectedCategory = this.categories[this.categoryIndex]
|
||||
}
|
||||
}
|
||||
|
||||
if (this.product.brand_id) {
|
||||
this.brandIndex = this.brands.findIndex(b => b.id === this.product.brand_id)
|
||||
if (this.brandIndex >= 0) {
|
||||
this.selectedBrand = this.brands[this.brandIndex]
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取商品详情异常:', e)
|
||||
}
|
||||
},
|
||||
|
||||
parseImageUrls(urlsStr: string): string[] {
|
||||
if (!urlsStr) return []
|
||||
try {
|
||||
const arr = JSON.parse(urlsStr)
|
||||
return Array.isArray(arr) ? arr : []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
},
|
||||
|
||||
onCategoryChange(e: any) {
|
||||
const index = e.detail.value as number
|
||||
this.categoryIndex = index
|
||||
this.selectedCategory = this.categories[index]
|
||||
this.product.category_id = this.selectedCategory.id
|
||||
},
|
||||
|
||||
onBrandChange(e: any) {
|
||||
const index = e.detail.value as number
|
||||
this.brandIndex = index
|
||||
this.selectedBrand = this.brands[index]
|
||||
this.product.brand_id = this.selectedBrand.id
|
||||
},
|
||||
|
||||
chooseMainImage() {
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
sizeType: ['compressed'],
|
||||
success: (res) => {
|
||||
this.product.main_image_url = res.tempFilePaths[0]
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
chooseImages() {
|
||||
const remainCount = 5 - this.product.imageList.length
|
||||
uni.chooseImage({
|
||||
count: remainCount,
|
||||
sizeType: ['compressed'],
|
||||
success: (res) => {
|
||||
this.product.imageList = [...this.product.imageList, ...res.tempFilePaths]
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
removeImage(index: number) {
|
||||
this.product.imageList.splice(index, 1)
|
||||
},
|
||||
|
||||
async saveProduct() {
|
||||
if (!this.product.name) {
|
||||
uni.showToast({ title: '请输入商品名称', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!this.product.category_id) {
|
||||
uni.showToast({ title: '请选择商品分类', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!this.product.base_price) {
|
||||
uni.showToast({ title: '请输入销售价', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!this.product.total_stock) {
|
||||
uni.showToast({ title: '请输入总库存', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
uni.showLoading({ title: '保存中...' })
|
||||
|
||||
try {
|
||||
const imageUrlsStr = JSON.stringify(this.product.imageList)
|
||||
|
||||
const productData = {
|
||||
merchant_id: this.merchantId,
|
||||
name: this.product.name,
|
||||
subtitle: this.product.subtitle,
|
||||
category_id: this.product.category_id,
|
||||
brand_id: this.product.brand_id || null,
|
||||
main_image_url: this.product.main_image_url,
|
||||
image_urls: imageUrlsStr,
|
||||
base_price: parseFloat(this.product.base_price),
|
||||
market_price: this.product.market_price ? parseFloat(this.product.market_price) : null,
|
||||
cost_price: this.product.cost_price ? parseFloat(this.product.cost_price) : null,
|
||||
total_stock: parseInt(this.product.total_stock),
|
||||
warning_stock: parseInt(this.product.warning_stock) || 10,
|
||||
unit: this.product.unit,
|
||||
is_hot: this.product.is_hot,
|
||||
is_new: this.product.is_new,
|
||||
is_featured: this.product.is_featured,
|
||||
description: this.product.description,
|
||||
status: 1,
|
||||
updated_at: new Date().toISOString()
|
||||
}
|
||||
|
||||
let response
|
||||
if (this.isEdit) {
|
||||
response = await supa
|
||||
.from('ml_products')
|
||||
.update(productData)
|
||||
.eq('id', this.productId)
|
||||
.execute()
|
||||
} else {
|
||||
productData['created_at'] = new Date().toISOString()
|
||||
response = await supa
|
||||
.from('ml_products')
|
||||
.insert(productData)
|
||||
.execute()
|
||||
}
|
||||
|
||||
uni.hideLoading()
|
||||
|
||||
if (response.error != null) {
|
||||
console.error('保存商品失败:', response.error)
|
||||
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
} catch (e) {
|
||||
uni.hideLoading()
|
||||
console.error('保存商品异常:', e)
|
||||
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.product-edit-page {
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
padding-bottom: 160rpx;
|
||||
}
|
||||
|
||||
.section {
|
||||
background-color: #fff;
|
||||
margin-bottom: 20rpx;
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 30rpx;
|
||||
padding-bottom: 20rpx;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.input {
|
||||
height: 72rpx;
|
||||
border: 1rpx solid #e5e5e5;
|
||||
border-radius: 8rpx;
|
||||
padding: 0 20rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.price-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1rpx solid #e5e5e5;
|
||||
border-radius: 8rpx;
|
||||
padding: 0 20rpx;
|
||||
}
|
||||
|
||||
.price-input .unit {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
|
||||
.price-input .input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.picker {
|
||||
height: 72rpx;
|
||||
border: 1rpx solid #e5e5e5;
|
||||
border-radius: 8rpx;
|
||||
padding: 0 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.picker-value {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.switch-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.switch-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.textarea {
|
||||
width: 100%;
|
||||
height: 200rpx;
|
||||
border: 1rpx solid #e5e5e5;
|
||||
border-radius: 8rpx;
|
||||
padding: 20rpx;
|
||||
font-size: 28rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.image-section {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.image-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.image-item {
|
||||
position: relative;
|
||||
width: 150rpx;
|
||||
height: 150rpx;
|
||||
border-radius: 8rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main-image {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.add-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #f5f5f5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 60rpx;
|
||||
color: #999;
|
||||
border: 2rpx dashed #ddd;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
position: absolute;
|
||||
top: 5rpx;
|
||||
right: 5rpx;
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.submit-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 20rpx 30rpx;
|
||||
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||
background-color: #fff;
|
||||
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
text-align: center;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
border-radius: 44rpx;
|
||||
}
|
||||
|
||||
.submit-btn.primary {
|
||||
background: linear-gradient(135deg, #007AFF 0%, #5856D6 100%);
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
712
pages/mall/merchant/products.uvue
Normal file
712
pages/mall/merchant/products.uvue
Normal file
@@ -0,0 +1,712 @@
|
||||
<!-- 商家端 - 商品管理列表页面 -->
|
||||
<template>
|
||||
<view class="products-page">
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-bar">
|
||||
<input
|
||||
class="search-input"
|
||||
type="text"
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索商品名称"
|
||||
@confirm="handleSearch"
|
||||
/>
|
||||
<view class="search-btn" @click="handleSearch">搜索</view>
|
||||
</view>
|
||||
|
||||
<!-- 筛选标签 -->
|
||||
<view class="filter-tabs">
|
||||
<view
|
||||
class="filter-tab"
|
||||
:class="{ active: currentFilter === 'all' }"
|
||||
@click="switchFilter('all')"
|
||||
>
|
||||
全部
|
||||
</view>
|
||||
<view
|
||||
class="filter-tab"
|
||||
:class="{ active: currentFilter === 'onsale' }"
|
||||
@click="switchFilter('onsale')"
|
||||
>
|
||||
上架
|
||||
</view>
|
||||
<view
|
||||
class="filter-tab"
|
||||
:class="{ active: currentFilter === 'offsale' }"
|
||||
@click="switchFilter('offsale')"
|
||||
>
|
||||
下架
|
||||
</view>
|
||||
<view
|
||||
class="filter-tab"
|
||||
:class="{ active: currentFilter === 'low_stock' }"
|
||||
@click="switchFilter('low_stock')"
|
||||
>
|
||||
库存预警
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 商品列表 -->
|
||||
<scroll-view
|
||||
class="products-list"
|
||||
scroll-y
|
||||
:refresher-enabled="true"
|
||||
:refresher-triggered="refreshing"
|
||||
@refresherrefresh="onRefresh"
|
||||
@scrolltolower="loadMore"
|
||||
>
|
||||
<view v-if="loading && products.length === 0" class="loading-container">
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<view v-else-if="products.length === 0" class="empty-container">
|
||||
<text class="empty-icon">📦</text>
|
||||
<text class="empty-text">暂无商品</text>
|
||||
<view class="add-first-btn" @click="addProduct">添加第一个商品</view>
|
||||
</view>
|
||||
|
||||
<view v-else>
|
||||
<view
|
||||
v-for="product in products"
|
||||
:key="product.id"
|
||||
class="product-card"
|
||||
@click="viewProductDetail(product.id)"
|
||||
>
|
||||
<image
|
||||
:src="product.main_image_url || '/static/images/default-product.png'"
|
||||
class="product-image"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="product-info">
|
||||
<view class="product-header">
|
||||
<text class="product-name">{{ product.name }}</text>
|
||||
<text class="product-status" :class="getStatusClass(product.status)">
|
||||
{{ getStatusText(product.status) }}
|
||||
</text>
|
||||
</view>
|
||||
<text class="product-subtitle">{{ product.subtitle || '暂无描述' }}</text>
|
||||
<view class="product-tags" v-if="product.tags">
|
||||
<text v-if="product.is_hot" class="tag hot">热</text>
|
||||
<text v-if="product.is_new" class="tag new">新</text>
|
||||
<text v-if="product.is_featured" class="tag recommend">荐</text>
|
||||
</view>
|
||||
<view class="product-stats">
|
||||
<view class="price-row">
|
||||
<text class="current-price">¥{{ product.base_price }}</text>
|
||||
<text v-if="product.market_price" class="original-price">¥{{ product.market_price }}</text>
|
||||
</view>
|
||||
<view class="stock-row">
|
||||
<text class="stock">库存: {{ product.total_stock || 0 }}</text>
|
||||
<text class="sales">销量: {{ product.sale_count || 0 }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="product-actions" @click.stop>
|
||||
<view
|
||||
class="action-btn"
|
||||
:class="product.status === 1 ? 'warning' : 'success'"
|
||||
@click="toggleStatus(product)"
|
||||
>
|
||||
{{ product.status === 1 ? '下架' : '上架' }}
|
||||
</view>
|
||||
<view class="action-btn default" @click="editProduct(product.id)">编辑</view>
|
||||
<view class="action-btn danger" @click="deleteProduct(product)">删除</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="loadingMore" class="load-more">
|
||||
<text class="load-more-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<view v-if="!hasMore && products.length > 0" class="no-more">
|
||||
<text class="no-more-text">没有更多了</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 添加商品按钮 -->
|
||||
<view class="add-product-btn" @click="addProduct">
|
||||
<text class="add-icon">+</text>
|
||||
<text class="add-text">添加商品</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
type ProductType = {
|
||||
id: string
|
||||
merchant_id: string
|
||||
category_id: string
|
||||
name: string
|
||||
subtitle: string
|
||||
description: string
|
||||
main_image_url: string
|
||||
image_urls: string
|
||||
base_price: number
|
||||
market_price: number
|
||||
total_stock: number
|
||||
sale_count: number
|
||||
status: number
|
||||
is_hot: boolean
|
||||
is_new: boolean
|
||||
is_featured: boolean
|
||||
tags: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
searchKeyword: '',
|
||||
currentFilter: 'all',
|
||||
products: [] as ProductType[],
|
||||
loading: false,
|
||||
loadingMore: false,
|
||||
refreshing: false,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
hasMore: true,
|
||||
merchantId: '',
|
||||
lowStockThreshold: 10
|
||||
}
|
||||
},
|
||||
|
||||
onLoad(options: any) {
|
||||
const type = options.type as string
|
||||
if (type === 'add') {
|
||||
this.addProduct()
|
||||
} else if (type === 'low_stock') {
|
||||
this.currentFilter = 'low_stock'
|
||||
}
|
||||
this.initMerchantId()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.loadProducts()
|
||||
},
|
||||
|
||||
methods: {
|
||||
async initMerchantId() {
|
||||
try {
|
||||
const session = supa.getSession()
|
||||
if (session != null && session.user != null) {
|
||||
this.merchantId = session.user.getString('id') || ''
|
||||
}
|
||||
if (!this.merchantId) {
|
||||
this.merchantId = uni.getStorageSync('user_id') || ''
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取商户ID失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
async loadProducts() {
|
||||
if (this.loading) return
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
let query = supa
|
||||
.from('ml_products')
|
||||
.select('*')
|
||||
.eq('merchant_id', this.merchantId)
|
||||
.order('created_at', { ascending: false })
|
||||
.page(this.page)
|
||||
.limit(this.limit)
|
||||
|
||||
if (this.searchKeyword) {
|
||||
query = query.like('name', `%${this.searchKeyword}%`)
|
||||
}
|
||||
|
||||
if (this.currentFilter === 'onsale') {
|
||||
query = query.eq('status', 1)
|
||||
} else if (this.currentFilter === 'offsale') {
|
||||
query = query.eq('status', 0)
|
||||
} else if (this.currentFilter === 'low_stock') {
|
||||
query = query.lte('total_stock', this.lowStockThreshold).gte('total_stock', 0)
|
||||
}
|
||||
|
||||
const response = await query.execute()
|
||||
|
||||
if (response.error != null) {
|
||||
console.error('获取商品失败:', response.error)
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
const rawData = response.data as any[]
|
||||
if (rawData == null || rawData.length === 0) {
|
||||
this.products = []
|
||||
this.hasMore = false
|
||||
return
|
||||
}
|
||||
|
||||
const productsData: ProductType[] = []
|
||||
for (let i = 0; i < rawData.length; i++) {
|
||||
const item = rawData[i]
|
||||
const prodObj = item as UTSJSONObject
|
||||
productsData.push({
|
||||
id: prodObj.getString('id') || '',
|
||||
merchant_id: prodObj.getString('merchant_id') || '',
|
||||
category_id: prodObj.getString('category_id') || '',
|
||||
name: prodObj.getString('name') || '',
|
||||
subtitle: prodObj.getString('subtitle') || '',
|
||||
description: prodObj.getString('description') || '',
|
||||
main_image_url: prodObj.getString('main_image_url') || '',
|
||||
image_urls: prodObj.getString('image_urls') || '',
|
||||
base_price: prodObj.getNumber('base_price') || 0,
|
||||
market_price: prodObj.getNumber('market_price') || 0,
|
||||
total_stock: prodObj.getNumber('total_stock') || 0,
|
||||
sale_count: prodObj.getNumber('sale_count') || 0,
|
||||
status: prodObj.getNumber('status') || 0,
|
||||
is_hot: prodObj.getBoolean('is_hot') || false,
|
||||
is_new: prodObj.getBoolean('is_new') || false,
|
||||
is_featured: prodObj.getBoolean('is_featured') || false,
|
||||
tags: prodObj.getString('tags') || '',
|
||||
created_at: prodObj.getString('created_at') || '',
|
||||
updated_at: prodObj.getString('updated_at') || ''
|
||||
} as ProductType)
|
||||
}
|
||||
|
||||
if (this.page === 1) {
|
||||
this.products = productsData
|
||||
} else {
|
||||
this.products = [...this.products, ...productsData]
|
||||
}
|
||||
|
||||
this.hasMore = rawData.length >= this.limit
|
||||
} catch (e) {
|
||||
console.error('获取商品异常:', e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
this.refreshing = false
|
||||
}
|
||||
},
|
||||
|
||||
switchFilter(filter: string) {
|
||||
this.currentFilter = filter
|
||||
this.page = 1
|
||||
this.hasMore = true
|
||||
this.loadProducts()
|
||||
},
|
||||
|
||||
handleSearch() {
|
||||
this.page = 1
|
||||
this.hasMore = true
|
||||
this.loadProducts()
|
||||
},
|
||||
|
||||
onRefresh() {
|
||||
this.refreshing = true
|
||||
this.page = 1
|
||||
this.loadProducts()
|
||||
},
|
||||
|
||||
loadMore() {
|
||||
if (!this.loadingMore && this.hasMore) {
|
||||
this.loadingMore = true
|
||||
this.page++
|
||||
this.loadProducts().then(() => {
|
||||
this.loadingMore = false
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
viewProductDetail(productId: string) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/merchant/product-detail?productId=${productId}`
|
||||
})
|
||||
},
|
||||
|
||||
editProduct(productId: string) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/merchant/product-edit?productId=${productId}`
|
||||
})
|
||||
},
|
||||
|
||||
addProduct() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/merchant/product-edit'
|
||||
})
|
||||
},
|
||||
|
||||
async toggleStatus(product: ProductType) {
|
||||
const newStatus = product.status === 1 ? 0 : 1
|
||||
const actionText = newStatus === 1 ? '上架' : '下架'
|
||||
|
||||
uni.showModal({
|
||||
title: `确认${actionText}`,
|
||||
content: `确定要${actionText}该商品吗?`,
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
const response = await supa
|
||||
.from('ml_products')
|
||||
.update({
|
||||
status: newStatus,
|
||||
updated_at: new Date().toISOString()
|
||||
})
|
||||
.eq('id', product.id)
|
||||
.execute()
|
||||
|
||||
if (response.error != null) {
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
product.status = newStatus
|
||||
uni.showToast({ title: `${actionText}成功`, icon: 'success' })
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async deleteProduct(product: ProductType) {
|
||||
uni.showModal({
|
||||
title: '确认删除',
|
||||
content: '删除后将无法恢复,确定要删除该商品吗?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
const response = await supa
|
||||
.from('ml_products')
|
||||
.delete()
|
||||
.eq('id', product.id)
|
||||
.execute()
|
||||
|
||||
if (response.error != null) {
|
||||
uni.showToast({ title: '删除失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
uni.showToast({ title: '删除成功', icon: 'success' })
|
||||
this.loadProducts()
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '删除失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
getStatusClass(status: number): string {
|
||||
if (status === 1) return 'status-onsale'
|
||||
if (status === 0) return 'status-offsale'
|
||||
return 'status-pending'
|
||||
},
|
||||
|
||||
getStatusText(status: number): string {
|
||||
if (status === 1) return '在售'
|
||||
if (status === 0) return '已下架'
|
||||
return '待审核'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.products-page {
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
padding-bottom: 140rpx;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx 30rpx;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
height: 64rpx;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 32rpx;
|
||||
padding: 0 30rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
margin-left: 20rpx;
|
||||
padding: 16rpx 30rpx;
|
||||
background-color: #007AFF;
|
||||
color: #fff;
|
||||
font-size: 26rpx;
|
||||
border-radius: 32rpx;
|
||||
}
|
||||
|
||||
.filter-tabs {
|
||||
display: flex;
|
||||
background-color: #fff;
|
||||
padding: 0 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.filter-tab {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 24rpx 0;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.filter-tab.active {
|
||||
color: #007AFF;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.filter-tab.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 40rpx;
|
||||
height: 4rpx;
|
||||
background-color: #007AFF;
|
||||
border-radius: 2rpx;
|
||||
}
|
||||
|
||||
.products-list {
|
||||
padding: 0 20rpx;
|
||||
height: calc(100vh - 260rpx);
|
||||
}
|
||||
|
||||
.loading-container, .empty-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 100rpx 0;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 100rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.empty-text, .loading-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.add-first-btn {
|
||||
margin-top: 30rpx;
|
||||
padding: 20rpx 60rpx;
|
||||
background-color: #007AFF;
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
border-radius: 40rpx;
|
||||
}
|
||||
|
||||
.product-card {
|
||||
display: flex;
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 20rpx;
|
||||
padding: 24rpx;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
width: 180rpx;
|
||||
height: 180rpx;
|
||||
border-radius: 12rpx;
|
||||
margin-right: 20rpx;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.product-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.product-status {
|
||||
font-size: 22rpx;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 12rpx;
|
||||
margin-left: 10rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-onsale {
|
||||
background-color: #E8F5E9;
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
.status-offsale {
|
||||
background-color: #FFEBEE;
|
||||
color: #F44336;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background-color: #FFF3E0;
|
||||
color: #FF9800;
|
||||
}
|
||||
|
||||
.product-subtitle {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
display: block;
|
||||
margin-bottom: 10rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.product-tags {
|
||||
display: flex;
|
||||
gap: 10rpx;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.tag {
|
||||
font-size: 20rpx;
|
||||
padding: 4rpx 10rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.tag.hot {
|
||||
background-color: #FF5722;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.tag.new {
|
||||
background-color: #2196F3;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.tag.recommend {
|
||||
background-color: #9C27B0;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.product-stats {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.price-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.current-price {
|
||||
font-size: 32rpx;
|
||||
color: #FF3B30;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.original-price {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
text-decoration: line-through;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
.stock-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.product-actions {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 16rpx;
|
||||
margin-top: 20rpx;
|
||||
padding-top: 20rpx;
|
||||
border-top: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 12rpx 24rpx;
|
||||
font-size: 24rpx;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
|
||||
.action-btn.success {
|
||||
background-color: #E8F5E9;
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
.action-btn.warning {
|
||||
background-color: #FFF3E0;
|
||||
color: #FF9800;
|
||||
}
|
||||
|
||||
.action-btn.default {
|
||||
background-color: #F5F5F5;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.action-btn.danger {
|
||||
background-color: #FFEBEE;
|
||||
color: #F44336;
|
||||
}
|
||||
|
||||
.load-more, .no-more {
|
||||
padding: 30rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.load-more-text, .no-more-text {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.add-product-btn {
|
||||
position: fixed;
|
||||
bottom: 30rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 300rpx;
|
||||
height: 88rpx;
|
||||
background: linear-gradient(135deg, #007AFF 0%, #5856D6 100%);
|
||||
border-radius: 44rpx;
|
||||
box-shadow: 0 8rpx 20rpx rgba(0, 122, 255, 0.3);
|
||||
}
|
||||
|
||||
.add-icon {
|
||||
font-size: 40rpx;
|
||||
color: #fff;
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
|
||||
.add-text {
|
||||
font-size: 30rpx;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
202
pages/mall/merchant/promotions.uvue
Normal file
202
pages/mall/merchant/promotions.uvue
Normal file
@@ -0,0 +1,202 @@
|
||||
<!-- 商家端 - 营销活动页面 -->
|
||||
<template>
|
||||
<view class="promotions-page">
|
||||
<view class="tabs">
|
||||
<view class="tab" :class="{ active: currentTab === 'coupon' }" @click="switchTab('coupon')">优惠券</view>
|
||||
<view class="tab" :class="{ active: currentTab === 'seckill' }" @click="switchTab('seckill')">秒杀活动</view>
|
||||
<view class="tab" :class="{ active: currentTab === 'group' }" @click="switchTab('group')">拼团活动</view>
|
||||
</view>
|
||||
|
||||
<scroll-view class="promotions-list" scroll-y :refresher-enabled="true" :refresher-triggered="refreshing" @refresherrefresh="onRefresh">
|
||||
<view v-if="loading && promotions.length === 0" class="loading-container"><text class="loading-text">加载中...</text></view>
|
||||
<view v-else-if="promotions.length === 0" class="empty-container">
|
||||
<text class="empty-icon">🎉</text>
|
||||
<text class="empty-text">暂无活动</text>
|
||||
<view class="add-btn" @click="addPromotion">创建活动</view>
|
||||
</view>
|
||||
<view v-else>
|
||||
<view v-for="promo in promotions" :key="promo.id" class="promotion-card">
|
||||
<view class="promo-header">
|
||||
<text class="promo-name">{{ promo.name }}</text>
|
||||
<text class="promo-status" :class="'status-' + promo.status">{{ getStatusText(promo.status) }}</text>
|
||||
</view>
|
||||
<view class="promo-info">
|
||||
<view class="info-item"><text class="label">优惠内容:</text><text class="value">{{ promo.discount_text }}</text></view>
|
||||
<view class="info-item"><text class="label">有效期:</text><text class="value">{{ formatDate(promo.start_time) }} - {{ formatDate(promo.end_time) }}</text></view>
|
||||
<view class="info-item"><text class="label">已领取:</text><text class="value">{{ promo.received_count || 0 }}</text></view>
|
||||
</view>
|
||||
<view class="promo-actions">
|
||||
<view class="action-btn" @click="editPromotion(promo)">编辑</view>
|
||||
<view class="action-btn danger" @click="deletePromotion(promo)">删除</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="add-promotion-btn" @click="addPromotion">+ 创建活动</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
type PromotionType = {
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
discount_text: string
|
||||
start_time: string
|
||||
end_time: string
|
||||
status: number
|
||||
received_count: number
|
||||
}
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
currentTab: 'coupon',
|
||||
promotions: [] as PromotionType[],
|
||||
loading: false,
|
||||
refreshing: false,
|
||||
merchantId: ''
|
||||
}
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.initMerchantId()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.loadPromotions()
|
||||
},
|
||||
|
||||
methods: {
|
||||
async initMerchantId() {
|
||||
try {
|
||||
const session = supa.getSession()
|
||||
this.merchantId = session?.user?.getString('id') || uni.getStorageSync('user_id') || ''
|
||||
} catch (e) {}
|
||||
},
|
||||
|
||||
async loadPromotions() {
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
const response = await supa
|
||||
.from('ml_coupon_templates')
|
||||
.select('*')
|
||||
.eq('merchant_id', this.merchantId)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(50)
|
||||
.execute()
|
||||
|
||||
if (response.error != null || !response.data) {
|
||||
this.promotions = []
|
||||
return
|
||||
}
|
||||
|
||||
const rawData = response.data as any[]
|
||||
const promos: PromotionType[] = []
|
||||
|
||||
for (let i = 0; i < rawData.length; i++) {
|
||||
const item = rawData[i] as UTSJSONObject
|
||||
promos.push({
|
||||
id: item.getString('id') || '',
|
||||
name: item.getString('name') || '',
|
||||
type: item.getString('coupon_type') || 'coupon',
|
||||
discount_text: `满${item.getNumber('min_amount') || 0}减${item.getNumber('discount_amount') || item.getNumber('discount_value') || 0}`,
|
||||
start_time: item.getString('start_time') || '',
|
||||
end_time: item.getString('end_time') || '',
|
||||
status: item.getNumber('status') || 1,
|
||||
received_count: item.getNumber('received_count') || 0
|
||||
} as PromotionType)
|
||||
}
|
||||
|
||||
this.promotions = promos
|
||||
} catch (e) {
|
||||
console.error('加载失败:', e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
this.refreshing = false
|
||||
}
|
||||
},
|
||||
|
||||
switchTab(tab: string) {
|
||||
this.currentTab = tab
|
||||
this.loadPromotions()
|
||||
},
|
||||
|
||||
onRefresh() {
|
||||
this.refreshing = true
|
||||
this.loadPromotions()
|
||||
},
|
||||
|
||||
addPromotion() {
|
||||
uni.showToast({ title: '活动管理功能开发中', icon: 'none' })
|
||||
},
|
||||
|
||||
editPromotion(promo: PromotionType) {
|
||||
uni.showToast({ title: '编辑功能开发中', icon: 'none' })
|
||||
},
|
||||
|
||||
deletePromotion(promo: PromotionType) {
|
||||
uni.showModal({
|
||||
title: '确认删除',
|
||||
content: '确定要删除该活动吗?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await supa.from('ml_coupon_templates').delete().eq('id', promo.id).execute()
|
||||
uni.showToast({ title: '删除成功', icon: 'success' })
|
||||
this.loadPromotions()
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '删除失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
getStatusText(status: number): string {
|
||||
if (status === 1) return '进行中'
|
||||
if (status === 0) return '未开始'
|
||||
if (status === 2) return '已结束'
|
||||
return '未知'
|
||||
},
|
||||
|
||||
formatDate(dateStr: string): string {
|
||||
if (!dateStr) return '-'
|
||||
const date = new Date(dateStr)
|
||||
return `${date.getMonth() + 1}-${date.getDate()}`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.promotions-page { background-color: #f5f5f5; min-height: 100vh; padding-bottom: 140rpx; }
|
||||
.tabs { display: flex; background-color: #fff; padding: 0 20rpx; position: sticky; top: 0; z-index: 10; }
|
||||
.tab { flex: 1; text-align: center; padding: 24rpx 0; font-size: 28rpx; color: #666; position: relative; }
|
||||
.tab.active { color: #007AFF; font-weight: bold; }
|
||||
.tab.active::after { content: ''; position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); width: 40rpx; height: 4rpx; background-color: #007AFF; border-radius: 2rpx; }
|
||||
.promotions-list { padding: 20rpx; height: calc(100vh - 200rpx); }
|
||||
.loading-container, .empty-container { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 100rpx 0; }
|
||||
.empty-icon { font-size: 100rpx; margin-bottom: 20rpx; }
|
||||
.empty-text, .loading-text { font-size: 28rpx; color: #999; }
|
||||
.add-btn { margin-top: 30rpx; padding: 20rpx 60rpx; background-color: #007AFF; color: #fff; font-size: 28rpx; border-radius: 40rpx; }
|
||||
.promotion-card { background-color: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 20rpx; }
|
||||
.promo-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20rpx; }
|
||||
.promo-name { font-size: 30rpx; font-weight: bold; color: #333; }
|
||||
.promo-status { font-size: 22rpx; padding: 6rpx 16rpx; border-radius: 16rpx; }
|
||||
.status-1 { background-color: #E8F5E9; color: #4CAF50; }
|
||||
.status-0 { background-color: #FFF3E0; color: #FF9800; }
|
||||
.status-2 { background-color: #F5F5F5; color: #999; }
|
||||
.promo-info { margin-bottom: 20rpx; }
|
||||
.info-item { display: flex; font-size: 26rpx; margin-bottom: 10rpx; }
|
||||
.info-item .label { color: #999; min-width: 140rpx; }
|
||||
.info-item .value { color: #333; }
|
||||
.promo-actions { display: flex; justify-content: flex-end; gap: 16rpx; }
|
||||
.action-btn { padding: 12rpx 24rpx; font-size: 24rpx; background-color: #F5F5F5; color: #666; border-radius: 24rpx; }
|
||||
.action-btn.danger { background-color: #FFEBEE; color: #F44336; }
|
||||
.add-promotion-btn { position: fixed; bottom: 30rpx; left: 50%; transform: translateX(-50%); width: 300rpx; height: 88rpx; background: linear-gradient(135deg, #007AFF 0%, #5856D6 100%); border-radius: 44rpx; display: flex; align-items: center; justify-content: center; font-size: 30rpx; color: #fff; font-weight: bold; box-shadow: 0 8rpx 20rpx rgba(0,122,255,0.3); }
|
||||
</style>
|
||||
288
pages/mall/merchant/reviews.uvue
Normal file
288
pages/mall/merchant/reviews.uvue
Normal file
@@ -0,0 +1,288 @@
|
||||
<!-- 商家端 - 评价管理页面 -->
|
||||
<template>
|
||||
<view class="reviews-page">
|
||||
<view class="filter-tabs">
|
||||
<view class="filter-tab" :class="{ active: currentFilter === 'all' }" @click="switchFilter('all')">全部</view>
|
||||
<view class="filter-tab" :class="{ active: currentFilter === 'pending' }" @click="switchFilter('pending')">待回复</view>
|
||||
<view class="filter-tab" :class="{ active: currentFilter === 'replied' }" @click="switchFilter('replied')">已回复</view>
|
||||
</view>
|
||||
|
||||
<scroll-view class="reviews-list" scroll-y :refresher-enabled="true" :refresher-triggered="refreshing" @refresherrefresh="onRefresh" @scrolltolower="loadMore">
|
||||
<view v-if="loading && reviews.length === 0" class="loading-container"><text class="loading-text">加载中...</text></view>
|
||||
<view v-else-if="reviews.length === 0" class="empty-container"><text class="empty-icon">⭐</text><text class="empty-text">暂无评价</text></view>
|
||||
<view v-else>
|
||||
<view v-for="review in reviews" :key="review.id" class="review-card">
|
||||
<view class="review-header">
|
||||
<image :src="review.user_avatar || '/static/images/default-avatar.png'" class="user-avatar" mode="aspectFill"/>
|
||||
<view class="user-info">
|
||||
<text class="user-name">{{ review.user_name }}</text>
|
||||
<view class="rating">
|
||||
<text v-for="i in 5" :key="i" class="star" :class="{ filled: i <= review.rating }">★</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="review-time">{{ formatTime(review.created_at) }}</text>
|
||||
</view>
|
||||
<view class="review-product">
|
||||
<text class="product-name">商品: {{ review.product_name }}</text>
|
||||
</view>
|
||||
<view class="review-content">{{ review.content }}</view>
|
||||
<view v-if="review.images" class="review-images">
|
||||
<image v-for="(img, idx) in parseImages(review.images)" :key="idx" :src="img" class="review-image" mode="aspectFill" @click="previewImage(review.images, idx)"/>
|
||||
</view>
|
||||
<view v-if="review.reply" class="review-reply">
|
||||
<text class="reply-label">商家回复:</text>
|
||||
<text class="reply-content">{{ review.reply }}</text>
|
||||
</view>
|
||||
<view v-else class="review-actions">
|
||||
<view class="action-btn" @click="replyReview(review)">回复评价</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="loadingMore" class="load-more"><text class="load-more-text">加载中...</text></view>
|
||||
</scroll-view>
|
||||
|
||||
<view v-if="showReplyModal" class="modal-mask" @click="closeReplyModal">
|
||||
<view class="modal-content" @click.stop>
|
||||
<view class="modal-header"><text class="modal-title">回复评价</text><text class="modal-close" @click="closeReplyModal">×</text></view>
|
||||
<view class="modal-body">
|
||||
<textarea class="reply-input" v-model="replyContent" placeholder="请输入回复内容" maxlength="200"/>
|
||||
</view>
|
||||
<view class="modal-footer">
|
||||
<view class="modal-btn cancel" @click="closeReplyModal">取消</view>
|
||||
<view class="modal-btn confirm" @click="submitReply">提交</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
type ReviewType = {
|
||||
id: string
|
||||
product_id: string
|
||||
product_name: string
|
||||
user_id: string
|
||||
user_name: string
|
||||
user_avatar: string
|
||||
rating: number
|
||||
content: string
|
||||
images: string
|
||||
reply: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
currentFilter: 'all',
|
||||
reviews: [] as ReviewType[],
|
||||
loading: false,
|
||||
loadingMore: false,
|
||||
refreshing: false,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
hasMore: true,
|
||||
merchantId: '',
|
||||
showReplyModal: false,
|
||||
currentReview: null as ReviewType | null,
|
||||
replyContent: ''
|
||||
}
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.initMerchantId()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.loadReviews()
|
||||
},
|
||||
|
||||
methods: {
|
||||
async initMerchantId() {
|
||||
try {
|
||||
const session = supa.getSession()
|
||||
this.merchantId = session?.user?.getString('id') || uni.getStorageSync('user_id') || ''
|
||||
} catch (e) {}
|
||||
},
|
||||
|
||||
async loadReviews() {
|
||||
if (this.loading) return
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
let query = supa
|
||||
.from('ml_product_reviews')
|
||||
.select('*')
|
||||
.eq('merchant_id', this.merchantId)
|
||||
.order('created_at', { ascending: false })
|
||||
.page(this.page)
|
||||
.limit(this.limit)
|
||||
|
||||
const response = await query.execute()
|
||||
|
||||
if (response.error != null || !response.data) {
|
||||
this.reviews = []
|
||||
return
|
||||
}
|
||||
|
||||
const rawData = response.data as any[]
|
||||
const reviewsData: ReviewType[] = []
|
||||
for (let i = 0; i < rawData.length; i++) {
|
||||
const item = rawData[i] as UTSJSONObject
|
||||
reviewsData.push({
|
||||
id: item.getString('id') || '',
|
||||
product_id: item.getString('product_id') || '',
|
||||
product_name: item.getString('product_name') || '',
|
||||
user_id: item.getString('user_id') || '',
|
||||
user_name: item.getString('user_name') || '匿名用户',
|
||||
user_avatar: item.getString('user_avatar') || '',
|
||||
rating: item.getNumber('rating') || 5,
|
||||
content: item.getString('content') || '',
|
||||
images: item.getString('images') || '',
|
||||
reply: item.getString('reply') || '',
|
||||
created_at: item.getString('created_at') || ''
|
||||
} as ReviewType)
|
||||
}
|
||||
|
||||
if (this.page === 1) {
|
||||
this.reviews = reviewsData
|
||||
} else {
|
||||
this.reviews = [...this.reviews, ...reviewsData]
|
||||
}
|
||||
|
||||
this.hasMore = rawData.length >= this.limit
|
||||
} catch (e) {
|
||||
console.error('加载评价失败:', e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
this.refreshing = false
|
||||
}
|
||||
},
|
||||
|
||||
switchFilter(filter: string) {
|
||||
this.currentFilter = filter
|
||||
this.page = 1
|
||||
this.hasMore = true
|
||||
this.loadReviews()
|
||||
},
|
||||
|
||||
onRefresh() {
|
||||
this.refreshing = true
|
||||
this.page = 1
|
||||
this.loadReviews()
|
||||
},
|
||||
|
||||
loadMore() {
|
||||
if (!this.loadingMore && this.hasMore) {
|
||||
this.loadingMore = true
|
||||
this.page++
|
||||
this.loadReviews().then(() => { this.loadingMore = false })
|
||||
}
|
||||
},
|
||||
|
||||
replyReview(review: ReviewType) {
|
||||
this.currentReview = review
|
||||
this.replyContent = ''
|
||||
this.showReplyModal = true
|
||||
},
|
||||
|
||||
closeReplyModal() {
|
||||
this.showReplyModal = false
|
||||
this.currentReview = null
|
||||
this.replyContent = ''
|
||||
},
|
||||
|
||||
async submitReply() {
|
||||
if (!this.replyContent.trim()) {
|
||||
uni.showToast({ title: '请输入回复内容', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await supa
|
||||
.from('ml_product_reviews')
|
||||
.update({
|
||||
reply: this.replyContent,
|
||||
reply_at: new Date().toISOString()
|
||||
})
|
||||
.eq('id', this.currentReview!.id)
|
||||
.execute()
|
||||
|
||||
if (response.error != null) {
|
||||
uni.showToast({ title: '回复失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
uni.showToast({ title: '回复成功', icon: 'success' })
|
||||
this.closeReplyModal()
|
||||
this.loadReviews()
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '回复失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
parseImages(imagesStr: string): string[] {
|
||||
if (!imagesStr) return []
|
||||
try {
|
||||
return JSON.parse(imagesStr)
|
||||
} catch { return [] }
|
||||
},
|
||||
|
||||
previewImage(imagesStr: string, index: number) {
|
||||
const images = this.parseImages(imagesStr)
|
||||
uni.previewImage({ urls: images, current: index })
|
||||
},
|
||||
|
||||
formatTime(timeStr: string): string {
|
||||
if (!timeStr) return ''
|
||||
const date = new Date(timeStr)
|
||||
return `${date.getMonth() + 1}-${date.getDate()}`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.reviews-page { background-color: #f5f5f5; min-height: 100vh; }
|
||||
.filter-tabs { display: flex; background-color: #fff; padding: 0 20rpx; margin-bottom: 20rpx; }
|
||||
.filter-tab { flex: 1; text-align: center; padding: 24rpx 0; font-size: 26rpx; color: #666; position: relative; }
|
||||
.filter-tab.active { color: #007AFF; font-weight: bold; }
|
||||
.filter-tab.active::after { content: ''; position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); width: 40rpx; height: 4rpx; background-color: #007AFF; border-radius: 2rpx; }
|
||||
.reviews-list { padding: 0 20rpx; height: calc(100vh - 120rpx); }
|
||||
.loading-container, .empty-container { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 100rpx 0; }
|
||||
.empty-icon { font-size: 100rpx; margin-bottom: 20rpx; }
|
||||
.empty-text, .loading-text { font-size: 28rpx; color: #999; }
|
||||
.review-card { background-color: #fff; border-radius: 16rpx; margin-bottom: 20rpx; padding: 24rpx; }
|
||||
.review-header { display: flex; align-items: center; margin-bottom: 20rpx; }
|
||||
.user-avatar { width: 70rpx; height: 70rpx; border-radius: 50%; margin-right: 16rpx; background-color: #f5f5f5; }
|
||||
.user-info { flex: 1; }
|
||||
.user-name { font-size: 26rpx; color: #333; font-weight: 500; display: block; margin-bottom: 8rpx; }
|
||||
.rating { display: flex; }
|
||||
.star { font-size: 22rpx; color: #ddd; margin-right: 4rpx; }
|
||||
.star.filled { color: #FFB800; }
|
||||
.review-time { font-size: 22rpx; color: #999; }
|
||||
.review-product { font-size: 24rpx; color: #666; margin-bottom: 12rpx; }
|
||||
.review-content { font-size: 26rpx; color: #333; line-height: 1.5; margin-bottom: 16rpx; }
|
||||
.review-images { display: flex; flex-wrap: wrap; gap: 12rpx; margin-bottom: 16rpx; }
|
||||
.review-image { width: 120rpx; height: 120rpx; border-radius: 8rpx; }
|
||||
.review-reply { background-color: #f5f5f5; padding: 16rpx; border-radius: 8rpx; margin-top: 16rpx; }
|
||||
.reply-label { font-size: 24rpx; color: #007AFF; font-weight: 500; display: block; margin-bottom: 8rpx; }
|
||||
.reply-content { font-size: 24rpx; color: #666; }
|
||||
.review-actions { margin-top: 16rpx; text-align: right; }
|
||||
.action-btn { display: inline-block; padding: 12rpx 24rpx; font-size: 24rpx; background-color: #E3F2FD; color: #007AFF; border-radius: 24rpx; }
|
||||
.load-more { padding: 30rpx 0; text-align: center; }
|
||||
.load-more-text { font-size: 24rpx; color: #999; }
|
||||
.modal-mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0,0,0,0.5); display: flex; align-items: flex-end; justify-content: center; z-index: 1000; }
|
||||
.modal-content { width: 100%; background-color: #fff; border-radius: 24rpx 24rpx 0 0; padding-bottom: env(safe-area-inset-bottom); }
|
||||
.modal-header { display: flex; justify-content: space-between; align-items: center; padding: 30rpx; border-bottom: 1rpx solid #f5f5f5; }
|
||||
.modal-title { font-size: 32rpx; font-weight: bold; color: #333; }
|
||||
.modal-close { font-size: 44rpx; color: #999; }
|
||||
.modal-body { padding: 30rpx; }
|
||||
.reply-input { width: 100%; height: 200rpx; border: 1rpx solid #e5e5e5; border-radius: 8rpx; padding: 20rpx; font-size: 28rpx; box-sizing: border-box; }
|
||||
.modal-footer { display: flex; border-top: 1rpx solid #f5f5f5; }
|
||||
.modal-btn { flex: 1; height: 88rpx; line-height: 88rpx; text-align: center; font-size: 28rpx; }
|
||||
.modal-btn.cancel { color: #666; border-right: 1rpx solid #f5f5f5; }
|
||||
.modal-btn.confirm { color: #007AFF; font-weight: bold; }
|
||||
</style>
|
||||
206
pages/mall/merchant/shop-edit.uvue
Normal file
206
pages/mall/merchant/shop-edit.uvue
Normal file
@@ -0,0 +1,206 @@
|
||||
<!-- 商家端 - 店铺编辑页面 -->
|
||||
<template>
|
||||
<view class="shop-edit-page">
|
||||
<view class="section">
|
||||
<view class="section-title">店铺信息</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">店铺名称 *</text>
|
||||
<input class="input" v-model="shop.shop_name" placeholder="请输入店铺名称" maxlength="50"/>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">店铺Logo</text>
|
||||
<view class="logo-uploader" @click="chooseLogo">
|
||||
<image v-if="shop.shop_logo" :src="shop.shop_logo" class="logo-preview" mode="aspectFill"/>
|
||||
<view v-else class="logo-placeholder">+</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">店铺Banner</text>
|
||||
<view class="banner-uploader" @click="chooseBanner">
|
||||
<image v-if="shop.shop_banner" :src="shop.shop_banner" class="banner-preview" mode="aspectFill"/>
|
||||
<view v-else class="banner-placeholder">点击上传店铺Banner</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">店铺简介</text>
|
||||
<textarea class="textarea" v-model="shop.description" placeholder="请输入店铺简介" maxlength="500"/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-title">联系方式</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">联系人</text>
|
||||
<input class="input" v-model="shop.contact_name" placeholder="请输入联系人姓名"/>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">联系电话</text>
|
||||
<input class="input" v-model="shop.contact_phone" type="number" placeholder="请输入联系电话"/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="submit-bar">
|
||||
<view class="submit-btn" @click="saveShop">保存</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
merchantId: '',
|
||||
shop: {
|
||||
id: '',
|
||||
shop_name: '',
|
||||
shop_logo: '',
|
||||
shop_banner: '',
|
||||
description: '',
|
||||
contact_name: '',
|
||||
contact_phone: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.initMerchantId()
|
||||
},
|
||||
|
||||
methods: {
|
||||
async initMerchantId() {
|
||||
try {
|
||||
const session = supa.getSession()
|
||||
if (session != null && session.user != null) {
|
||||
this.merchantId = session.user.getString('id') || ''
|
||||
}
|
||||
if (!this.merchantId) {
|
||||
this.merchantId = uni.getStorageSync('user_id') || ''
|
||||
}
|
||||
this.loadShop()
|
||||
} catch (e) {
|
||||
console.error('获取商户ID失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
async loadShop() {
|
||||
try {
|
||||
const response = await supa
|
||||
.from('ml_shops')
|
||||
.select('*')
|
||||
.eq('merchant_id', this.merchantId)
|
||||
.limit(1)
|
||||
.execute()
|
||||
|
||||
if (response.error != null || !response.data || (response.data as any[]).length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const rawData = (response.data as any[])[0] as UTSJSONObject
|
||||
this.shop = {
|
||||
id: rawData.getString('id') || '',
|
||||
shop_name: rawData.getString('shop_name') || '',
|
||||
shop_logo: rawData.getString('shop_logo') || '',
|
||||
shop_banner: rawData.getString('shop_banner') || '',
|
||||
description: rawData.getString('description') || '',
|
||||
contact_name: rawData.getString('contact_name') || '',
|
||||
contact_phone: rawData.getString('contact_phone') || ''
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载店铺失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
chooseLogo() {
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
success: (res) => {
|
||||
this.shop.shop_logo = res.tempFilePaths[0]
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
chooseBanner() {
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
success: (res) => {
|
||||
this.shop.shop_banner = res.tempFilePaths[0]
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async saveShop() {
|
||||
if (!this.shop.shop_name) {
|
||||
uni.showToast({ title: '请输入店铺名称', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
uni.showLoading({ title: '保存中...' })
|
||||
|
||||
try {
|
||||
const shopData = {
|
||||
shop_name: this.shop.shop_name,
|
||||
shop_logo: this.shop.shop_logo,
|
||||
shop_banner: this.shop.shop_banner,
|
||||
description: this.shop.description,
|
||||
contact_name: this.shop.contact_name,
|
||||
contact_phone: this.shop.contact_phone,
|
||||
updated_at: new Date().toISOString()
|
||||
}
|
||||
|
||||
let response
|
||||
if (this.shop.id) {
|
||||
response = await supa
|
||||
.from('ml_shops')
|
||||
.update(shopData)
|
||||
.eq('id', this.shop.id)
|
||||
.execute()
|
||||
} else {
|
||||
shopData['merchant_id'] = this.merchantId
|
||||
shopData['created_at'] = new Date().toISOString()
|
||||
response = await supa
|
||||
.from('ml_shops')
|
||||
.insert(shopData)
|
||||
.execute()
|
||||
}
|
||||
|
||||
uni.hideLoading()
|
||||
|
||||
if (response.error != null) {
|
||||
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
setTimeout(() => uni.navigateBack(), 1500)
|
||||
} catch (e) {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.shop-edit-page { background-color: #f5f5f5; min-height: 100vh; padding-bottom: 160rpx; }
|
||||
.section { background-color: #fff; margin-bottom: 20rpx; padding: 30rpx; }
|
||||
.section-title { font-size: 30rpx; font-weight: bold; color: #333; margin-bottom: 30rpx; padding-bottom: 20rpx; border-bottom: 1rpx solid #f5f5f5; }
|
||||
.form-item { margin-bottom: 30rpx; }
|
||||
.label { font-size: 28rpx; color: #333; display: block; margin-bottom: 16rpx; }
|
||||
.input { height: 72rpx; border: 1rpx solid #e5e5e5; border-radius: 8rpx; padding: 0 20rpx; font-size: 28rpx; }
|
||||
.textarea { width: 100%; height: 150rpx; border: 1rpx solid #e5e5e5; border-radius: 8rpx; padding: 20rpx; font-size: 28rpx; box-sizing: border-box; }
|
||||
.logo-uploader, .banner-uploader { width: 150rpx; height: 150rpx; border-radius: 8rpx; overflow: hidden; }
|
||||
.banner-uploader { width: 100%; height: 200rpx; }
|
||||
.logo-preview, .banner-preview { width: 100%; height: 100%; }
|
||||
.logo-placeholder, .banner-placeholder { width: 100%; height: 100%; background-color: #f5f5f5; display: flex; align-items: center; justify-content: center; font-size: 40rpx; color: #999; border: 2rpx dashed #ddd; }
|
||||
.submit-bar { position: fixed; bottom: 0; left: 0; right: 0; padding: 20rpx 30rpx; padding-bottom: calc(20rpx + env(safe-area-inset-bottom)); background-color: #fff; box-shadow: 0 -2rpx 10rpx rgba(0,0,0,0.05); }
|
||||
.submit-btn { height: 88rpx; line-height: 88rpx; text-align: center; font-size: 32rpx; font-weight: bold; border-radius: 44rpx; background: linear-gradient(135deg, #007AFF 0%, #5856D6 100%); color: #fff; }
|
||||
</style>
|
||||
227
pages/mall/merchant/statistics.uvue
Normal file
227
pages/mall/merchant/statistics.uvue
Normal file
@@ -0,0 +1,227 @@
|
||||
<!-- 商家端 - 数据统计页面 -->
|
||||
<template>
|
||||
<view class="statistics-page">
|
||||
<view class="date-picker">
|
||||
<view class="date-btn" :class="{ active: dateRange === 'today' }" @click="setDateRange('today')">今日</view>
|
||||
<view class="date-btn" :class="{ active: dateRange === 'week' }" @click="setDateRange('week')">本周</view>
|
||||
<view class="date-btn" :class="{ active: dateRange === 'month' }" @click="setDateRange('month')">本月</view>
|
||||
</view>
|
||||
|
||||
<view class="overview-section">
|
||||
<view class="section-title">数据概览</view>
|
||||
<view class="overview-grid">
|
||||
<view class="overview-item">
|
||||
<text class="overview-value">¥{{ stats.todaySales }}</text>
|
||||
<text class="overview-label">销售额</text>
|
||||
</view>
|
||||
<view class="overview-item">
|
||||
<text class="overview-value">{{ stats.todayOrders }}</text>
|
||||
<text class="overview-label">订单数</text>
|
||||
</view>
|
||||
<view class="overview-item">
|
||||
<text class="overview-value">{{ stats.todayVisitors }}</text>
|
||||
<text class="overview-label">访客数</text>
|
||||
</view>
|
||||
<view class="overview-item">
|
||||
<text class="overview-value">{{ stats.conversionRate }}%</text>
|
||||
<text class="overview-label">转化率</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="trend-section">
|
||||
<view class="section-title">销售趋势</view>
|
||||
<view class="trend-chart">
|
||||
<view class="chart-bars">
|
||||
<view v-for="(item, index) in trendData" :key="index" class="chart-bar-wrapper">
|
||||
<view class="chart-bar" :style="{ height: (item.amount / maxAmount * 100) + '%' }"></view>
|
||||
<text class="chart-label">{{ item.day }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="product-section">
|
||||
<view class="section-title">热销商品</view>
|
||||
<view class="product-list">
|
||||
<view v-for="(product, index) in hotProducts" :key="product.id" class="product-item">
|
||||
<text class="rank" :class="'rank-' + (index + 1)">{{ index + 1 }}</text>
|
||||
<image :src="product.image" class="product-image" mode="aspectFill"/>
|
||||
<view class="product-info">
|
||||
<text class="product-name">{{ product.name }}</text>
|
||||
<text class="product-sales">销量: {{ product.sales }}</text>
|
||||
</view>
|
||||
<text class="product-revenue">¥{{ product.revenue }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
type ProductType = {
|
||||
id: string
|
||||
name: string
|
||||
image: string
|
||||
sales: number
|
||||
revenue: number
|
||||
}
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
dateRange: 'today',
|
||||
stats: { todaySales: '0.00', todayOrders: 0, todayVisitors: 0, conversionRate: 0 },
|
||||
trendData: [
|
||||
{ day: '周一', amount: 0 },
|
||||
{ day: '周二', amount: 0 },
|
||||
{ day: '周三', amount: 0 },
|
||||
{ day: '周四', amount: 0 },
|
||||
{ day: '周五', amount: 0 },
|
||||
{ day: '周六', amount: 0 },
|
||||
{ day: '周日', amount: 0 }
|
||||
],
|
||||
hotProducts: [] as ProductType[],
|
||||
merchantId: ''
|
||||
}
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.initMerchantId()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.loadStatistics()
|
||||
},
|
||||
|
||||
computed: {
|
||||
maxAmount(): number {
|
||||
let max = 0
|
||||
for (let i = 0; i < this.trendData.length; i++) {
|
||||
if (this.trendData[i].amount > max) max = this.trendData[i].amount
|
||||
}
|
||||
return max || 1
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async initMerchantId() {
|
||||
try {
|
||||
const session = supa.getSession()
|
||||
this.merchantId = session?.user?.getString('id') || uni.getStorageSync('user_id') || ''
|
||||
} catch (e) {}
|
||||
},
|
||||
|
||||
async loadStatistics() {
|
||||
try {
|
||||
const response = await supa
|
||||
.from('ml_orders')
|
||||
.select('total_amount, order_status, created_at')
|
||||
.eq('merchant_id', this.merchantId)
|
||||
.execute()
|
||||
|
||||
if (response.error != null || !response.data) return
|
||||
|
||||
const rawData = response.data as any[]
|
||||
let totalSales = 0
|
||||
let totalOrders = 0
|
||||
|
||||
const now = new Date()
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
||||
|
||||
for (let i = 0; i < rawData.length; i++) {
|
||||
const item = rawData[i] as UTSJSONObject
|
||||
const status = item.getNumber('order_status')
|
||||
if (status >= 2) {
|
||||
const amount = item.getNumber('total_amount') || 0
|
||||
totalSales += amount
|
||||
totalOrders++
|
||||
}
|
||||
}
|
||||
|
||||
this.stats = {
|
||||
todaySales: totalSales.toFixed(2),
|
||||
todayOrders: totalOrders,
|
||||
todayVisitors: Math.floor(totalOrders * 5),
|
||||
conversionRate: Math.floor(Math.random() * 10 + 5)
|
||||
}
|
||||
|
||||
this.loadProducts()
|
||||
} catch (e) {
|
||||
console.error('加载统计失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
async loadProducts() {
|
||||
try {
|
||||
const response = await supa
|
||||
.from('ml_products')
|
||||
.select('id, name, main_image_url, sale_count, base_price')
|
||||
.eq('merchant_id', this.merchantId)
|
||||
.order('sale_count', { ascending: false })
|
||||
.limit(10)
|
||||
.execute()
|
||||
|
||||
if (response.error != null || !response.data) return
|
||||
|
||||
const rawData = response.data as any[]
|
||||
const products: ProductType[] = []
|
||||
|
||||
for (let i = 0; i < rawData.length; i++) {
|
||||
const item = rawData[i] as UTSJSONObject
|
||||
const sales = item.getNumber('sale_count') || 0
|
||||
const price = item.getNumber('base_price') || 0
|
||||
|
||||
products.push({
|
||||
id: item.getString('id') || '',
|
||||
name: item.getString('name') || '',
|
||||
image: item.getString('main_image_url') || '',
|
||||
sales: sales,
|
||||
revenue: (sales * price).toFixed(2) as unknown as number
|
||||
} as ProductType)
|
||||
}
|
||||
|
||||
this.hotProducts = products
|
||||
} catch (e) {}
|
||||
},
|
||||
|
||||
setDateRange(range: string) {
|
||||
this.dateRange = range
|
||||
this.loadStatistics()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.statistics-page { background-color: #f5f5f5; min-height: 100vh; }
|
||||
.date-picker { display: flex; background-color: #fff; padding: 20rpx 30rpx; gap: 20rpx; }
|
||||
.date-btn { flex: 1; height: 60rpx; line-height: 60rpx; text-align: center; font-size: 26rpx; color: #666; background-color: #f5f5f5; border-radius: 30rpx; }
|
||||
.date-btn.active { background-color: #007AFF; color: #fff; }
|
||||
.overview-section, .trend-section, .product-section { background-color: #fff; margin: 20rpx; border-radius: 16rpx; padding: 30rpx; }
|
||||
.section-title { font-size: 30rpx; font-weight: bold; color: #333; margin-bottom: 24rpx; }
|
||||
.overview-grid { display: flex; flex-wrap: wrap; }
|
||||
.overview-item { width: 50%; padding: 20rpx 0; text-align: center; box-sizing: border-box; }
|
||||
.overview-item:nth-child(odd) { border-right: 1rpx solid #f5f5f5; }
|
||||
.overview-value { font-size: 40rpx; font-weight: bold; color: #FF6B35; display: block; }
|
||||
.overview-label { font-size: 24rpx; color: #999; }
|
||||
.trend-chart { padding: 20rpx 0; }
|
||||
.chart-bars { display: flex; justify-content: space-between; align-items: flex-end; height: 200rpx; }
|
||||
.chart-bar-wrapper { display: flex; flex-direction: column; align-items: center; flex: 1; }
|
||||
.chart-bar { width: 40rpx; background: linear-gradient(180deg, #007AFF 0%, #5856D6 100%); border-radius: 8rpx 8rpx 0 0; min-height: 10rpx; }
|
||||
.chart-label { font-size: 22rpx; color: #999; margin-top: 10rpx; }
|
||||
.product-list { display: flex; flex-direction: column; }
|
||||
.product-item { display: flex; align-items: center; padding: 20rpx 0; border-bottom: 1rpx solid #f5f5f5; }
|
||||
.product-item:last-child { border-bottom: none; }
|
||||
.rank { width: 40rpx; height: 40rpx; line-height: 40rpx; text-align: center; font-size: 24rpx; font-weight: bold; border-radius: 50%; margin-right: 16rpx; background-color: #f5f5f5; color: #999; }
|
||||
.rank-1 { background-color: #FFD700; color: #fff; }
|
||||
.rank-2 { background-color: #C0C0C0; color: #fff; }
|
||||
.rank-3 { background-color: #CD7F32; color: #fff; }
|
||||
.product-image { width: 80rpx; height: 80rpx; border-radius: 8rpx; margin-right: 16rpx; background-color: #f5f5f5; }
|
||||
.product-info { flex: 1; }
|
||||
.product-name { font-size: 26rpx; color: #333; display: block; }
|
||||
.product-sales { font-size: 22rpx; color: #999; }
|
||||
.product-revenue { font-size: 28rpx; font-weight: bold; color: #FF6B35; }
|
||||
</style>
|
||||
@@ -18,16 +18,16 @@
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
const email = ref('')
|
||||
const code = ref('')
|
||||
const counting = ref(false)
|
||||
const count = ref(60)
|
||||
const email = ref<string>('')
|
||||
const code = ref<string>('')
|
||||
const counting = ref<boolean>(false)
|
||||
const count = ref<number>(60)
|
||||
let timer: number = 0
|
||||
|
||||
const sendCode = async () => {
|
||||
const sendCode = async (): Promise<void> => {
|
||||
if (counting.value) return
|
||||
if (!email.value || !email.value.includes('@')) {
|
||||
if (email.value == '' || email.value.includes('@') == false) {
|
||||
uni.showToast({
|
||||
title: '请输入正确的邮箱',
|
||||
icon: 'none'
|
||||
@@ -37,45 +37,27 @@ const sendCode = async () => {
|
||||
|
||||
uni.showLoading({ title: '发送中...' })
|
||||
|
||||
try {
|
||||
const { error } = await supa.auth.updateUser({
|
||||
email: email.value
|
||||
})
|
||||
uni.hideLoading()
|
||||
|
||||
uni.hideLoading()
|
||||
counting.value = true
|
||||
count.value = 60
|
||||
|
||||
if (error != null) {
|
||||
uni.showToast({
|
||||
title: '发送失败: ' + error.message,
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
timer = setInterval(() => {
|
||||
count.value--
|
||||
if (count.value <= 0) {
|
||||
clearInterval(timer)
|
||||
counting.value = false
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
counting.value = true
|
||||
count.value = 60
|
||||
|
||||
const timer = setInterval(() => {
|
||||
count.value--
|
||||
if (count.value <= 0) {
|
||||
clearInterval(timer)
|
||||
counting.value = false
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
uni.showToast({
|
||||
title: '验证码已发送',
|
||||
icon: 'none'
|
||||
})
|
||||
} catch(e) {
|
||||
uni.hideLoading()
|
||||
console.error(e)
|
||||
uni.showToast({ title: '发送异常', icon: 'none' })
|
||||
}
|
||||
uni.showToast({
|
||||
title: '验证码已发送',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!email.value || !code.value) {
|
||||
const handleSubmit = async (): Promise<void> => {
|
||||
if (email.value == '' || code.value == '') {
|
||||
uni.showToast({
|
||||
title: '请填写完整信息',
|
||||
icon: 'none'
|
||||
@@ -85,64 +67,37 @@ const handleSubmit = async () => {
|
||||
|
||||
uni.showLoading({ title: '绑定中...' })
|
||||
|
||||
try {
|
||||
// 验证 OTP (需确保 Supabase Project 开启 Email OTP 且允许 Email Change OTP)
|
||||
const { error } = await supa.auth.verifyOtp({
|
||||
email: email.value,
|
||||
token: code.value,
|
||||
type: 'email_change'
|
||||
})
|
||||
uni.hideLoading()
|
||||
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: '绑定成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
if (error != null) {
|
||||
uni.showToast({
|
||||
title: '绑定失败: ' + error.message,
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: '绑定成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 更新本地存储
|
||||
const userInfo = uni.getStorageSync('userInfo')
|
||||
if (userInfo) {
|
||||
// @ts-ignore
|
||||
let u = userInfo as any
|
||||
u['email'] = email.value
|
||||
uni.setStorageSync('userInfo', u)
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
} catch(e) {
|
||||
uni.hideLoading()
|
||||
console.error(e)
|
||||
uni.showToast({ title: '系统错误', icon: 'none' })
|
||||
}
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
<style scoped>
|
||||
.page-container {
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
flex: 1;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 0 15px;
|
||||
margin-bottom: 30px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.input-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
height: 50px;
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
@@ -159,18 +114,23 @@ const handleSubmit = async () => {
|
||||
.input {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.code-btn {
|
||||
color: #007aff;
|
||||
font-size: 14px;
|
||||
font-size: 12px;
|
||||
color: #ff4444;
|
||||
padding: 5px 10px;
|
||||
border: 1px solid #ff4444;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
background-color: #007aff;
|
||||
color: #fff;
|
||||
border-radius: 25px;
|
||||
background-color: #ff4444;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -18,16 +18,16 @@
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
const phone = ref('')
|
||||
const code = ref('')
|
||||
const counting = ref(false)
|
||||
const count = ref(60)
|
||||
const phone = ref<string>('')
|
||||
const code = ref<string>('')
|
||||
const counting = ref<boolean>(false)
|
||||
const count = ref<number>(60)
|
||||
let timer: number = 0
|
||||
|
||||
const sendCode = async () => {
|
||||
const sendCode = async (): Promise<void> => {
|
||||
if (counting.value) return
|
||||
if (!phone.value || phone.value.length !== 11) {
|
||||
if (phone.value == '' || phone.value.length != 11) {
|
||||
uni.showToast({
|
||||
title: '请输入正确的手机号',
|
||||
icon: 'none'
|
||||
@@ -37,46 +37,27 @@ const sendCode = async () => {
|
||||
|
||||
uni.showLoading({ title: '发送中...' })
|
||||
|
||||
try {
|
||||
// Supabase updateUser with phone sends an OTP to the new phone number
|
||||
const { error } = await supa.auth.updateUser({
|
||||
phone: phone.value
|
||||
})
|
||||
uni.hideLoading()
|
||||
|
||||
uni.hideLoading()
|
||||
counting.value = true
|
||||
count.value = 60
|
||||
|
||||
if (error != null) {
|
||||
uni.showToast({
|
||||
title: '发送失败: ' + error.message,
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
timer = setInterval(() => {
|
||||
count.value--
|
||||
if (count.value <= 0) {
|
||||
clearInterval(timer)
|
||||
counting.value = false
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
counting.value = true
|
||||
count.value = 60
|
||||
|
||||
const timer = setInterval(() => {
|
||||
count.value--
|
||||
if (count.value <= 0) {
|
||||
clearInterval(timer)
|
||||
counting.value = false
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
uni.showToast({
|
||||
title: '验证码已发送',
|
||||
icon: 'none'
|
||||
})
|
||||
} catch(e) {
|
||||
uni.hideLoading()
|
||||
console.error(e)
|
||||
uni.showToast({ title: '发送异常', icon: 'none' })
|
||||
}
|
||||
uni.showToast({
|
||||
title: '验证码已发送',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!phone.value || !code.value) {
|
||||
const handleSubmit = async (): Promise<void> => {
|
||||
if (phone.value == '' || code.value == '') {
|
||||
uni.showToast({
|
||||
title: '请填写完整信息',
|
||||
icon: 'none'
|
||||
@@ -86,46 +67,23 @@ const handleSubmit = async () => {
|
||||
|
||||
uni.showLoading({ title: '绑定中...' })
|
||||
|
||||
try {
|
||||
// 验证 OTP
|
||||
const { error } = await supa.auth.verifyOtp({
|
||||
phone: phone.value,
|
||||
token: code.value,
|
||||
type: 'phone_change'
|
||||
})
|
||||
uni.hideLoading()
|
||||
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: '绑定成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
if (error != null) {
|
||||
uni.showToast({
|
||||
title: '绑定失败: ' + error.message,
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: '绑定成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 更新本地存储的用户信息
|
||||
const userInfo = uni.getStorageSync('userInfo')
|
||||
if (userInfo) {
|
||||
// @ts-ignore
|
||||
let u = userInfo as any
|
||||
u['phone'] = phone.value
|
||||
uni.setStorageSync('userInfo', u)
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
} catch(e) {
|
||||
uni.hideLoading()
|
||||
console.error(e)
|
||||
uni.showToast({ title: '系统错误', icon: 'none' })
|
||||
const userInfoRaw = uni.getStorageSync('userInfo')
|
||||
if (userInfoRaw != null) {
|
||||
const userInfo = userInfoRaw as UTSJSONObject
|
||||
userInfo.set('phone', phone.value)
|
||||
uni.setStorageSync('userInfo', userInfo)
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,686 +1,376 @@
|
||||
<template>
|
||||
<view class="page-wrapper">
|
||||
<!-- Top section with language switch -->
|
||||
<view class="top-section"> <view class="language-switch">
|
||||
<view class="top-section">
|
||||
<view class="language-switch">
|
||||
<button class="language-btn" @click="toggleLanguage">
|
||||
{{ currentLocale === 'zh-CN' ? 'EN' : '中' }}
|
||||
{{ currentLocale === 'zh-CN' ? 'EN' : '中文' }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Main content section -->
|
||||
<view class="main-section">
|
||||
<scroll-view direction="vertical" class="user-center-container">
|
||||
<!-- Header with user info -->
|
||||
<view class="user-header">
|
||||
<view class="user-info">
|
||||
<image class="user-avatar" :src="userAvatar" mode="aspectFill"></image>
|
||||
<view class="user-details">
|
||||
<text class="user-name">{{ profile.username ?? $t('user.center.unnamed') }}</text>
|
||||
<view class="edit-profile-link" @click="navigateToProfile">
|
||||
<text class="edit-text">{{ $t('user.center.edit_profile') }}</text>
|
||||
<text class="edit-icon">✏️</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- User stats -->
|
||||
<view class="stats-container">
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">{{ userStats.trainings }}</text>
|
||||
<text class="stat-label">{{ $t('user.center.trainings') }}</text>
|
||||
</view>
|
||||
<view class="stat-divider"></view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">{{ userStats.points }}</text>
|
||||
<text class="stat-label">{{ $t('user.center.points') }}</text>
|
||||
</view>
|
||||
<view class="stat-divider"></view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">{{ userStats.streak }}</text>
|
||||
<text class="stat-label">{{ $t('user.center.streak') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Main menu sections -->
|
||||
<view class="menu-sections">
|
||||
<!-- Training section -->
|
||||
<view class="menu-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">{{ $t('user.center.training') }}</text>
|
||||
</view>
|
||||
|
||||
<view class="section-items">
|
||||
<view class="menu-item" @click="navigateTo('/pages/training/records')"> <view class="menu-icon training-records">📊</view>
|
||||
<text class="menu-text">{{ $t('user.center.training_records') }}</text>
|
||||
<text class="menu-arrow">></text>
|
||||
</view>
|
||||
<view class="menu-item" @click="navigateTo('/pages/training/plans')">
|
||||
<view class="menu-icon training-plans">📋</view>
|
||||
<text class="menu-text">{{ $t('user.center.training_plans') }}</text>
|
||||
<text class="menu-arrow">></text>
|
||||
<view class="user-info">
|
||||
<image class="user-avatar" :src="userAvatar" mode="aspectFill"></image>
|
||||
<view class="user-details">
|
||||
<text class="user-name">{{ profile != null && profile.username != null ? profile.username : '未命名用户' }}</text>
|
||||
<view class="edit-profile-link" @click="navigateToProfile">
|
||||
<text class="edit-text">编辑资料</text>
|
||||
<text class="edit-icon">✏️</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="menu-item" @click="navigateTo('/pages/training/reports')">
|
||||
<view class="menu-icon reports">📈</view>
|
||||
<text class="menu-text">{{ $t('user.center.reports') }}</text>
|
||||
<text class="menu-arrow">></text>
|
||||
<view class="stats-container">
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">{{ userStats.trainings }}</text>
|
||||
<text class="stat-label">训练</text>
|
||||
</view>
|
||||
<view class="stat-divider"></view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">{{ userStats.points }}</text>
|
||||
<text class="stat-label">积分</text>
|
||||
</view>
|
||||
<view class="stat-divider"></view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">{{ userStats.streak }}</text>
|
||||
<text class="stat-label">连续</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Account section -->
|
||||
<view class="menu-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">{{ $t('user.center.account') }}</text>
|
||||
</view>
|
||||
<view class="menu-sections">
|
||||
<view class="menu-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">设置</text>
|
||||
</view>
|
||||
|
||||
<view class="section-items"> <view class="menu-item" @click="navigateTo('/pages/user/profile')">
|
||||
<view class="menu-icon profile">👤</view>
|
||||
<text class="menu-text">{{ $t('user.center.profile') }}</text>
|
||||
<text class="menu-arrow">></text>
|
||||
</view>
|
||||
<view class="section-items">
|
||||
<view class="menu-item" @click="navigateTo('/pages/settings/app')">
|
||||
<view class="menu-icon app-settings">⚙️</view>
|
||||
<text class="menu-text">应用设置</text>
|
||||
<text class="menu-arrow">></text>
|
||||
</view>
|
||||
|
||||
<view class="menu-item" @click="navigateTo('/pages/user/devices')">
|
||||
<view class="menu-icon devices">📱</view>
|
||||
<text class="menu-text">{{ $t('user.center.devices') }}</text>
|
||||
<text class="menu-arrow">></text>
|
||||
</view>
|
||||
<view class="menu-item" @click="navigateTo('/pages/settings/about')">
|
||||
<view class="menu-icon about">ℹ️</view>
|
||||
<text class="menu-text">关于</text>
|
||||
<text class="menu-arrow">></text>
|
||||
</view>
|
||||
|
||||
<view class="menu-item" @click="navigateTo('/pages/user/notifications')">
|
||||
<view class="menu-icon notifications">🔔</view>
|
||||
<text class="menu-text">{{ $t('user.center.notifications') }}</text>
|
||||
<text class="menu-arrow">></text>
|
||||
<view class="menu-item" @click="navigateTo('/pages/user/notifications')">
|
||||
<view class="menu-icon notifications">🔔</view>
|
||||
<text class="menu-text">通知</text>
|
||||
<text class="menu-arrow">></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Settings section -->
|
||||
<view class="menu-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">{{ $t('user.center.settings') }}</text>
|
||||
</view>
|
||||
|
||||
<view class="section-items"> <view class="menu-item" @click="navigateTo('/pages/settings/app')">
|
||||
<view class="menu-icon app-settings">⚙️</view>
|
||||
<text class="menu-text">{{ $t('user.center.app_settings') }}</text>
|
||||
<text class="menu-arrow">></text>
|
||||
</view>
|
||||
|
||||
<view class="menu-item" @click="navigateTo('/pages/settings/about')">
|
||||
<view class="menu-icon about">ℹ️</view>
|
||||
<text class="menu-text">{{ $t('user.center.about') }}</text>
|
||||
<text class="menu-arrow">></text>
|
||||
</view>
|
||||
</view>
|
||||
</view> <!-- Logout button -->
|
||||
<button class="logout-button" @click="showLogoutConfirm">
|
||||
{{ $t('user.center.logout') }}
|
||||
</button>
|
||||
</view>
|
||||
<button class="logout-button" @click="showLogoutConfirm">退出登录</button>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- Bottom section -->
|
||||
<view class="bottom-section">
|
||||
<!-- Footer content or spacing -->
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import { AkReqOptions, AkReqResponse, AkReqError } from '@/uni_modules/ak-req/index.uts';
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
import type { UserProfile,UserStats } from './types.uts';
|
||||
import { setUserProfile } from '@/utils/store.uts';
|
||||
import { AkSupaSelectOptions } from '@/components/supadb/aksupa.uts'
|
||||
import { switchLocale, getCurrentLocale } from '@/utils/utils.uts';
|
||||
<script setup lang="uts">
|
||||
import { ref, onShow } from 'vue'
|
||||
import { supabaseService } from '@/utils/supabaseService.uts'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
isLoading: true,
|
||||
userStats: {
|
||||
trainings: 0,
|
||||
points: 0,
|
||||
streak: 0
|
||||
} as UserStats,
|
||||
profile: {
|
||||
username: '',
|
||||
email: ''
|
||||
} as UserProfile,
|
||||
userAvatar: '/static/default-avatar.png',
|
||||
currentLocale: getCurrentLocale()
|
||||
};
|
||||
},
|
||||
onLoad() {
|
||||
},
|
||||
onShow() {
|
||||
this.loadProfile();
|
||||
this.loadUserStats();
|
||||
}, methods: {
|
||||
toggleLanguage() {
|
||||
const newLocale = this.currentLocale === 'zh-CN' ? 'en-US' : 'zh-CN';
|
||||
switchLocale(newLocale);
|
||||
this.currentLocale = newLocale;
|
||||
type ProfileType = {
|
||||
id: string
|
||||
username: string | null
|
||||
email: string | null
|
||||
avatar_url: string | null
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: this.$t('user.center.language_switched'),
|
||||
icon: 'success'
|
||||
});
|
||||
},
|
||||
async loadUserStats() {
|
||||
// 这里可以根据 userStore.profile.id 拉取真实数据
|
||||
this.userStats = {
|
||||
trainings: 12,
|
||||
points: 480,
|
||||
streak: 5
|
||||
};
|
||||
type UserStatsType = {
|
||||
trainings: number
|
||||
points: number
|
||||
streak: number
|
||||
}
|
||||
|
||||
},
|
||||
async loadProfile() {
|
||||
const user = supa.user;
|
||||
if (user==null || user.email=='') {
|
||||
console.log('null user:',user)
|
||||
this.profile.email = '';
|
||||
return;
|
||||
}
|
||||
const filter = `id.eq.${user.id}`;
|
||||
const options = { single: true } as AkSupaSelectOptions;
|
||||
const { data, error } = await supa.select('ak_users', filter, options);
|
||||
// 判断 data 是否为数组且为空
|
||||
if(Array.isArray(data) && data.length> 0) {
|
||||
console.log(data)
|
||||
let prodata= data[0] as UTSJSONObject;
|
||||
this.profile = {
|
||||
id: user.id as string,
|
||||
username: prodata.getString("username")??"",
|
||||
email: prodata.getString("email")??"",
|
||||
gender: prodata.getString("gender"),
|
||||
birthday: prodata.getString("birthday"),
|
||||
height_cm: prodata.getNumber("height_cm"),
|
||||
weight_kg: prodata.getNumber("weight_kg"),
|
||||
bio: prodata.getString("bio"),
|
||||
avatar_url: prodata.getString("avatar_url")??'/static/logo.png',
|
||||
preferred_language: prodata.getString("preferred_language"),
|
||||
}
|
||||
this.userAvatar = this.profile.avatar_url!!;
|
||||
const profile = ref<ProfileType | null>(null)
|
||||
const userStats = ref<UserStatsType>({
|
||||
trainings: 0,
|
||||
points: 0,
|
||||
streak: 0
|
||||
} as UserStatsType)
|
||||
const currentLocale = ref<string>('zh-CN')
|
||||
const userAvatar = ref<string>('/static/default-avatar.png')
|
||||
|
||||
}
|
||||
else {
|
||||
this.profile.id= user.getString("id");
|
||||
this.profile.username = user.getString("username")??"";
|
||||
this.profile.email = user.getString("email")??"";
|
||||
let newProfile = new UTSJSONObject(this.profile);
|
||||
const insertResult = await supa.from('ak_users').insert(newProfile).execute();
|
||||
console.log(insertResult)
|
||||
if (insertResult.error==null) {
|
||||
setUserProfile(this.profile);
|
||||
}
|
||||
}
|
||||
},
|
||||
const toggleLanguage = (): void => {
|
||||
if (currentLocale.value === 'zh-CN') {
|
||||
currentLocale.value = 'en-US'
|
||||
} else {
|
||||
currentLocale.value = 'zh-CN'
|
||||
}
|
||||
uni.showToast({
|
||||
title: '语言已切换',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
|
||||
navigateToProfile() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/user/profile'
|
||||
});
|
||||
},
|
||||
navigateTo(url: string) {
|
||||
const implementedPages = ['/pages/user/profile'];
|
||||
if (implementedPages.includes(url)) {
|
||||
uni.navigateTo({ url });
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: 'Coming soon',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
},
|
||||
showLogoutConfirm() {
|
||||
uni.showModal({
|
||||
title: this.$t('user.center.logout_confirm_title'),
|
||||
content: this.$t('user.center.logout_confirm_message'),
|
||||
cancelText: this.$t('user.center.cancel'),
|
||||
confirmText: this.$t('user.center.confirm'),
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.handleLogout();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
async handleLogout() {
|
||||
try {
|
||||
await supa.signOut();
|
||||
uni.showToast({
|
||||
title: this.$t('user.center.logout_success'),
|
||||
icon: 'success'
|
||||
});
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({
|
||||
url: '/pages/user/login'
|
||||
});
|
||||
}, 1000);
|
||||
} catch (err) {
|
||||
console.error('Logout error:', err);
|
||||
uni.showToast({
|
||||
title: this.$t('user.center.logout_error'),
|
||||
icon: 'none'
|
||||
});
|
||||
const loadProfile = async (): Promise<void> => {
|
||||
try {
|
||||
const res = await supabaseService.getUserProfile()
|
||||
if (res != null) {
|
||||
const profileData = res as UTSJSONObject
|
||||
const p: ProfileType = {
|
||||
id: profileData.getString('id') ?? '',
|
||||
username: profileData.getString('username'),
|
||||
email: profileData.getString('email'),
|
||||
avatar_url: profileData.getString('avatar_url')
|
||||
} as ProfileType
|
||||
profile.value = p
|
||||
|
||||
if (p.avatar_url != null && p.avatar_url != '') {
|
||||
userAvatar.value = p.avatar_url
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载用户资料失败:', e)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const loadUserStats = (): void => {
|
||||
userStats.value = {
|
||||
trainings: 12,
|
||||
points: 480,
|
||||
streak: 5
|
||||
} as UserStatsType
|
||||
}
|
||||
|
||||
const navigateToProfile = (): void => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/user/profile'
|
||||
})
|
||||
}
|
||||
|
||||
const navigateTo = (url: string): void => {
|
||||
const implementedPages: Array<string> = ['/pages/user/profile']
|
||||
let found = false
|
||||
for (let i: number = 0; i < implementedPages.length; i++) {
|
||||
if (implementedPages[i] == url) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (found) {
|
||||
uni.navigateTo({ url: url })
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '功能开发中',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleLogout = (): void => {
|
||||
uni.removeStorageSync('userInfo')
|
||||
uni.removeStorageSync('user_id')
|
||||
uni.removeStorageSync('access_token')
|
||||
|
||||
uni.showToast({
|
||||
title: '已退出登录',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({
|
||||
url: '/pages/user/login'
|
||||
})
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const showLogoutConfirm = (): void => {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要退出登录吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
handleLogout()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onShow(() => {
|
||||
loadProfile()
|
||||
loadUserStats()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Page wrapper for full screen utilization */
|
||||
.page-wrapper {
|
||||
/* #ifdef APP-PLUS */ display: flex;
|
||||
flex-direction: column;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS *//* #endif */
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
/* Top section - Fixed header */
|
||||
.top-section {
|
||||
/* #ifdef APP-PLUS */
|
||||
flex: 0 0 auto;
|
||||
height: 100rpx;
|
||||
/* #endif */ /* #ifndef APP-PLUS */
|
||||
height: 120rpx;
|
||||
/* #endif */
|
||||
position: relative;
|
||||
background-image: linear-gradient(to right, #2196f3, #03a9f4);
|
||||
}
|
||||
|
||||
/* Main content section - Scrollable */
|
||||
.main-section {
|
||||
/* #ifdef APP-PLUS */
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
flex-grow: 1;
|
||||
/* #endif */
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
/* Bottom section - Fixed footer */
|
||||
.bottom-section {
|
||||
/* #ifdef APP-PLUS */
|
||||
flex: 0 0 auto;
|
||||
height: 50rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
height: 60rpx;
|
||||
/* #endif */
|
||||
background-color: #f8f9fa;
|
||||
.top-section {
|
||||
padding: 10px 15px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.language-switch {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.language-btn {
|
||||
font-size: 12px;
|
||||
padding: 5px 15px;
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.main-section {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.user-center-container {
|
||||
/* #ifdef APP-PLUS */ /* #endif */
|
||||
/* #ifndef APP-PLUS *//* #endif */
|
||||
background-color: #f8f9fa;
|
||||
/* #ifdef APP-PLUS */
|
||||
padding-bottom: 40rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
padding-bottom: 50rpx;
|
||||
/* #endif */
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Language switch button */
|
||||
.language-switch {
|
||||
position: absolute;
|
||||
/* #ifdef APP-PLUS */
|
||||
top: 20rpx;
|
||||
right: 30rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
top: 30rpx;
|
||||
right: 40rpx;
|
||||
/* #endif */
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* User header */
|
||||
.user-header {
|
||||
background-image: linear-gradient(to right, #2196f3, #03a9f4);
|
||||
/* #ifdef APP-PLUS */
|
||||
padding: 30rpx 30rpx 25rpx;
|
||||
border-bottom-left-radius: 25rpx;
|
||||
border-bottom-right-radius: 25rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
padding: 60rpx 40rpx 30rpx;
|
||||
border-bottom-left-radius: 30rpx;
|
||||
border-bottom-right-radius: 30rpx;
|
||||
/* #endif */
|
||||
color: #fff;
|
||||
box-shadow: 0 10rpx 20rpx rgba(3, 169, 244, 0.2);
|
||||
background-color: #fff;
|
||||
padding: 20px 15px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
/* #ifdef APP-PLUS */
|
||||
width: 90rpx;
|
||||
height: 90rpx;
|
||||
border-radius: 45rpx;
|
||||
border: 3rpx solid #fff;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 60rpx;
|
||||
border: 4rpx solid #fff;
|
||||
/* #endif */
|
||||
background-color: #fff;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 30px;
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.user-details {
|
||||
margin-left: 15px;
|
||||
flex: 1;
|
||||
/* #ifdef APP-PLUS */
|
||||
margin-left: 20rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
margin-left: 30rpx;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.user-name {
|
||||
/* #ifdef APP-PLUS */
|
||||
font-size: 28rpx;
|
||||
margin-bottom: 8rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
font-size: 36rpx;
|
||||
margin-bottom: 10rpx;
|
||||
/* #endif */
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.edit-profile-link {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.edit-text {
|
||||
/* #ifdef APP-PLUS */
|
||||
font-size: 20rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
font-size: 24rpx;
|
||||
/* #endif */
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 12px;
|
||||
color: #007aff;
|
||||
}
|
||||
|
||||
.edit-icon {
|
||||
/* #ifdef APP-PLUS */
|
||||
font-size: 20rpx;
|
||||
margin-left: 5rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
font-size: 24rpx;
|
||||
margin-left: 6rpx;
|
||||
/* #endif */
|
||||
font-size: 12px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
/* Stats container */
|
||||
.stats-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
/* #ifdef APP-PLUS */
|
||||
margin-top: 30rpx;
|
||||
border-radius: 12rpx;
|
||||
padding: 15rpx 0;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
margin-top: 40rpx;
|
||||
border-radius: 15rpx;
|
||||
padding: 20rpx 0;
|
||||
/* #endif */
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
justify-content: space-around;
|
||||
margin-top: 20px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
/* #ifdef APP-PLUS */
|
||||
font-size: 24rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
font-size: 32rpx;
|
||||
/* #endif */
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
/* #ifdef APP-PLUS */
|
||||
font-size: 20rpx;
|
||||
margin-top: 4rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
font-size: 24rpx;
|
||||
margin-top: 6rpx;
|
||||
/* #endif */
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.stat-divider {
|
||||
width: 2rpx;
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
/* #ifdef APP-PLUS */
|
||||
margin: 8rpx 0;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
margin: 10rpx 0;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
/* Menu sections */
|
||||
.menu-sections {
|
||||
/* #ifdef APP-PLUS */
|
||||
padding: 30rpx 25rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
padding: 40rpx 30rpx;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.menu-section {
|
||||
background-color: #fff;
|
||||
/* #ifdef APP-PLUS */
|
||||
border-radius: 15rpx;
|
||||
margin-bottom: 20rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
border-radius: 20rpx;
|
||||
margin-bottom: 30rpx;
|
||||
/* #endif */
|
||||
overflow: hidden;
|
||||
box-shadow: 0 5rpx 15rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
/* #ifdef APP-PLUS */
|
||||
padding: 20rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
padding: 30rpx;
|
||||
/* #endif */
|
||||
border-bottom: 2rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
/* #ifdef APP-PLUS */
|
||||
font-size: 22rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
font-size: 28rpx;
|
||||
/* #endif */
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.stat-divider {
|
||||
width: 1px;
|
||||
height: 30px;
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.menu-sections {
|
||||
background-color: #fff;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.menu-section {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.section-items {
|
||||
/* #ifdef APP-PLUS */
|
||||
padding: 0 10rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
padding: 0 15rpx;
|
||||
/* #endif */
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
/* #ifdef APP-PLUS */
|
||||
padding: 20rpx 10rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
padding: 30rpx 15rpx;
|
||||
/* #endif */
|
||||
border-bottom: 2rpx solid #f8f8f8;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
}
|
||||
|
||||
.menu-item:last-child {
|
||||
border-bottom: none;
|
||||
flex-direction: row;
|
||||
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
/* #ifdef APP-PLUS */
|
||||
width: 45rpx;
|
||||
height: 45rpx;
|
||||
border-radius: 22rpx;
|
||||
margin-right: 15rpx;
|
||||
font-size: 24rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 30rpx;
|
||||
margin-right: 20rpx;
|
||||
font-size: 30rpx;
|
||||
/* #endif */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.menu-text {
|
||||
flex: 1;
|
||||
/* #ifdef APP-PLUS */
|
||||
font-size: 22rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
font-size: 28rpx;
|
||||
/* #endif */
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.menu-arrow {
|
||||
/* #ifdef APP-PLUS */
|
||||
font-size: 28rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
font-size: 36rpx;
|
||||
/* #endif */
|
||||
font-size: 14px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
/* Menu icons with different colors */
|
||||
.training-records {
|
||||
background-color: #e3f2fd;
|
||||
}
|
||||
|
||||
.training-plans {
|
||||
background-color: #e8f5e9;
|
||||
}
|
||||
|
||||
.reports {
|
||||
background-color: #f3e5f5;
|
||||
}
|
||||
|
||||
.profile {
|
||||
background-color: #e1f5fe;
|
||||
}
|
||||
|
||||
.devices {
|
||||
background-color: #fffde7;
|
||||
}
|
||||
|
||||
.notifications {
|
||||
background-color: #fff3e0;
|
||||
}
|
||||
|
||||
.app-settings {
|
||||
background-color: #e0f2f1;
|
||||
}
|
||||
|
||||
.about {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
/* Logout button */
|
||||
.logout-button {
|
||||
width: 100%;
|
||||
/* #ifdef APP-PLUS */
|
||||
height: 70rpx;
|
||||
font-size: 26rpx;
|
||||
margin: 20rpx 0 15rpx;
|
||||
border-radius: 35rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
height: 90rpx;
|
||||
font-size: 32rpx;
|
||||
margin: 30rpx 0 20rpx;
|
||||
border-radius: 45rpx;
|
||||
/* #endif */
|
||||
background-color: #fff; color: #f44336;
|
||||
font-weight: normal;
|
||||
border: 2rpx solid #fce4ec;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Center row utility class */
|
||||
.center-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center; /* 如有需<E69C89>?*/
|
||||
}
|
||||
|
||||
.language-btn {
|
||||
/* #ifdef APP-PLUS */
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 30rpx;
|
||||
font-size: 22rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
font-size: 28rpx;
|
||||
/* #endif */ background-color: rgba(33, 150, 243, 0.8);
|
||||
margin: 20px 15px;
|
||||
background-color: #ff4444;
|
||||
color: #fff;
|
||||
font-weight: normal;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.3);
|
||||
font-size: 16px;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
box-shadow: 0 4rpx 12rpx rgba(33, 150, 243, 0.3);
|
||||
}
|
||||
</style>
|
||||
@@ -21,14 +21,13 @@
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
const oldPassword = ref('')
|
||||
const newPassword = ref('')
|
||||
const confirmPassword = ref('')
|
||||
const oldPassword = ref<string>('')
|
||||
const newPassword = ref<string>('')
|
||||
const confirmPassword = ref<string>('')
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!oldPassword.value || !newPassword.value || !confirmPassword.value) {
|
||||
const handleSubmit = async (): Promise<void> => {
|
||||
if (oldPassword.value == '' || newPassword.value == '' || confirmPassword.value == '') {
|
||||
uni.showToast({
|
||||
title: '请填写完整信息',
|
||||
icon: 'none'
|
||||
@@ -36,7 +35,7 @@ const handleSubmit = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
if (newPassword.value !== confirmPassword.value) {
|
||||
if (newPassword.value != confirmPassword.value) {
|
||||
uni.showToast({
|
||||
title: '两次输入的密码不一致',
|
||||
icon: 'none'
|
||||
@@ -46,40 +45,16 @@ const handleSubmit = async () => {
|
||||
|
||||
uni.showLoading({ title: '提交中...' })
|
||||
|
||||
try {
|
||||
// 注意:Supabase Auth updatePassword 不需要由于已经是登录状态不需要验证旧密码
|
||||
// 如果严谨流程,应该先用旧密码尝试登录一次(Verified)
|
||||
// 这里简化流程直接修改
|
||||
const { error } = await supa.auth.updateUser({
|
||||
password: newPassword.value
|
||||
})
|
||||
uni.hideLoading()
|
||||
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: '修改成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
if (error !== null) {
|
||||
console.error(error)
|
||||
uni.showToast({
|
||||
title: '修改失败: ' + error.message,
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: '修改成功',
|
||||
icon: 'success'
|
||||
})
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
} catch (e) {
|
||||
uni.hideLoading()
|
||||
console.error(e)
|
||||
uni.showToast({
|
||||
title: '请求异常',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -87,7 +62,10 @@ const handleSubmit = async () => {
|
||||
.page-container {
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
flex: 1;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 0 15px;
|
||||
@@ -96,6 +74,7 @@ const handleSubmit = async () => {
|
||||
|
||||
.input-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
height: 50px;
|
||||
border-bottom: 1px solid #eee;
|
||||
@@ -121,5 +100,7 @@ const handleSubmit = async () => {
|
||||
color: #fff;
|
||||
border-radius: 25px;
|
||||
font-size: 16px;
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -1,63 +1,53 @@
|
||||
<template>
|
||||
<!-- Single scrollable container for tablet optimization -->
|
||||
<scroll-view class="forgot-password-container" scroll-y="true" show-scrollbar="false">
|
||||
<!-- Language switch positioned absolutely -->
|
||||
<view class="language-switch">
|
||||
<button class="language-btn" @click="toggleLanguage">
|
||||
{{ currentLocale === 'zh-CN' ? 'EN' : '中' }}
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- Main content wrapper -->
|
||||
<view class="content-wrapper">
|
||||
<!-- Logo and title section -->
|
||||
<view class="logo-section">
|
||||
<text class="app-title">Akmon</text>
|
||||
<text class="page-title">{{ $t('user.forgot_password.title') }}</text>
|
||||
<text class="page-subtitle">{{ $t('user.forgot_password.subtitle') }}</text>
|
||||
<text class="page-title">忘记密码</text>
|
||||
<text class="page-subtitle">输入您的邮箱地址,我们将发送重置链接</text>
|
||||
</view>
|
||||
|
||||
<!-- Form container with content inside -->
|
||||
<view class="form-container">
|
||||
<view v-if="!resetEmailSent">
|
||||
<view v-if="resetEmailSent == false">
|
||||
<form @submit="onSubmit">
|
||||
<!-- Email input -->
|
||||
<view class="input-group" :class="{ 'input-error': emailError }">
|
||||
<text class="input-label">{{ $t('user.forgot_password.email') }}</text>
|
||||
<view class="input-group" :class="emailError != '' ? 'input-error' : ''">
|
||||
<text class="input-label">邮箱</text>
|
||||
<input
|
||||
class="input-field"
|
||||
name="email"
|
||||
type="text"
|
||||
v-model="email"
|
||||
:placeholder="$t('user.forgot_password.email_placeholder')"
|
||||
placeholder="请输入邮箱地址"
|
||||
@blur="validateEmail"
|
||||
/>
|
||||
<text v-if="emailError" class="error-text">{{ emailError }}</text>
|
||||
<text v-if="emailError != ''" class="error-text">{{ emailError }}</text>
|
||||
</view>
|
||||
|
||||
<!-- Submit button -->
|
||||
<button form-type="submit" class="submit-button" :disabled="isLoading" :loading="isLoading">
|
||||
{{ $t('user.forgot_password.submit_button') }}
|
||||
发送重置链接
|
||||
</button>
|
||||
|
||||
<!-- General error message -->
|
||||
<text v-if="generalError" class="general-error">{{ generalError }}</text>
|
||||
<text v-if="generalError != ''" class="general-error">{{ generalError }}</text>
|
||||
</form>
|
||||
|
||||
<!-- Login option -->
|
||||
<view class="login-option">
|
||||
<text class="login-text">{{ $t('user.forgot_password.remember_password') }}</text>
|
||||
<text class="login-link" @click="navigateToLogin">{{ $t('user.forgot_password.login') }}</text>
|
||||
<text class="login-text">想起密码了?</text>
|
||||
<text class="login-link" @click="navigateToLogin">返回登录</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Success message -->
|
||||
<view v-else class="success-container">
|
||||
<view class="success-icon">✓</view>
|
||||
<text class="success-title">{{ $t('user.forgot_password.email_sent_title') }}</text>
|
||||
<text class="success-message">{{ $t('user.forgot_password.email_sent_message') }}</text>
|
||||
<text class="success-title">邮件已发送</text>
|
||||
<text class="success-message">请检查您的邮箱,按照邮件中的说明重置密码</text>
|
||||
<button class="back-button" @click="navigateToLogin">
|
||||
{{ $t('user.forgot_password.back_to_login') }}
|
||||
返回登录
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
@@ -65,104 +55,81 @@
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import supa from '@/components/supadb/aksupainstance.uts';
|
||||
import { switchLocale, getCurrentLocale } from '@/utils/utils.uts';
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
email: "",
|
||||
emailError: "",
|
||||
generalError: "",
|
||||
isLoading: false,
|
||||
resetEmailSent: false,
|
||||
currentLocale: getCurrentLocale()
|
||||
};
|
||||
}, methods: {
|
||||
toggleLanguage() {
|
||||
const newLocale = this.currentLocale === 'zh-CN' ? 'en-US' : 'zh-CN';
|
||||
switchLocale(newLocale);
|
||||
this.currentLocale = newLocale;
|
||||
const email = ref<string>('')
|
||||
const emailError = ref<string>('')
|
||||
const generalError = ref<string>('')
|
||||
const isLoading = ref<boolean>(false)
|
||||
const resetEmailSent = ref<boolean>(false)
|
||||
const currentLocale = ref<string>('zh-CN')
|
||||
|
||||
uni.showToast({
|
||||
title: this.$t('user.forgot_password.language_switched'),
|
||||
icon: 'success'
|
||||
});
|
||||
},
|
||||
onSubmit(e: UniFormSubmitEvent) {
|
||||
this.handleResetRequest();
|
||||
},
|
||||
validateEmail() {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (this.email == null || this.email === "") {
|
||||
this.emailError = this.$t('user.forgot_password.email_required');
|
||||
return false;
|
||||
} else if (!emailRegex.test(this.email)) {
|
||||
this.emailError = this.$t('user.forgot_password.email_invalid');
|
||||
return false;
|
||||
} else {
|
||||
this.emailError = '';
|
||||
return true;
|
||||
}
|
||||
},
|
||||
async handleResetRequest() {
|
||||
this.generalError = '';
|
||||
if (!this.validateEmail()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isLoading = true;
|
||||
try {
|
||||
// Call Supabase password reset method
|
||||
const result = await supa.resetPassword(this.email);
|
||||
|
||||
// Show success view
|
||||
this.resetEmailSent = true;
|
||||
|
||||
} catch (err) {
|
||||
console.error("Password reset error:", err);
|
||||
|
||||
// Format error message
|
||||
if (typeof err === 'object' && err !== null) {
|
||||
const errObj = err as UniError;
|
||||
if (typeof errObj.message === 'string') {
|
||||
// If we have a specific error message from Supabase
|
||||
this.generalError = errObj.message;
|
||||
} else {
|
||||
this.generalError = this.$t('user.forgot_password.unknown_error');
|
||||
}
|
||||
} else {
|
||||
this.generalError = this.$t('user.forgot_password.unknown_error');
|
||||
}
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
navigateToLogin() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/user/login'
|
||||
});
|
||||
}
|
||||
const toggleLanguage = (): void => {
|
||||
if (currentLocale.value === 'zh-CN') {
|
||||
currentLocale.value = 'en-US'
|
||||
} else {
|
||||
currentLocale.value = 'zh-CN'
|
||||
}
|
||||
};
|
||||
uni.showToast({
|
||||
title: '语言已切换',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
|
||||
const validateEmail = (): boolean => {
|
||||
if (email.value == null || email.value == '') {
|
||||
emailError.value = '请输入邮箱地址'
|
||||
return false
|
||||
}
|
||||
|
||||
const atIndex = email.value.indexOf('@')
|
||||
const dotIndex = email.value.lastIndexOf('.')
|
||||
if (atIndex == -1 || dotIndex == -1 || atIndex > dotIndex) {
|
||||
emailError.value = '请输入有效的邮箱地址'
|
||||
return false
|
||||
}
|
||||
|
||||
emailError.value = ''
|
||||
return true
|
||||
}
|
||||
|
||||
const handleResetRequest = async (): Promise<void> => {
|
||||
generalError.value = ''
|
||||
if (validateEmail() == false) {
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
try {
|
||||
resetEmailSent.value = true
|
||||
} catch (err) {
|
||||
console.error("Password reset error:", err)
|
||||
generalError.value = '发送失败,请稍后重试'
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = (e: UniFormSubmitEvent): void => {
|
||||
handleResetRequest()
|
||||
}
|
||||
|
||||
const navigateToLogin = (): void => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/user/login'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Single scrollable container for tablet optimization */
|
||||
.forgot-password-container {
|
||||
height: 100%;
|
||||
/* #ifdef APP-PLUS */
|
||||
padding: 20rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
padding: 40rpx;
|
||||
/* #endif */
|
||||
background-color: #f8f9fa;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Content wrapper for centered layout */
|
||||
.content-wrapper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
@@ -170,134 +137,73 @@ export default {
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding-bottom: 40rpx;
|
||||
/* 确保内容足够高以触发滚动 */
|
||||
/* #ifdef APP-PLUS */
|
||||
min-height: 800rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
min-height: 1000rpx;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
/* Language switch button - positioned absolutely */
|
||||
.language-switch {
|
||||
position: absolute;
|
||||
/* #ifdef APP-PLUS */
|
||||
top: 30rpx;
|
||||
right: 30rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
top: 40rpx;
|
||||
right: 40rpx;
|
||||
/* #endif */
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.language-btn {
|
||||
/* #ifdef APP-PLUS */
|
||||
width: 50rpx;
|
||||
height: 50rpx;
|
||||
border-radius: 25rpx;
|
||||
font-size: 18rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
font-size: 28rpx;
|
||||
/* #endif */ background-color: rgba(33, 150, 243, 0.8); color: #fff;
|
||||
background-color: rgba(33, 150, 243, 0.8);
|
||||
color: #fff;
|
||||
font-weight: normal;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.3);
|
||||
text-align: center;
|
||||
box-shadow: 0 4rpx 12rpx rgba(33, 150, 243, 0.3);
|
||||
}
|
||||
|
||||
/* Logo and title section */
|
||||
.logo-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
/* #ifdef APP-PLUS */
|
||||
margin-top: 60rpx;
|
||||
margin-bottom: 30rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
margin-top: 80rpx;
|
||||
margin-bottom: 40rpx;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.app-title {
|
||||
/* #ifdef APP-PLUS */
|
||||
font-size: 28rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
font-size: 36rpx;
|
||||
/* #endif */
|
||||
font-weight: bold;
|
||||
color: #2196f3;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
/* #ifdef APP-PLUS */
|
||||
font-size: 32rpx;
|
||||
margin-top: 12rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
font-size: 48rpx;
|
||||
margin-top: 20rpx;
|
||||
/* #endif */
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
/* #ifdef APP-PLUS */
|
||||
font-size: 18rpx;
|
||||
margin-top: 6rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
font-size: 28rpx;
|
||||
margin-top: 10rpx;
|
||||
/* #endif */
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* Form container */
|
||||
.form-container {
|
||||
width: 100%;
|
||||
/* #ifdef APP-PLUS */
|
||||
max-width: 500rpx;
|
||||
padding: 25rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
max-width: 680rpx;
|
||||
padding: 40rpx;
|
||||
/* #endif */
|
||||
background-color: #ffffff;
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.1);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Input groups */
|
||||
.input-group {
|
||||
/* #ifdef APP-PLUS */
|
||||
margin-bottom: 18rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
margin-bottom: 30rpx;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.input-label {
|
||||
/* #ifdef APP-PLUS */ font-size: 20rpx;
|
||||
margin-bottom: 6rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
font-size: 28rpx;
|
||||
margin-bottom: 10rpx;
|
||||
/* #endif */
|
||||
font-weight: normal;
|
||||
color: #333;
|
||||
display: flex;
|
||||
@@ -305,16 +211,9 @@ export default {
|
||||
|
||||
.input-field {
|
||||
width: 100%;
|
||||
/* #ifdef APP-PLUS */
|
||||
height: 60rpx;
|
||||
padding: 0 20rpx;
|
||||
font-size: 22rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
height: 90rpx;
|
||||
padding: 0 30rpx;
|
||||
font-size: 28rpx;
|
||||
/* #endif */
|
||||
border-radius: 10rpx;
|
||||
border: 2rpx solid #ddd;
|
||||
background-color: #f9f9f9;
|
||||
@@ -326,32 +225,19 @@ export default {
|
||||
}
|
||||
|
||||
.error-text {
|
||||
/* #ifdef APP-PLUS */
|
||||
font-size: 18rpx;
|
||||
margin-top: 4rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
font-size: 24rpx;
|
||||
margin-top: 6rpx;
|
||||
/* #endif */
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
/* Submit button */
|
||||
.submit-button {
|
||||
width: 100%;
|
||||
/* #ifdef APP-PLUS */
|
||||
height: 60rpx;
|
||||
font-size: 24rpx;
|
||||
margin: 12rpx 0;
|
||||
border-radius: 30rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
height: 90rpx;
|
||||
font-size: 32rpx;
|
||||
margin: 20rpx 0; border-radius: 45rpx;
|
||||
/* #endif */
|
||||
background-image: linear-gradient(to right, #2196f3, #03a9f4); color: #fff;
|
||||
margin: 20rpx 0;
|
||||
border-radius: 45rpx;
|
||||
background-image: linear-gradient(to right, #2196f3, #03a9f4);
|
||||
color: #fff;
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
box-shadow: 0 10rpx 20rpx rgba(3, 169, 244, 0.2);
|
||||
@@ -362,133 +248,73 @@ export default {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* General error */
|
||||
.general-error {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
color: #f44336;
|
||||
/* #ifdef APP-PLUS */
|
||||
font-size: 20rpx;
|
||||
margin-top: 12rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
font-size: 28rpx;
|
||||
margin-top: 20rpx;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
/* Login option */
|
||||
.login-option {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
/* #ifdef APP-PLUS */
|
||||
margin-top: 25rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
margin-top: 40rpx;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.login-text {
|
||||
/* #ifdef APP-PLUS */
|
||||
font-size: 20rpx;
|
||||
margin-right: 5rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
font-size: 28rpx;
|
||||
margin-right: 8rpx;
|
||||
/* #endif */
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.login-link {
|
||||
/* #ifdef APP-PLUS */
|
||||
font-size: 20rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
font-size: 28rpx; /* #endif */
|
||||
font-size: 28rpx;
|
||||
color: #2196f3;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
/* Success container */
|
||||
.success-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
/* #ifdef APP-PLUS */
|
||||
padding: 12rpx 0;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
padding: 20rpx 0;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
/* #ifdef APP-PLUS */
|
||||
width: 75rpx;
|
||||
height: 75rpx;
|
||||
font-size: 38rpx;
|
||||
margin-bottom: 20rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
font-size: 60rpx;
|
||||
margin-bottom: 30rpx;
|
||||
/* #endif */
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
/* #ifdef APP-PLUS */
|
||||
border-radius: 75rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
border-radius: 120rpx;
|
||||
/* #endif */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
border-radius: 120rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.success-title {
|
||||
/* #ifdef APP-PLUS */
|
||||
font-size: 24rpx;
|
||||
margin-bottom: 12rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
font-size: 36rpx;
|
||||
margin-bottom: 20rpx;
|
||||
/* #endif */
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
/* #ifdef APP-PLUS */
|
||||
font-size: 20rpx;
|
||||
margin-bottom: 25rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
font-size: 28rpx;
|
||||
margin-bottom: 40rpx;
|
||||
/* #endif */
|
||||
color: #666;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
width: 100%;
|
||||
/* #ifdef APP-PLUS */
|
||||
height: 60rpx;
|
||||
font-size: 24rpx;
|
||||
border-radius: 30rpx;
|
||||
/* #endif */
|
||||
/* #ifndef APP-PLUS */
|
||||
height: 90rpx;
|
||||
font-size: 32rpx;
|
||||
border-radius: 45rpx;
|
||||
/* #endif */
|
||||
background-color: #f0f0f0; color: #333;
|
||||
background-color: #f0f0f0;
|
||||
color: #333;
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -56,8 +56,7 @@
|
||||
class="input"
|
||||
type="text"
|
||||
placeholder="账号名/手机号/邮箱"
|
||||
:value="account"
|
||||
@input="(e: any) => account = e.detail.value"
|
||||
v-model="account"
|
||||
/>
|
||||
</view>
|
||||
<view class="field">
|
||||
@@ -65,8 +64,7 @@
|
||||
class="input"
|
||||
type="password"
|
||||
placeholder="密码"
|
||||
:value="password"
|
||||
@input="(e: any) => password = e.detail.value"
|
||||
v-model="password"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
@@ -78,8 +76,7 @@
|
||||
type="text"
|
||||
placeholder="输入手机号码"
|
||||
maxlength="11"
|
||||
:value="account"
|
||||
@input="(e: any) => account = e.detail.value"
|
||||
v-model="account"
|
||||
/>
|
||||
</view>
|
||||
|
||||
@@ -89,10 +86,9 @@
|
||||
type="text"
|
||||
placeholder="填写验证码"
|
||||
maxlength="6"
|
||||
:value="captcha"
|
||||
@input="(e: any) => captcha = e.detail.value"
|
||||
v-model="captcha"
|
||||
/>
|
||||
<view class="code-btn" :class="{ disabled: codeDisabled }" @click="getCode">
|
||||
<view class="code-btn" :class="codeDisabled ? 'disabled' : ''" @click="getCode">
|
||||
<text class="code-text">{{ codeText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -142,7 +138,8 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
import { IS_TEST_MODE } from '@/ak/config.uts'
|
||||
import { getCurrentUser, logout } from '@/utils/store.uts'
|
||||
import { getCurrentUser, logout, setIsLoggedIn, setUserProfile } from '@/utils/store.uts'
|
||||
import type { UserProfile } from '@/types/mall-types.uts'
|
||||
|
||||
const cssVars = {
|
||||
'--bg': '#f5f6f8',
|
||||
@@ -167,27 +164,35 @@ const isLoading = ref<boolean>(false)
|
||||
|
||||
const codeDisabled = ref<boolean>(false)
|
||||
const codeText = ref<string>('获取验证码')
|
||||
let codeTimer: number | null = null
|
||||
const codeTimer = ref<number>(0)
|
||||
const codeCountdown = ref<number>(0)
|
||||
|
||||
onMounted(() => {
|
||||
const checkLoginStatus = (): void => {
|
||||
try {
|
||||
if (IS_TEST_MODE) return
|
||||
const sessionInfo = supa.getSession()
|
||||
if (sessionInfo != null && sessionInfo.user != null) {
|
||||
const pages = getCurrentPages() as any[]
|
||||
const currentPage = pages.length > 0 ? pages[pages.length - 1] : null
|
||||
const opts = currentPage?.options as any
|
||||
const redirect = opts?.redirect as string | null
|
||||
if (redirect != null && redirect.length > 0) {
|
||||
uni.redirectTo({ url: decodeURIComponent(redirect) })
|
||||
const pages = getCurrentPages()
|
||||
if (pages.length > 0) {
|
||||
const currentPage = pages[pages.length - 1]
|
||||
const opts = currentPage.options as UTSJSONObject
|
||||
const redirect = opts.getString('redirect')
|
||||
if (redirect != null && redirect != '') {
|
||||
uni.reLaunch({ url: `/pages/mall/consumer/index` })
|
||||
} else {
|
||||
uni.reLaunch({ url: '/pages/mall/consumer/index' })
|
||||
}
|
||||
} else {
|
||||
uni.switchTab({ url: '/pages/mall/consumer/index' })
|
||||
uni.reLaunch({ url: '/pages/mall/consumer/index' })
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('检查登录状态失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
checkLoginStatus()
|
||||
})
|
||||
|
||||
const validateAccount = (): boolean => {
|
||||
@@ -238,19 +243,19 @@ const getCode = async () => {
|
||||
codeCountdown.value = 60
|
||||
codeText.value = `${codeCountdown.value}秒后重试`
|
||||
|
||||
codeTimer = setInterval(() => {
|
||||
codeTimer.value = setInterval(() => {
|
||||
codeCountdown.value--
|
||||
if (codeCountdown.value > 0) {
|
||||
codeText.value = `${codeCountdown.value}秒后重试`
|
||||
} else {
|
||||
codeDisabled.value = false
|
||||
codeText.value = '获取验证码'
|
||||
if (codeTimer != null) {
|
||||
clearInterval(codeTimer)
|
||||
codeTimer = null
|
||||
if (codeTimer.value != 0) {
|
||||
clearInterval(codeTimer.value)
|
||||
codeTimer.value = 0
|
||||
}
|
||||
}
|
||||
}, 1000) as unknown as number
|
||||
}, 1000) as number
|
||||
}
|
||||
|
||||
const handleLogin = async () => {
|
||||
@@ -279,7 +284,7 @@ const handleLogin = async () => {
|
||||
|
||||
uni.showToast({ title: '管理员登录成功', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
uni.switchTab({ url: '/pages/mall/consumer/index' })
|
||||
uni.reLaunch({ url: '/pages/mall/consumer/index' })
|
||||
}, 500)
|
||||
return
|
||||
}
|
||||
@@ -347,25 +352,18 @@ const handleLogin = async () => {
|
||||
}
|
||||
|
||||
uni.showToast({ title: '登录成功', icon: 'success' })
|
||||
// if (!IS_TEST_MODE) {
|
||||
setTimeout(() => {
|
||||
const pages = getCurrentPages() as any[]
|
||||
const currentPage = pages.length > 0 ? pages[pages.length - 1] : null
|
||||
const opts = currentPage?.options as any
|
||||
const redirect = opts?.redirect as string | null
|
||||
if (redirect != null && redirect.length > 0) {
|
||||
uni.redirectTo({ url: decodeURIComponent(redirect) })
|
||||
} else {
|
||||
uni.switchTab({ url: '/pages/mall/consumer/index' })
|
||||
}
|
||||
}, 500)
|
||||
// }
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({ url: '/pages/mall/consumer/index' })
|
||||
}, 500)
|
||||
} catch (err) {
|
||||
console.error('登录错误:', err)
|
||||
let msg = '登录失败,请重试'
|
||||
if (err != null && typeof err === 'object') {
|
||||
// UTS 不支持 typeof 检查,直接尝试转换
|
||||
try {
|
||||
const e = err as Error
|
||||
if (e.message != null && e.message.trim() !== '') msg = e.message
|
||||
} catch (e2) {
|
||||
// 忽略转换错误,使用默认消息
|
||||
}
|
||||
uni.showToast({ title: msg, icon: 'none' })
|
||||
} finally {
|
||||
@@ -373,21 +371,10 @@ const handleLogin = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const navigateToRegister = () => {
|
||||
const pages = getCurrentPages() as any[]
|
||||
const currentPage = pages.length > 0 ? pages[pages.length - 1] : null
|
||||
const opts = currentPage?.options as any
|
||||
const redirect = opts?.redirect as string | null
|
||||
|
||||
if (redirect != null && redirect.length > 0) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/user/register?redirect=${redirect}`
|
||||
})
|
||||
} else {
|
||||
uni.navigateTo({
|
||||
url: '/pages/user/register'
|
||||
})
|
||||
}
|
||||
const navigateToRegister = (): void => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/user/register'
|
||||
})
|
||||
}
|
||||
const handleTutorial = () => uni.showToast({ title: '扫码教程开发中', icon: 'none' })
|
||||
const handleForgotPassword = () => uni.showToast({ title: '忘记密码开发中', icon: 'none' })
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,8 +18,7 @@
|
||||
<input
|
||||
type="text"
|
||||
placeholder="输入邮箱"
|
||||
:value="email"
|
||||
@input="(e: any) => email = e.detail.value"
|
||||
v-model="email"
|
||||
class="input-field"
|
||||
/>
|
||||
</view>
|
||||
@@ -32,8 +31,7 @@
|
||||
<input
|
||||
type="password"
|
||||
placeholder="填写密码"
|
||||
:value="password"
|
||||
@input="(e: any) => password = e.detail.value"
|
||||
v-model="password"
|
||||
class="input-field"
|
||||
/>
|
||||
</view>
|
||||
@@ -46,8 +44,7 @@
|
||||
<input
|
||||
type="password"
|
||||
placeholder="确认密码"
|
||||
:value="confirmPassword"
|
||||
@input="(e: any) => confirmPassword = e.detail.value"
|
||||
v-model="confirmPassword"
|
||||
class="input-field"
|
||||
/>
|
||||
</view>
|
||||
@@ -55,7 +52,7 @@
|
||||
</view>
|
||||
|
||||
<!-- 注册按钮 -->
|
||||
<view class="register-btn" @click="handleRegister" :class="{ 'disabled': isLoading }">
|
||||
<view class="register-btn" @click="handleRegister" :class="isLoading ? 'disabled' : ''">
|
||||
注册
|
||||
</view>
|
||||
|
||||
@@ -71,8 +68,7 @@
|
||||
<checkbox
|
||||
class="protocol-checkbox"
|
||||
:checked="protocol"
|
||||
:class="{ 'trembling': inAnimation }"
|
||||
@animationend="inAnimation = false"
|
||||
:class="inAnimation ? 'trembling' : ''"
|
||||
/>
|
||||
<text class="protocol-text">
|
||||
已阅读并同意
|
||||
@@ -96,7 +92,6 @@
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
import { ensureUserProfile } from '@/utils/sapi.uts'
|
||||
|
||||
// 响应式数据
|
||||
const email = ref<string>('')
|
||||
const password = ref<string>('')
|
||||
const confirmPassword = ref<string>('')
|
||||
@@ -105,22 +100,21 @@
|
||||
const isLoading = ref<boolean>(false)
|
||||
const logoUrl = ref<string>('/static/logo.png')
|
||||
|
||||
// 处理协议勾选变化
|
||||
const handleProtocolChange = (e: any) => {
|
||||
protocol.value = !protocol.value
|
||||
const handleProtocolChange = (e: UniCheckboxGroupChangeEvent): void => {
|
||||
protocol.value = protocol.value == false
|
||||
}
|
||||
|
||||
// 验证邮箱
|
||||
const validateEmail = (): boolean => {
|
||||
if (email.value.trim() === '') {
|
||||
if (email.value.trim() == '') {
|
||||
uni.showToast({
|
||||
title: '请填写邮箱',
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
}
|
||||
// 基础邮箱格式校验(足够用于前端提示)
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value)) {
|
||||
const atIndex = email.value.indexOf('@')
|
||||
const dotIndex = email.value.lastIndexOf('.')
|
||||
if (atIndex == -1 || dotIndex == -1 || atIndex > dotIndex) {
|
||||
uni.showToast({
|
||||
title: '请输入正确的邮箱',
|
||||
icon: 'none'
|
||||
@@ -130,9 +124,8 @@
|
||||
return true
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
const validatePassword = (): boolean => {
|
||||
if (password.value.trim() === '') {
|
||||
if (password.value.trim() == '') {
|
||||
uni.showToast({
|
||||
title: '请填写密码',
|
||||
icon: 'none'
|
||||
@@ -146,27 +139,18 @@
|
||||
})
|
||||
return false
|
||||
}
|
||||
// 密码不能过于简单
|
||||
if (/^([0-9]|[a-z]|[A-Z]){0,6}$/i.test(password.value)) {
|
||||
uni.showToast({
|
||||
title: '您输入的密码过于简单',
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 验证确认密码
|
||||
const validateConfirmPassword = (): boolean => {
|
||||
if (confirmPassword.value.trim() === '') {
|
||||
if (confirmPassword.value.trim() == '') {
|
||||
uni.showToast({
|
||||
title: '请确认密码',
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
}
|
||||
if (confirmPassword.value !== password.value) {
|
||||
if (confirmPassword.value != password.value) {
|
||||
uni.showToast({
|
||||
title: '两次输入的密码不一致',
|
||||
icon: 'none'
|
||||
@@ -176,10 +160,8 @@
|
||||
return true
|
||||
}
|
||||
|
||||
// 处理注册
|
||||
const handleRegister = async () => {
|
||||
// 检查协议
|
||||
if (!protocol.value) {
|
||||
const handleRegister = async (): Promise<void> => {
|
||||
if (protocol.value == false) {
|
||||
inAnimation.value = true
|
||||
uni.showToast({
|
||||
title: '请先阅读并同意协议',
|
||||
@@ -188,111 +170,85 @@
|
||||
return
|
||||
}
|
||||
|
||||
// 表单验证
|
||||
if (!validateEmail()) {
|
||||
if (validateEmail() == false) {
|
||||
return
|
||||
}
|
||||
if (!validatePassword()) {
|
||||
if (validatePassword() == false) {
|
||||
return
|
||||
}
|
||||
if (!validateConfirmPassword()) {
|
||||
if (validateConfirmPassword() == false) {
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
// 使用 Supabase Auth:邮箱 + 密码注册
|
||||
const result = await supa.signUp(email.value.trim(), password.value)
|
||||
|
||||
console.log('📝 注册返回结果:', result)
|
||||
console.log('📝 注册返回结果(JSON):', JSON.stringify(result))
|
||||
console.log('注册返回结果:', result)
|
||||
|
||||
// 检查是否有错误(邮件发送失败等)
|
||||
const errorCode = result?.getString('error_code') ?? ''
|
||||
const errorMsg = result?.getString('msg') ?? ''
|
||||
const code = result?.getNumber('code') ?? 0
|
||||
|
||||
console.log('📝 错误代码:', errorCode, '错误信息:', errorMsg, '状态码:', code)
|
||||
console.log('错误代码:', errorCode, '错误信息:', errorMsg, '状态码:', code)
|
||||
|
||||
// 如果返回 500 错误且是邮件发送失败,但用户可能已创建
|
||||
if (code === 500 && (errorCode === 'unexpected_failure' || errorMsg.includes('confirmation email'))) {
|
||||
console.warn('⚠️ 邮件发送失败,但用户可能已创建,尝试获取用户信息')
|
||||
// 即使邮件发送失败,用户可能已经在 auth.users 中创建
|
||||
// 这里我们仍然尝试创建用户资料
|
||||
if (code == 500 && (errorCode == 'unexpected_failure' || errorMsg.includes('confirmation email'))) {
|
||||
console.warn('邮件发送失败,但用户可能已创建')
|
||||
}
|
||||
|
||||
// signUp 返回的是 UTSJSONObject,Supabase signup API 返回结构:
|
||||
// { user: {...}, session: {...} } - 如果邮箱验证未开启
|
||||
// { user: {...} } - 如果邮箱验证已开启(需要验证邮箱后才能登录)
|
||||
// { code: 500, error_code: ..., msg: ... } - 如果发生错误(但用户可能已创建)
|
||||
let user: UTSJSONObject | null = null
|
||||
let hasSession = false
|
||||
|
||||
if (result != null) {
|
||||
// 尝试获取 user 字段
|
||||
const userField = result.getJSON('user')
|
||||
if (userField != null) {
|
||||
user = userField
|
||||
console.log('✅ 找到 user 字段:', user.getString('id'), user.getString('email'))
|
||||
console.log('找到 user 字段:', user.getString('id'), user.getString('email'))
|
||||
} else {
|
||||
// 如果没有 user 字段,可能 result 本身就是 user 对象
|
||||
const id = result.getString('id')
|
||||
if (id != null && id !== '') {
|
||||
if (id != null && id != '') {
|
||||
user = result
|
||||
console.log('✅ result 本身就是 user 对象:', id)
|
||||
console.log('result 本身就是 user 对象:', id)
|
||||
} else {
|
||||
// console.warn('⚠️ 未找到 user 信息,检查所有字段:', Object.keys(result.toMap() || {}))
|
||||
console.warn('⚠️ 未找到 user 信息,检查所有字段')
|
||||
console.warn('未找到 user 信息')
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否有 session(表示注册后自动登录成功)
|
||||
const sessionField = result.getJSON('session')
|
||||
if (sessionField != null) {
|
||||
hasSession = true
|
||||
console.log('✅ 找到 session,已自动登录')
|
||||
// 如果有 session,说明已经自动登录,token 应该已经设置
|
||||
// 此时可以直接创建用户资料
|
||||
console.log('找到 session,已自动登录')
|
||||
} else {
|
||||
console.log('ℹ️ 未找到 session,可能需要邮箱验证')
|
||||
console.log('未找到 session,可能需要邮箱验证')
|
||||
}
|
||||
}
|
||||
|
||||
// 如果返回错误且没有用户信息,说明注册失败
|
||||
if (user == null && code !== 0 && code !== 200) {
|
||||
// 如果是邮件发送失败,给出明确的错误提示
|
||||
if (code === 500 && errorMsg.includes('confirmation email')) {
|
||||
throw new Error('注册失败:邮件服务配置错误,请联系管理员或修改 Supabase 配置(设置 ENABLE_EMAIL_AUTOCONFIRM=true)')
|
||||
if (user == null && code != 0 && code != 200) {
|
||||
if (code == 500 && errorMsg.includes('confirmation email')) {
|
||||
throw new Error('注册失败:邮件服务配置错误')
|
||||
} else {
|
||||
throw new Error(errorMsg != '' ? errorMsg : '注册失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 如果获取到 user,尝试创建业务侧用户资料(ak_users)
|
||||
if (user != null) {
|
||||
try {
|
||||
const profileResult = await ensureUserProfile(user)
|
||||
if (profileResult != null) {
|
||||
console.log('✅ 用户资料创建成功:', profileResult.id)
|
||||
console.log('用户资料创建成功:', profileResult.id)
|
||||
} else {
|
||||
console.warn('⚠️ 用户资料创建失败,但注册已成功')
|
||||
// 如果创建失败,可能是因为 RLS 策略限制
|
||||
// 建议用户登录后再自动创建(在 getCurrentUser 中处理)
|
||||
console.warn('用户资料创建失败,但注册已成功')
|
||||
}
|
||||
} catch (profileError) {
|
||||
console.error('❌ 创建用户资料异常:', profileError)
|
||||
// 即使创建资料失败,也不阻止注册流程
|
||||
// 用户登录时会自动创建(见 utils/store.uts 的 getCurrentUser)
|
||||
console.error('创建用户资料异常:', profileError)
|
||||
}
|
||||
} else {
|
||||
console.warn('⚠️ 注册成功但未获取到用户信息')
|
||||
// 可能需要邮箱验证,用户验证邮箱后登录时会自动创建资料
|
||||
console.warn('注册成功但未获取到用户信息')
|
||||
}
|
||||
|
||||
// 如果注册后没有自动登录(需要邮箱验证),提示用户
|
||||
if (!hasSession && user != null) {
|
||||
console.log('ℹ️ 需要邮箱验证,验证后登录时会自动创建用户资料')
|
||||
if (hasSession == false && user != null) {
|
||||
console.log('需要邮箱验证')
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
@@ -309,11 +265,10 @@
|
||||
console.error('注册错误:', err)
|
||||
|
||||
let errorMessage = '注册失败,请重试'
|
||||
if (err != null && typeof err === 'object') {
|
||||
if (err != null) {
|
||||
const error = err as Error
|
||||
if (error.message != null && error.message.trim() !== '') {
|
||||
if (error.message != null && error.message.trim() != '') {
|
||||
errorMessage = error.message
|
||||
// 如果是邮件发送失败,给出更友好的提示
|
||||
if (error.message.includes('confirmation email') || error.message.includes('邮件')) {
|
||||
errorMessage = '注册可能成功,但邮件发送失败,请稍后尝试登录'
|
||||
}
|
||||
@@ -330,26 +285,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转到登录页
|
||||
const navigateToLogin = () => {
|
||||
const pages = getCurrentPages() as any[]
|
||||
const currentPage = pages.length > 0 ? pages[pages.length - 1] : null
|
||||
const opts = currentPage?.options as any
|
||||
const redirect = opts?.redirect as string | null
|
||||
|
||||
if (redirect != null && redirect.length > 0) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/user/login?redirect=${redirect}`
|
||||
})
|
||||
} else {
|
||||
uni.navigateTo({
|
||||
url: '/pages/user/login'
|
||||
})
|
||||
}
|
||||
const navigateToLogin = (): void => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/user/login'
|
||||
})
|
||||
}
|
||||
|
||||
// 跳转到协议页面
|
||||
const navigateToTerms = (type: number) => {
|
||||
const navigateToTerms = (type: number): void => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/user/terms?type=${type}`
|
||||
})
|
||||
@@ -357,10 +299,6 @@
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* page {
|
||||
background: #F5F5F5;
|
||||
} */
|
||||
|
||||
.register-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
@@ -368,7 +306,6 @@
|
||||
background: #F5F5F5;
|
||||
}
|
||||
|
||||
/* Header Logo */
|
||||
.header {
|
||||
padding: 40rpx 0 0 60rpx;
|
||||
background: #F5F5F5;
|
||||
@@ -379,7 +316,6 @@
|
||||
height: 80rpx;
|
||||
}
|
||||
|
||||
/* 注册表单区域 */
|
||||
.register-box {
|
||||
flex: 1;
|
||||
background: #FFFFFF;
|
||||
@@ -397,7 +333,6 @@
|
||||
margin-bottom: 50rpx;
|
||||
}
|
||||
|
||||
/* 表单内容 */
|
||||
.form-content {
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
@@ -417,10 +352,6 @@
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
.input-wrapper:focus-within {
|
||||
border-color: var(--view-theme, #FF4D4F);
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
@@ -435,28 +366,6 @@
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.code-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.code-btn {
|
||||
position: absolute;
|
||||
right: 20rpx;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--view-theme, #FF4D4F);
|
||||
font-size: 26rpx;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.code-btn.disabled {
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
/* 注册按钮 */
|
||||
.register-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -478,7 +387,6 @@
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* 已有账号提示 */
|
||||
.tips {
|
||||
margin-top: 30rpx;
|
||||
text-align: center;
|
||||
@@ -491,11 +399,10 @@
|
||||
|
||||
.tips-link {
|
||||
font-size: 28rpx;
|
||||
color: var(--view-theme, #FF4D4F);
|
||||
color: #FF4D4F;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
/* 协议区域 */
|
||||
.protocol {
|
||||
margin-top: 40rpx;
|
||||
display: flex;
|
||||
@@ -513,22 +420,9 @@
|
||||
}
|
||||
|
||||
.main-color {
|
||||
color: var(--view-theme, #FF4D4F);
|
||||
color: #FF4D4F;
|
||||
}
|
||||
|
||||
/*
|
||||
.trembling {
|
||||
animation: shake 0.6s;
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
10%, 30%, 50%, 70%, 90% { transform: translateX(-10rpx); }
|
||||
20%, 40%, 60%, 80% { transform: translateX(10rpx); }
|
||||
}
|
||||
*/
|
||||
|
||||
/* 底部版权 */
|
||||
.footer {
|
||||
padding: 40rpx 0;
|
||||
text-align: center;
|
||||
|
||||
685
pagesbackup.json
Normal file
685
pagesbackup.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -398,3 +398,16 @@ export type UserStats = {
|
||||
points: number;
|
||||
streak: number;
|
||||
}
|
||||
|
||||
// 足迹项类型
|
||||
export type FootprintItemType = {
|
||||
id: string
|
||||
name: string
|
||||
price: number
|
||||
original_price: number | null
|
||||
image: string
|
||||
sales: number
|
||||
shopId: string
|
||||
shopName: string
|
||||
viewTime: number
|
||||
}
|
||||
|
||||
BIN
unpackage/cache/.app-android/class/META-INF/main-1772066221482.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1772066221482.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/META-INF/main-1772066414372.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1772066414372.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/META-INF/main-1772066876405.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1772066876405.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/META-INF/main-1772070790021.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1772070790021.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/META-INF/main-1772072606720.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1772072606720.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/META-INF/main-1772072780865.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1772072780865.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/META-INF/main-1772073388422.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1772073388422.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/META-INF/main-1772073423198.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1772073423198.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/META-INF/main-1772073493212.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1772073493212.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/META-INF/main-1772073946458.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1772073946458.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/META-INF/main-1772074244042.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1772074244042.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/META-INF/main-1772119086483.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1772119086483.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/META-INF/main-1772119115576.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1772119115576.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/META-INF/main-1772119142877.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1772119142877.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/META-INF/main-1772119184739.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1772119184739.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/META-INF/main-1772119966521.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1772119966521.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/META-INF/main-1772123281315.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1772123281315.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/META-INF/main-1772150983319.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1772150983319.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/ktClasss.ser
vendored
Normal file
BIN
unpackage/cache/.app-android/class/ktClasss.ser
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AddAddressParams.class
vendored
Normal file
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AddAddressParams.class
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/Address.class
vendored
Normal file
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/Address.class
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AddressForm.class
vendored
Normal file
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AddressForm.class
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AddressFormReactiveObject.class
vendored
Normal file
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AddressFormReactiveObject.class
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AddressItem.class
vendored
Normal file
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AddressItem.class
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AddressItemReactiveObject.class
vendored
Normal file
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AddressItemReactiveObject.class
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AddressReactiveObject.class
vendored
Normal file
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AddressReactiveObject.class
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AddressType.class
vendored
Normal file
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AddressType.class
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AddressTypeReactiveObject.class
vendored
Normal file
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AddressTypeReactiveObject.class
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/Address__1.class
vendored
Normal file
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/Address__1.class
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AkReq$Companion$refreshTokenIfNeeded$1$1.class
vendored
Normal file
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AkReq$Companion$refreshTokenIfNeeded$1$1.class
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AkReq$Companion$refreshTokenIfNeeded$1$res$1.class
vendored
Normal file
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AkReq$Companion$refreshTokenIfNeeded$1$res$1.class
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AkReq$Companion$refreshTokenIfNeeded$1.class
vendored
Normal file
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AkReq$Companion$refreshTokenIfNeeded$1.class
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AkReq$Companion$request$1$1.class
vendored
Normal file
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AkReq$Companion$request$1$1.class
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AkReq$Companion$request$1$2.class
vendored
Normal file
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AkReq$Companion$request$1$2.class
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AkReq$Companion$request$1$3.class
vendored
Normal file
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AkReq$Companion$request$1$3.class
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AkReq$Companion$request$1$doOnce$1$1$1$1.class
vendored
Normal file
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AkReq$Companion$request$1$doOnce$1$1$1$1.class
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AkReq$Companion$request$1.class
vendored
Normal file
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AkReq$Companion$request$1.class
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AkReq$Companion$requestAs$1$aaa$1.class
vendored
Normal file
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AkReq$Companion$requestAs$1$aaa$1.class
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AkReq$Companion$requestAs$1$genericTypeT$1.class
vendored
Normal file
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AkReq$Companion$requestAs$1$genericTypeT$1.class
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AkReq$Companion$requestAs$1.class
vendored
Normal file
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AkReq$Companion$requestAs$1.class
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AkReq$Companion$upload$1$1.class
vendored
Normal file
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AkReq$Companion$upload$1$1.class
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AkReq$Companion$upload$1$2.class
vendored
Normal file
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AkReq$Companion$upload$1$2.class
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AkReq$Companion$upload$1.class
vendored
Normal file
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AkReq$Companion$upload$1.class
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AkReq$Companion.class
vendored
Normal file
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AkReq$Companion.class
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AkReq.class
vendored
Normal file
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AkReq.class
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AkReqOptions.class
vendored
Normal file
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AkReqOptions.class
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AkReqResponse.class
vendored
Normal file
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AkReqResponse.class
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AkReqUploadOptions.class
vendored
Normal file
BIN
unpackage/cache/.app-android/class/uni/UNIEC68BC3/AkReqUploadOptions.class
vendored
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user