consumer模块完成度95%,能编译在安卓端运行,在解决数据获取和页面布局问题

This commit is contained in:
cyh666666
2026-02-27 08:20:43 +08:00
parent e606c597ca
commit b9acce6c35
1554 changed files with 23471 additions and 8551 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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: ''
}

View File

@@ -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>

View File

@@ -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 != ''
})

View File

@@ -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

View File

@@ -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)}&timestamp=${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;

View File

@@ -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>

View File

@@ -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 => {

View File

@@ -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
},

View File

@@ -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'

View File

@@ -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'

View File

@@ -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)
}
}
})

View File

@@ -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;

View File

@@ -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)

View File

@@ -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)
}
}
})

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 {