consumer模块完成度95%,能编译在安卓端运行,在解决数据获取和页面布局问题
This commit is contained in:
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()
|
||||
|
||||
const keys = UTSJSONObject.keys(o)
|
||||
for (let j: number = 0; j < keys.length; j++) {
|
||||
const key = keys[j]
|
||||
orderObj.set(key, o.get(key))
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// 按时间倒序 (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
|
||||
})
|
||||
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.recentOrders = this.allOrders.slice(0, 5)
|
||||
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,9 +736,7 @@ const goBack = () => {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
margin-right: 10px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
|
||||
.upload-btn {
|
||||
width: 70px;
|
||||
height: 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,53 +260,64 @@ 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()
|
||||
cacheSize.value = '12.5 MB'
|
||||
|
||||
// 获取应用版本
|
||||
// @ts-ignore
|
||||
const appInfo = uni.getAppBaseInfo()
|
||||
if (appInfo?.appVersion) {
|
||||
appVersion.value = appInfo.appVersion
|
||||
if (appInfo != null) {
|
||||
const infoObj = appInfo as UTSJSONObject
|
||||
const version = infoObj.getString('appVersion')
|
||||
if (version != null) {
|
||||
appVersion.value = version
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 计算缓存大小
|
||||
const calculateCacheSize = () => {
|
||||
// 这里应该计算实际缓存大小,这里使用模拟数据
|
||||
cacheSize.value = '12.5 MB'
|
||||
}
|
||||
onMounted(() => {
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
statusBarHeight.value = systemInfo.statusBarHeight ?? 0
|
||||
loadUserInfo()
|
||||
loadSettings()
|
||||
})
|
||||
|
||||
// 跳转到个人资料
|
||||
const goToProfile = () => {
|
||||
@@ -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()
|
||||
|
||||
if (error !== null) {
|
||||
console.error('登出失败:', error)
|
||||
// 即使失败也继续清除本地状态
|
||||
}
|
||||
|
||||
// 清除本地存储的用户信息
|
||||
uni.removeStorageSync('userInfo')
|
||||
uni.removeStorageSync('user_id')
|
||||
uni.removeStorageSync('access_token')
|
||||
|
||||
uni.hideLoading()
|
||||
|
||||
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')
|
||||
uni.showLoading({
|
||||
title: '正在退出...'
|
||||
})
|
||||
|
||||
uni.removeStorageSync('userInfo')
|
||||
uni.removeStorageSync('user_id')
|
||||
uni.removeStorageSync('access_token')
|
||||
|
||||
uni.hideLoading()
|
||||
|
||||
uni.showToast({
|
||||
title: '已退出登录',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
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> = []
|
||||
|
||||
for (let idx: number = 0; idx < rawList.length; idx++) {
|
||||
const item = rawList[idx] as UTSJSONObject
|
||||
const images: Array<string> = []
|
||||
|
||||
// 注意:类型转换,UTS中 any 到具体的类型转换
|
||||
// 1. 尝试 main_image_url
|
||||
if (item.main_image_url != null && item.main_image_url !== '') {
|
||||
images.push(item.main_image_url!)
|
||||
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>
|
||||
@@ -217,4 +218,4 @@ const goHome = () => {
|
||||
padding-top: 50px;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
</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) {
|
||||
for (let i: number = 0; i < mappedData.length; i++) {
|
||||
transactions.value.push(mappedData[i])
|
||||
}
|
||||
currentPage.value = page
|
||||
} else {
|
||||
transactions.value = mappedData
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
hasMore.value = mappedData.length >= limit
|
||||
} catch (err) {
|
||||
console.error('加载交易记录失败:', err)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
if (loadMore) {
|
||||
transactions.value.push(...mappedData)
|
||||
} else {
|
||||
transactions.value = mappedData
|
||||
}
|
||||
|
||||
if (mappedData.length < limit) {
|
||||
hasMore.value = false
|
||||
} else {
|
||||
hasMore.value = true
|
||||
}
|
||||
|
||||
currentPage.value = page
|
||||
} 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 {
|
||||
|
||||
Reference in New Issue
Block a user