完善下单逻辑及其ui展示,修复支付倒计时显示错误bug

This commit is contained in:
2026-05-25 15:35:41 +08:00
parent d25f80ccdd
commit cecb51a8e2
40 changed files with 13040 additions and 3217 deletions

View File

@@ -0,0 +1,461 @@
{
"pages": [
{
"path": "pages/main/index",
"style": {
"navigationBarTitleText": "首页",
"navigationStyle": "custom",
"enablePullDownRefresh": false
}
},
{
"path": "pages/user/boot",
"style": {
"navigationBarTitleText": ""
}
},
{
"path": "pages/user/login",
"style": {
"navigationBarTitleText": "用户登录",
"navigationStyle": "custom"
}
},
{
"path": "pages/user/register",
"style": {
"navigationBarTitleText": "注册"
}
},
{
"path": "pages/user/forgot-password",
"style": {
"navigationBarTitleText": "忘记密码"
}
},
{
"path": "pages/user/terms",
"style": {
"navigationBarTitleText": "用户协议与隐私政策"
}
},
{
"path": "pages/user/center",
"style": {
"navigationBarTitleText": "用户中心"
}
},
{
"path": "pages/user/profile",
"style": {
"navigationBarTitleText": "个人资料"
}
},
{
"path": "pages/user/change-password",
"style": {
"navigationBarTitleText": "修改密码"
}
},
{
"path": "pages/user/bind-phone",
"style": {
"navigationBarTitleText": "绑定手机"
}
},
{
"path": "pages/user/bind-email",
"style": {
"navigationBarTitleText": "绑定邮箱"
}
},
{
"path": "pages/main/messages",
"style": {
"navigationBarTitleText": "消息",
"enablePullDownRefresh": true
}
},
{
"path": "pages/main/cart",
"style": {
"navigationBarTitleText": "购物车",
"navigationStyle": "custom"
}
},
{
"path": "pages/main/profile",
"style": {
"navigationBarTitleText": "我的",
"navigationStyle": "custom"
}
},
{
"path": "pages/main/category",
"style": {
"navigationBarTitleText": "分类",
"navigationStyle": "custom"
}
}
],
"subPackages": [
{
"root": "pages/mall/consumer",
"pages": [
{
"path": "settings",
"style": {
"navigationBarTitleText": "设置"
}
},
{
"path": "edit-profile",
"style": {
"navigationBarTitleText": "编辑资料"
}
},
{
"path": "wallet",
"style": {
"navigationBarTitleText": "我的钱包"
}
},
{
"path": "withdraw",
"style": {
"navigationBarTitleText": "余额提现"
}
},
{
"path": "search",
"style": {
"navigationBarTitleText": "搜索",
"navigationStyle": "custom"
}
},
{
"path": "product-detail",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"path": "channel-detail",
"style": {
"navigationBarTitleText": "频道详情",
"navigationStyle": "custom"
}
},
{
"path": "shop-detail",
"style": {
"navigationBarTitleText": "店铺详情"
}
},
{
"path": "coupons",
"style": {
"navigationBarTitleText": "我的优惠券"
}
},
{
"path": "favorites",
"style": {
"navigationBarTitleText": "我的收藏"
}
},
{
"path": "footprint",
"style": {
"navigationBarTitleText": "我的足迹"
}
},
{
"path": "address",
"style": {
"navigationBarTitleText": "地址"
}
},
{
"path": "address-list",
"style": {
"navigationBarTitleText": "收货地址"
}
},
{
"path": "address-edit",
"style": {
"navigationBarTitleText": "编辑地址"
}
},
{
"path": "checkout",
"style": {
"navigationBarTitleText": "确认订单"
}
},
{
"path": "payment",
"style": {
"navigationBarTitleText": "收银台"
}
},
{
"path": "payment-success",
"style": {
"navigationBarTitleText": "支付成功",
"navigationStyle": "custom"
}
},
{
"path": "orders",
"style": {
"navigationBarTitleText": "我的订单",
"enablePullDownRefresh": true
}
},
{
"path": "order-detail",
"style": {
"navigationBarTitleText": "订单详情"
}
},
{
"path": "logistics",
"style": {
"navigationBarTitleText": "物流详情"
}
},
{
"path": "review",
"style": {
"navigationBarTitleText": "评价晒单"
}
},
{
"path": "refund",
"style": {
"navigationBarTitleText": "退款/售后"
}
},
{
"path": "apply-refund",
"style": {
"navigationBarTitleText": "申请售后"
}
},
{
"path": "refund-review",
"style": {
"navigationBarTitleText": "服务评价"
}
},
{
"path": "chat",
"style": {
"navigationBarTitleText": "客服聊天",
"navigationStyle": "custom"
}
},
{
"path": "chat_new",
"style": {
"navigationBarTitleText": "客服聊天(新版)"
}
},
{
"path": "subscription/plan-list",
"style": {
"navigationBarTitleText": "软件订阅"
}
},
{
"path": "subscription/plan-detail",
"style": {
"navigationBarTitleText": "订阅详情"
}
},
{
"path": "subscription/subscribe-checkout",
"style": {
"navigationBarTitleText": "确认订阅"
}
},
{
"path": "subscription/my-subscriptions",
"style": {
"navigationBarTitleText": "我的订阅"
}
},
{
"path": "subscription/followed-shops",
"style": {
"navigationBarTitleText": "关注店铺"
}
},
{
"path": "points/index",
"style": {
"navigationBarTitleText": "积分管理"
}
},
{
"path": "points/signin",
"style": {
"navigationBarTitleText": "签到"
}
},
{
"path": "points/exchange",
"style": {
"navigationBarTitleText": "积分兑换"
}
},
{
"path": "points/exchange-records",
"style": {
"navigationBarTitleText": "兑换记录"
}
},
{
"path": "red-packets/index",
"style": {
"navigationBarTitleText": "我的红包"
}
},
{
"path": "bank-cards/index",
"style": {
"navigationBarTitleText": "银行卡管理"
}
},
{
"path": "bank-cards/add",
"style": {
"navigationBarTitleText": "添加银行卡"
}
},
{
"path": "home-service/index",
"style": {
"navigationBarTitleText": "居家上门服务",
"navigationStyle": "custom"
}
},
{
"path": "home-service/apply",
"style": {
"navigationBarTitleText": "提交服务申请",
"navigationStyle": "custom"
}
},
{
"path": "home-service/service-detail",
"style": {
"navigationBarTitleText": "预约服务",
"navigationStyle": "custom"
}
},
{
"path": "home-service/order-detail",
"style": {
"navigationBarTitleText": "服务单详情",
"navigationStyle": "custom"
}
},
{
"path": "home-service/feedback",
"style": {
"navigationBarTitleText": "验收反馈",
"navigationStyle": "custom"
}
},
{
"path": "bank-cards/verify",
"style": {
"navigationBarTitleText": "银行卡验证"
}
},
{
"path": "balance/index",
"style": {
"navigationBarTitleText": "余额"
}
},
{
"path": "my-reviews",
"style": {
"navigationBarTitleText": "我的评价"
}
},
{
"path": "message-detail",
"style": {
"navigationBarTitleText": "消息详情"
}
},
{
"path": "member/index",
"style": {
"navigationBarTitleText": "会员中心"
}
},
{
"path": "product-reviews",
"style": {
"navigationBarTitleText": "商品评价"
}
}
]
}
],
"tabBar": {
"color": "#999999",
"selectedColor": "#ff5000",
"backgroundColor": "#ffffff",
"borderStyle": "black",
"list": [
{
"pagePath": "pages/main/index",
"text": "首页",
"iconPath": "static/tabbar/home.png",
"selectedIconPath": "static/tabbar/home-active.png"
},
{
"pagePath": "pages/main/messages",
"text": "消息",
"iconPath": "static/tabbar/message.png",
"selectedIconPath": "static/tabbar/message.png"
},
{
"pagePath": "pages/main/cart",
"text": "购物车",
"iconPath": "static/tabbar/cart.png",
"selectedIconPath": "static/tabbar/cart.png"
},
{
"pagePath": "pages/main/profile",
"text": "我的",
"iconPath": "static/tabbar/user.png",
"selectedIconPath": "static/tabbar/user.png"
}
]
},
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "mall",
"navigationBarBackgroundColor": "#FFFFFF",
"backgroundColor": "#F8F8F8"
},
"condition": {
"current": 0,
"list": [
{
"name": "consumer端",
"path": "pages/main/index",
"query": "role=consumer"
}
]
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,464 @@
{
"pages": [
{
"path": "pages/main/index",
"style": {
"navigationBarTitleText": "首页",
"navigationStyle": "custom",
"enablePullDownRefresh": false
}
},
{
"path": "pages/user/boot",
"style": {
"navigationBarTitleText": ""
}
},
{
"path": "pages/user/login",
"style": {
"navigationBarTitleText": "用户登录",
"navigationStyle": "custom"
}
},
{
"path": "pages/user/register",
"style": {
"navigationBarTitleText": "注册"
}
},
{
"path": "pages/user/forgot-password",
"style": {
"navigationBarTitleText": "忘记密码"
}
},
{
"path": "pages/user/terms",
"style": {
"navigationBarTitleText": "用户协议与隐私政策"
}
},
{
"path": "pages/user/center",
"style": {
"navigationBarTitleText": "用户中心"
}
},
{
"path": "pages/user/profile",
"style": {
"navigationBarTitleText": "个人资料"
}
},
{
"path": "pages/user/change-password",
"style": {
"navigationBarTitleText": "修改密码"
}
},
{
"path": "pages/user/bind-phone",
"style": {
"navigationBarTitleText": "绑定手机"
}
},
{
"path": "pages/user/bind-email",
"style": {
"navigationBarTitleText": "绑定邮箱"
}
},
{
"path": "pages/main/messages",
"style": {
"navigationBarTitleText": "消息",
"enablePullDownRefresh": true
}
},
{
"path": "pages/main/cart",
"style": {
"navigationBarTitleText": "购物车",
"navigationStyle": "custom"
}
},
{
"path": "pages/main/profile",
"style": {
"navigationBarTitleText": "我的",
"navigationStyle": "custom"
}
},
{
"path": "pages/main/category",
"style": {
"navigationBarTitleText": "分类",
"navigationStyle": "custom"
}
}
],
"subPackages": [
{
"root": "pages/mall/consumer",
"pages": [
{
"path": "settings",
"style": {
"navigationBarTitleText": "设置"
}
},
{
"path": "edit-profile",
"style": {
"navigationBarTitleText": "编辑资料"
}
},
{
"path": "wallet",
"style": {
"navigationBarTitleText": "我的钱包"
}
},
{
"path": "withdraw",
"style": {
"navigationBarTitleText": "余额提现"
}
},
{
"path": "search",
"style": {
"navigationBarTitleText": "搜索",
"navigationStyle": "custom"
}
},
{
"path": "product-detail",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"path": "channel-detail",
"style": {
"navigationBarTitleText": "频道详情",
"navigationStyle": "custom"
}
},
{
"path": "shop-detail",
"style": {
"navigationBarTitleText": "店铺详情"
}
},
{
"path": "coupons",
"style": {
"navigationBarTitleText": "我的优惠券"
}
},
{
"path": "favorites",
"style": {
"navigationBarTitleText": "我的收藏"
}
},
{
"path": "footprint",
"style": {
"navigationBarTitleText": "我的足迹"
}
},
{
"path": "address",
"style": {
"navigationBarTitleText": "地址"
}
},
{
"path": "address-list",
"style": {
"navigationBarTitleText": "收货地址"
}
},
{
"path": "address-edit",
"style": {
"navigationBarTitleText": "编辑地址"
}
},
{
"path": "checkout",
"style": {
"navigationBarTitleText": "确认订单"
}
},
{
"path": "payment",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"path": "payment-success",
"style": {
"navigationBarTitleText": "支付成功",
"navigationStyle": "custom"
}
},
{
"path": "orders",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom",
"enablePullDownRefresh": true,
"backgroundColor": "#f5f5f5"
}
},
{
"path": "order-detail",
"style": {
"navigationBarTitleText": "订单详情"
}
},
{
"path": "logistics",
"style": {
"navigationBarTitleText": "物流详情"
}
},
{
"path": "review",
"style": {
"navigationBarTitleText": "评价晒单"
}
},
{
"path": "refund",
"style": {
"navigationBarTitleText": "退款/售后"
}
},
{
"path": "apply-refund",
"style": {
"navigationBarTitleText": "申请售后"
}
},
{
"path": "refund-review",
"style": {
"navigationBarTitleText": "服务评价"
}
},
{
"path": "chat",
"style": {
"navigationBarTitleText": "客服聊天",
"navigationStyle": "custom"
}
},
{
"path": "chat_new",
"style": {
"navigationBarTitleText": "客服聊天(新版)"
}
},
{
"path": "subscription/plan-list",
"style": {
"navigationBarTitleText": "软件订阅"
}
},
{
"path": "subscription/plan-detail",
"style": {
"navigationBarTitleText": "订阅详情"
}
},
{
"path": "subscription/subscribe-checkout",
"style": {
"navigationBarTitleText": "确认订阅"
}
},
{
"path": "subscription/my-subscriptions",
"style": {
"navigationBarTitleText": "我的订阅"
}
},
{
"path": "subscription/followed-shops",
"style": {
"navigationBarTitleText": "关注店铺"
}
},
{
"path": "points/index",
"style": {
"navigationBarTitleText": "积分管理"
}
},
{
"path": "points/signin",
"style": {
"navigationBarTitleText": "签到"
}
},
{
"path": "points/exchange",
"style": {
"navigationBarTitleText": "积分兑换"
}
},
{
"path": "points/exchange-records",
"style": {
"navigationBarTitleText": "兑换记录"
}
},
{
"path": "red-packets/index",
"style": {
"navigationBarTitleText": "我的红包"
}
},
{
"path": "bank-cards/index",
"style": {
"navigationBarTitleText": "银行卡管理"
}
},
{
"path": "bank-cards/add",
"style": {
"navigationBarTitleText": "添加银行卡"
}
},
{
"path": "home-service/index",
"style": {
"navigationBarTitleText": "居家上门服务",
"navigationStyle": "custom"
}
},
{
"path": "home-service/apply",
"style": {
"navigationBarTitleText": "提交服务申请",
"navigationStyle": "custom"
}
},
{
"path": "home-service/service-detail",
"style": {
"navigationBarTitleText": "预约服务",
"navigationStyle": "custom"
}
},
{
"path": "home-service/order-detail",
"style": {
"navigationBarTitleText": "服务单详情",
"navigationStyle": "custom"
}
},
{
"path": "home-service/feedback",
"style": {
"navigationBarTitleText": "验收反馈",
"navigationStyle": "custom"
}
},
{
"path": "bank-cards/verify",
"style": {
"navigationBarTitleText": "银行卡验证"
}
},
{
"path": "balance/index",
"style": {
"navigationBarTitleText": "余额"
}
},
{
"path": "my-reviews",
"style": {
"navigationBarTitleText": "我的评价"
}
},
{
"path": "message-detail",
"style": {
"navigationBarTitleText": "消息详情"
}
},
{
"path": "member/index",
"style": {
"navigationBarTitleText": "会员中心"
}
},
{
"path": "product-reviews",
"style": {
"navigationBarTitleText": "商品评价"
}
}
]
}
],
"tabBar": {
"color": "#999999",
"selectedColor": "#ff5000",
"backgroundColor": "#ffffff",
"borderStyle": "black",
"list": [
{
"pagePath": "pages/main/index",
"text": "首页",
"iconPath": "static/tabbar/home.png",
"selectedIconPath": "static/tabbar/home-active.png"
},
{
"pagePath": "pages/main/messages",
"text": "消息",
"iconPath": "static/tabbar/message.png",
"selectedIconPath": "static/tabbar/message.png"
},
{
"pagePath": "pages/main/cart",
"text": "购物车",
"iconPath": "static/tabbar/cart.png",
"selectedIconPath": "static/tabbar/cart.png"
},
{
"pagePath": "pages/main/profile",
"text": "我的",
"iconPath": "static/tabbar/user.png",
"selectedIconPath": "static/tabbar/user.png"
}
]
},
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "mall",
"navigationBarBackgroundColor": "#FFFFFF",
"backgroundColor": "#F8F8F8"
},
"condition": {
"current": 0,
"list": [
{
"name": "consumer端",
"path": "pages/main/index",
"query": "role=consumer"
}
]
}
}

View File

@@ -52,13 +52,13 @@
:key="product.id + '-' + pIndex" :key="product.id + '-' + pIndex"
class="hmall-recommend-product" class="hmall-recommend-product"
> >
<image class="hmall-recommend-product-image" :src="getChannelProductImage(product)" mode="aspectFill" /> <image class="hmall-recommend-product-image" :src="getChannelProductImage(product)" mode="aspectFill" @error="handleChannelProductImageError(product.id)" />
<text class="hmall-recommend-product-name">{{ product.shortName }}</text> <text class="hmall-recommend-product-name">{{ getChannelProductTitle(product) }}</text>
<view class="hmall-recommend-price-row"> <view class="hmall-recommend-price-row">
<text class="hmall-recommend-product-tag" :style="{ color: channel.themeColor }">{{ product.tag }}</text> <text class="hmall-recommend-product-tag" :style="{ color: channel.themeColor }">{{ getChannelProductTag(product) }}</text>
<text class="hmall-recommend-product-price" :style="{ color: channel.themeColor }">¥{{ formatChannelPrice(product.price) }}</text> <text class="hmall-recommend-product-price" :style="{ color: channel.themeColor }">¥{{ formatChannelPrice(getChannelSalePrice(product)) }}</text>
</view> </view>
<text v-if="product.marketPrice > product.price" class="hmall-recommend-market-price">¥{{ formatChannelPrice(product.marketPrice) }}</text> <text v-if="showChannelMarketPrice(product)" class="hmall-recommend-market-price">¥{{ formatChannelPrice(getChannelMarketPrice(product)) }}</text>
</view> </view>
</view> </view>
</view> </view>
@@ -167,6 +167,7 @@ import type { Category, Product } from '@/utils/supabaseService.uts'
import type { MarketingChannel, ChannelProduct, SimpleCategoryChannel } from '@/utils/mockChannelData.uts' import type { MarketingChannel, ChannelProduct, SimpleCategoryChannel } from '@/utils/mockChannelData.uts'
const failedProductImageIds = ref<string[]>([]) const failedProductImageIds = ref<string[]>([])
const failedChannelImageIds = ref<string[]>([])
type SecondaryCategoryPage = { type SecondaryCategoryPage = {
id: string id: string
@@ -266,9 +267,50 @@ function getCategoryDisplayIcon(category: Category): string {
} }
function getChannelProductImage(product: ChannelProduct): string { function getChannelProductImage(product: ChannelProduct): string {
if (failedChannelImageIds.value.indexOf(product.id) != -1) {
return '/static/images/default.png'
}
return product.image != '' ? product.image : '/static/images/default.png' return product.image != '' ? product.image : '/static/images/default.png'
} }
function handleChannelProductImageError(productId: string): void {
if (productId == '') {
return
}
if (failedChannelImageIds.value.indexOf(productId) == -1) {
failedChannelImageIds.value.push(productId)
}
}
function getChannelProductTitle(product: ChannelProduct): string {
if (product.shortName != null && product.shortName != '') {
return product.shortName
}
if (product.name != null && product.name != '') {
return product.name
}
return '商品补充中'
}
function getChannelProductTag(product: ChannelProduct): string {
if (product.tag != null && product.tag != '') {
return product.tag
}
return '活动价'
}
function getChannelSalePrice(product: ChannelProduct): number {
return product.price > 0 ? product.price : 0
}
function getChannelMarketPrice(product: ChannelProduct): number {
return product.marketPrice > 0 ? product.marketPrice : 0
}
function showChannelMarketPrice(product: ChannelProduct): boolean {
return getChannelMarketPrice(product) > getChannelSalePrice(product)
}
function formatChannelPrice(price: number): string { function formatChannelPrice(price: number): string {
const rounded = Math.round(price) const rounded = Math.round(price)
if (Math.abs(price - rounded) < 0.001) { if (Math.abs(price - rounded) < 0.001) {

View File

@@ -0,0 +1,363 @@
<template>
<view class="guess-you-like-section">
<view v-if="showTitle" class="guess-header">
<text class="guess-title">{{ titleText }}</text>
</view>
<view v-if="products.length > 0" class="guess-grid">
<view
v-for="product in products"
:key="product.id"
class="guess-card"
@click="handleProductClick(product.id)"
>
<image class="guess-image" :src="product.image" mode="aspectFill" />
<view class="guess-body">
<text class="guess-name" :lines="2">{{ product.name }}</text>
<view class="guess-price-row">
<text class="guess-price">¥{{ product.price.toFixed(2) }}</text>
<text v-if="product.originalPrice > product.price" class="guess-original-price">¥{{ product.originalPrice.toFixed(2) }}</text>
</view>
<text v-if="product.salesText !== ''" class="guess-sales">{{ product.salesText }}</text>
</view>
</view>
</view>
<view v-else-if="!loading && errorText === ''" class="guess-empty">
<text class="guess-empty-text">暂无推荐商品</text>
</view>
<view v-if="loading" class="guess-status-row">
<text class="guess-status-text">加载中...</text>
</view>
<view v-else-if="errorText !== ''" class="guess-status-row">
<text class="guess-status-text">{{ errorText }}</text>
</view>
<view v-else-if="finished && products.length > 0" class="guess-status-row">
<text class="guess-status-text">没有更多了</text>
</view>
</view>
</template>
<script setup lang="uts">
import { computed, onMounted, ref, watch } from 'vue'
import { supabaseService } from '@/utils/supabaseService.uts'
type RecommendProduct = {
id: string
name: string
image: string
price: number
originalPrice: number
salesText: string
categoryId: string
}
const DEFAULT_IMAGE = '/static/logo.png'
const props = defineProps({
title: {
type: String,
default: '猜你喜欢'
},
pageSize: {
type: Number,
default: 8
},
categoryId: {
type: String,
default: ''
},
excludeProductIds: {
type: Array,
default: () => []
},
loadMoreKey: {
type: Number,
default: 0
},
source: {
type: String,
default: ''
},
showTitle: {
type: Boolean,
default: true
}
})
const emit = defineEmits<{
(e: 'productClick', productId: string): void
}>()
const products = ref<Array<RecommendProduct>>([])
const page = ref<number>(1)
const loading = ref<boolean>(false)
const finished = ref<boolean>(false)
const errorText = ref<string>('')
const titleText = computed((): string => {
return props.title as string
})
const toExcludeProductIds = (): Array<string> => {
const result: Array<string> = []
try {
const normalized = JSON.parse(JSON.stringify(props.excludeProductIds))
if (Array.isArray(normalized)) {
for (let i = 0; i < normalized.length; i++) {
const item = normalized[i]
if (item != null) {
const itemText = '' + item
if (itemText !== '' && result.indexOf(itemText) < 0) {
result.push(itemText)
}
}
}
}
} catch (e) {
}
return result
}
const getNumberField = (obj: UTSJSONObject, key: string): number => {
try {
const value = obj.getNumber(key)
if (value != null) {
return value
}
} catch (e) {
}
return 0
}
const getStringField = (obj: UTSJSONObject, key: string): string => {
try {
const value = obj.getString(key)
if (value != null) {
return value
}
} catch (e) {
}
return ''
}
const formatSalesText = (salesCount: number): string => {
if (salesCount <= 0) {
return ''
}
if (salesCount >= 10000) {
const value = Math.floor(salesCount / 100) / 100
return '已售 ' + value + '万'
}
return '已售 ' + salesCount
}
const parseProducts = (rows: Array<UTSJSONObject>): Array<RecommendProduct> => {
const result: Array<RecommendProduct> = []
for (let i = 0; i < rows.length; i++) {
const row = rows[i]
const productId = getStringField(row, 'id')
if (productId === '') {
continue
}
const price = getNumberField(row, 'base_price') > 0 ? getNumberField(row, 'base_price') : getNumberField(row, 'price')
const originalPrice = getNumberField(row, 'market_price') > 0 ? getNumberField(row, 'market_price') : getNumberField(row, 'original_price')
const image = getStringField(row, 'main_image_url') !== ''
? getStringField(row, 'main_image_url')
: (getStringField(row, 'image_url') !== '' ? getStringField(row, 'image_url') : DEFAULT_IMAGE)
const salesCount = getNumberField(row, 'sale_count')
const name = getStringField(row, 'name') !== '' ? getStringField(row, 'name') : '精选商品'
result.push({
id: productId,
name,
image,
price: price > 0 ? price : 0,
originalPrice: originalPrice > 0 ? originalPrice : 0,
salesText: formatSalesText(salesCount),
categoryId: getStringField(row, 'category_id')
})
}
return result
}
const loadProducts = async (reset: boolean): Promise<void> => {
if (loading.value) {
return
}
if (!reset && finished.value) {
return
}
if (reset) {
products.value = []
page.value = 1
finished.value = false
errorText.value = ''
}
loading.value = true
try {
const requestParams = new UTSJSONObject()
requestParams.set('limit', props.pageSize as number)
requestParams.set('offset', (page.value - 1) * (props.pageSize as number))
requestParams.set('categoryId', props.categoryId as string)
requestParams.set('source', props.source as string)
requestParams.set('excludeProductIds', toExcludeProductIds())
const rows = await supabaseService.getRecommendProducts(requestParams)
const parsed = parseProducts(rows)
if (reset) {
products.value = parsed
} else {
const merged = products.value.slice()
for (let i = 0; i < parsed.length; i++) {
let exists = false
for (let j = 0; j < merged.length; j++) {
if (merged[j].id === parsed[i].id) {
exists = true
break
}
}
if (!exists) {
merged.push(parsed[i])
}
}
products.value = merged
}
if (parsed.length < (props.pageSize as number)) {
finished.value = true
} else {
page.value = page.value + 1
}
if (parsed.length === 0 && products.value.length === 0) {
finished.value = true
}
} catch (error) {
console.error('加载猜你喜欢失败:', error)
errorText.value = '推荐商品加载失败'
} finally {
loading.value = false
}
}
const handleProductClick = (productId: string): void => {
emit('productClick', productId)
}
watch(() => props.loadMoreKey, (newValue: number, oldValue: number) => {
if (newValue !== oldValue) {
loadProducts(false)
}
})
watch(() => props.categoryId, (newValue: string, oldValue: string) => {
if (newValue !== oldValue) {
loadProducts(true)
}
})
watch(() => JSON.stringify(props.excludeProductIds), (newValue: string, oldValue: string) => {
if (newValue !== oldValue) {
loadProducts(true)
}
})
onMounted(() => {
loadProducts(true)
})
</script>
<style scoped>
.guess-you-like-section {
display: flex;
flex-direction: column;
margin-top: 12px;
}
.guess-header {
padding: 0 4px 10px;
}
.guess-title {
font-size: 16px;
font-weight: bold;
color: #222222;
}
.guess-grid {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
}
.guess-card {
width: 48.5%;
background-color: #ffffff;
border-radius: 12px;
overflow: hidden;
margin-bottom: 12px;
}
.guess-image {
width: 100%;
height: 148px;
background-color: #f2f2f2;
}
.guess-body {
padding: 10px;
display: flex;
flex-direction: column;
}
.guess-name {
font-size: 13px;
line-height: 19px;
color: #222222;
height: 38px;
margin-bottom: 8px;
}
.guess-price-row {
display: flex;
flex-direction: row;
align-items: baseline;
flex-wrap: wrap;
}
.guess-price {
font-size: 16px;
font-weight: bold;
color: #ff4d4f;
margin-right: 6px;
}
.guess-original-price {
font-size: 11px;
color: #999999;
text-decoration: line-through;
}
.guess-sales {
font-size: 11px;
color: #999999;
margin-top: 6px;
}
.guess-empty,
.guess-status-row {
padding: 14px 0;
display: flex;
justify-content: center;
align-items: center;
}
.guess-empty-text,
.guess-status-text {
font-size: 12px;
color: #999999;
}
</style>

View File

@@ -0,0 +1,114 @@
BEGIN;
CREATE TABLE IF NOT EXISTS public.hss_service_catalog (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
category TEXT NOT NULL DEFAULT '',
price NUMERIC(10, 2) NOT NULL DEFAULT 0,
duration_text TEXT NOT NULL DEFAULT '',
summary TEXT NOT NULL DEFAULT '',
tags_json JSONB NOT NULL DEFAULT '[]'::jsonb,
suitable_for TEXT NOT NULL DEFAULT '',
sort_no INTEGER NOT NULL DEFAULT 0,
status SMALLINT NOT NULL DEFAULT 1,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_hss_service_catalog_status_sort
ON public.hss_service_catalog(status, sort_no)
WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_hss_service_catalog_category
ON public.hss_service_catalog(category)
WHERE deleted_at IS NULL;
CREATE OR REPLACE FUNCTION public.tg_hss_service_catalog_updated_at()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS trg_hss_service_catalog_updated_at ON public.hss_service_catalog;
CREATE TRIGGER trg_hss_service_catalog_updated_at
BEFORE UPDATE ON public.hss_service_catalog
FOR EACH ROW
EXECUTE FUNCTION public.tg_hss_service_catalog_updated_at();
ALTER TABLE public.hss_service_catalog ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS hss_service_catalog_public_select ON public.hss_service_catalog;
CREATE POLICY hss_service_catalog_public_select
ON public.hss_service_catalog
FOR SELECT
USING (deleted_at IS NULL AND status = 1);
INSERT INTO public.hss_service_catalog (
id,
name,
category,
price,
duration_text,
summary,
tags_json,
suitable_for,
sort_no,
status
) VALUES
(
'svc-001',
'基础上门护理',
'日常照护',
168,
'约 2 小时',
'覆盖生命体征监测、基础照护、风险提醒。',
'["适老化", "护理员上门", "支持家属陪同"]'::jsonb,
'行动不便、术后恢复、慢病随访老人',
10,
1
),
(
'svc-002',
'康复训练指导',
'康复支持',
260,
'约 3 小时',
'提供肢体训练、步态练习和居家康复建议。',
'["康复师", "步骤清晰", "可连续预约"]'::jsonb,
'卒中恢复、术后康复、失能半失能老人',
20,
1
),
(
'svc-003',
'慢病健康随访',
'健康管理',
128,
'约 90 分钟',
'完成血压血糖监测、用药核对与健康宣教。',
'["随访", "慢病", "可生成记录"]'::jsonb,
'高血压、糖尿病等长期管理老人',
30,
1
)
ON CONFLICT (id) DO UPDATE SET
name = EXCLUDED.name,
category = EXCLUDED.category,
price = EXCLUDED.price,
duration_text = EXCLUDED.duration_text,
summary = EXCLUDED.summary,
tags_json = EXCLUDED.tags_json,
suitable_for = EXCLUDED.suitable_for,
sort_no = EXCLUDED.sort_no,
status = EXCLUDED.status,
deleted_at = NULL,
updated_at = now();
COMMENT ON TABLE public.hss_service_catalog IS '居家上门服务目录表';
COMMIT;

View File

@@ -0,0 +1,274 @@
BEGIN;
ALTER TABLE public.ml_user_addresses
ADD COLUMN IF NOT EXISTS latitude DOUBLE PRECISION,
ADD COLUMN IF NOT EXISTS longitude DOUBLE PRECISION,
ADD COLUMN IF NOT EXISTS coordinate_type TEXT NOT NULL DEFAULT 'gcj02';
CREATE TABLE IF NOT EXISTS public.hss_service_orders (
id TEXT PRIMARY KEY,
order_no TEXT NOT NULL UNIQUE,
user_id UUID NOT NULL,
service_id TEXT NOT NULL,
service_name TEXT NOT NULL,
service_snapshot_json JSONB NOT NULL DEFAULT '{}'::jsonb,
service_address_id UUID,
address_snapshot_json JSONB NOT NULL DEFAULT '{}'::jsonb,
recipient_name TEXT NOT NULL DEFAULT '',
recipient_phone TEXT NOT NULL DEFAULT '',
contact_name TEXT NOT NULL DEFAULT '',
contact_phone TEXT NOT NULL DEFAULT '',
appointment_time TIMESTAMPTZ,
remark TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'created',
current_assignment_id TEXT NOT NULL DEFAULT '',
current_staff_id UUID,
accepted_at TIMESTAMPTZ,
departed_at TIMESTAMPTZ,
arrived_at TIMESTAMPTZ,
service_started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
pending_acceptance_at TIMESTAMPTZ,
accepted_by_user_at TIMESTAMPTZ,
reviewed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ,
deleted_by UUID,
CONSTRAINT chk_hss_service_orders_status CHECK (
status IN (
'created', 'paid', 'assigned', 'accepted', 'rejected', 'departed', 'arrived',
'in_service', 'completed', 'pending_acceptance', 'accepted_by_user',
'reviewed', 'settled', 'cancelled', 'exception'
)
)
);
CREATE TABLE IF NOT EXISTS public.hss_service_assignments (
id TEXT PRIMARY KEY,
order_id TEXT NOT NULL REFERENCES public.hss_service_orders(id) ON DELETE CASCADE,
staff_id UUID NOT NULL REFERENCES public.ml_delivery_staff(id) ON DELETE RESTRICT,
station_id UUID REFERENCES public.ml_delivery_stations(id) ON DELETE SET NULL,
status TEXT NOT NULL DEFAULT 'assigned',
assigned_at TIMESTAMPTZ NOT NULL DEFAULT now(),
accepted_at TIMESTAMPTZ,
rejected_at TIMESTAMPTZ,
reject_reason TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT chk_hss_service_assignments_status CHECK (
status IN ('assigned', 'accepted', 'rejected', 'departed', 'arrived', 'in_service', 'pending_acceptance', 'completed', 'cancelled', 'exception')
)
);
CREATE TABLE IF NOT EXISTS public.hss_service_execution_records (
id TEXT PRIMARY KEY,
order_id TEXT NOT NULL REFERENCES public.hss_service_orders(id) ON DELETE CASCADE,
assignment_id TEXT NOT NULL REFERENCES public.hss_service_assignments(id) ON DELETE CASCADE,
checkin_time TIMESTAMPTZ,
checkin_latitude DOUBLE PRECISION,
checkin_longitude DOUBLE PRECISION,
checkin_address TEXT NOT NULL DEFAULT '',
service_started_at TIMESTAMPTZ,
service_finished_at TIMESTAMPTZ,
actual_duration_minutes INTEGER NOT NULL DEFAULT 0,
service_items_json JSONB NOT NULL DEFAULT '[]'::jsonb,
summary TEXT NOT NULL DEFAULT '',
remark TEXT NOT NULL DEFAULT '',
track_points_json JSONB NOT NULL DEFAULT '[]'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS public.hss_service_evidence_files (
id TEXT PRIMARY KEY,
order_id TEXT NOT NULL REFERENCES public.hss_service_orders(id) ON DELETE CASCADE,
execution_record_id TEXT REFERENCES public.hss_service_execution_records(id) ON DELETE CASCADE,
phase TEXT NOT NULL DEFAULT 'service',
file_type TEXT NOT NULL DEFAULT 'image',
storage_path TEXT NOT NULL DEFAULT '',
file_url TEXT NOT NULL DEFAULT '',
latitude DOUBLE PRECISION,
longitude DOUBLE PRECISION,
captured_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS public.hss_service_order_status_logs (
id TEXT PRIMARY KEY,
order_id TEXT NOT NULL REFERENCES public.hss_service_orders(id) ON DELETE CASCADE,
from_status TEXT NOT NULL DEFAULT '',
to_status TEXT NOT NULL,
operator_id UUID,
operator_role TEXT NOT NULL DEFAULT '',
remark TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS public.hss_service_reviews (
id TEXT PRIMARY KEY,
order_id TEXT NOT NULL REFERENCES public.hss_service_orders(id) ON DELETE CASCADE,
user_id UUID NOT NULL,
rating INTEGER NOT NULL DEFAULT 5,
tags_json JSONB NOT NULL DEFAULT '[]'::jsonb,
content TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_hss_service_orders_user_status
ON public.hss_service_orders(user_id, status)
WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_hss_service_orders_staff_status
ON public.hss_service_orders(current_staff_id, status)
WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_hss_service_assignments_staff_status
ON public.hss_service_assignments(staff_id, status);
CREATE INDEX IF NOT EXISTS idx_hss_service_execution_records_order
ON public.hss_service_execution_records(order_id);
CREATE INDEX IF NOT EXISTS idx_hss_service_evidence_files_order
ON public.hss_service_evidence_files(order_id);
CREATE INDEX IF NOT EXISTS idx_hss_service_logs_order_created
ON public.hss_service_order_status_logs(order_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_hss_service_reviews_order
ON public.hss_service_reviews(order_id);
CREATE OR REPLACE FUNCTION public.tg_hss_set_updated_at()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS trg_hss_service_orders_updated_at ON public.hss_service_orders;
CREATE TRIGGER trg_hss_service_orders_updated_at
BEFORE UPDATE ON public.hss_service_orders
FOR EACH ROW
EXECUTE FUNCTION public.tg_hss_set_updated_at();
DROP TRIGGER IF EXISTS trg_hss_service_assignments_updated_at ON public.hss_service_assignments;
CREATE TRIGGER trg_hss_service_assignments_updated_at
BEFORE UPDATE ON public.hss_service_assignments
FOR EACH ROW
EXECUTE FUNCTION public.tg_hss_set_updated_at();
DROP TRIGGER IF EXISTS trg_hss_service_execution_records_updated_at ON public.hss_service_execution_records;
CREATE TRIGGER trg_hss_service_execution_records_updated_at
BEFORE UPDATE ON public.hss_service_execution_records
FOR EACH ROW
EXECUTE FUNCTION public.tg_hss_set_updated_at();
ALTER TABLE public.hss_service_orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.hss_service_assignments ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.hss_service_execution_records ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.hss_service_evidence_files ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.hss_service_order_status_logs ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.hss_service_reviews ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS hss_service_orders_user_select ON public.hss_service_orders;
CREATE POLICY hss_service_orders_user_select
ON public.hss_service_orders
FOR SELECT
TO authenticated
USING (
deleted_at IS NULL AND (
user_id IN (SELECT id FROM public.ak_users WHERE auth_id = auth.uid())
OR current_staff_id IN (SELECT id FROM public.ml_delivery_staff WHERE uid IN (SELECT id FROM public.ak_users WHERE auth_id = auth.uid()) AND deleted_at IS NULL)
)
);
DROP POLICY IF EXISTS hss_service_orders_user_insert ON public.hss_service_orders;
CREATE POLICY hss_service_orders_user_insert
ON public.hss_service_orders
FOR INSERT
TO authenticated
WITH CHECK (user_id IN (SELECT id FROM public.ak_users WHERE auth_id = auth.uid()));
DROP POLICY IF EXISTS hss_service_orders_user_update ON public.hss_service_orders;
CREATE POLICY hss_service_orders_user_update
ON public.hss_service_orders
FOR UPDATE
TO authenticated
USING (
deleted_at IS NULL AND (
user_id IN (SELECT id FROM public.ak_users WHERE auth_id = auth.uid())
OR current_staff_id IN (SELECT id FROM public.ml_delivery_staff WHERE uid IN (SELECT id FROM public.ak_users WHERE auth_id = auth.uid()) AND deleted_at IS NULL)
)
)
WITH CHECK (
deleted_at IS NULL AND (
user_id IN (SELECT id FROM public.ak_users WHERE auth_id = auth.uid())
OR current_staff_id IN (SELECT id FROM public.ml_delivery_staff WHERE uid IN (SELECT id FROM public.ak_users WHERE auth_id = auth.uid()) AND deleted_at IS NULL)
)
);
DROP POLICY IF EXISTS hss_service_assignments_staff_select ON public.hss_service_assignments;
CREATE POLICY hss_service_assignments_staff_select
ON public.hss_service_assignments
FOR SELECT
TO authenticated
USING (staff_id IN (SELECT id FROM public.ml_delivery_staff WHERE uid IN (SELECT id FROM public.ak_users WHERE auth_id = auth.uid()) AND deleted_at IS NULL));
DROP POLICY IF EXISTS hss_service_assignments_staff_update ON public.hss_service_assignments;
CREATE POLICY hss_service_assignments_staff_update
ON public.hss_service_assignments
FOR UPDATE
TO authenticated
USING (staff_id IN (SELECT id FROM public.ml_delivery_staff WHERE uid IN (SELECT id FROM public.ak_users WHERE auth_id = auth.uid()) AND deleted_at IS NULL))
WITH CHECK (staff_id IN (SELECT id FROM public.ml_delivery_staff WHERE uid IN (SELECT id FROM public.ak_users WHERE auth_id = auth.uid()) AND deleted_at IS NULL));
DROP POLICY IF EXISTS hss_service_assignments_staff_insert ON public.hss_service_assignments;
CREATE POLICY hss_service_assignments_staff_insert
ON public.hss_service_assignments
FOR INSERT
TO authenticated
WITH CHECK (staff_id IN (SELECT id FROM public.ml_delivery_staff WHERE uid IN (SELECT id FROM public.ak_users WHERE auth_id = auth.uid()) AND deleted_at IS NULL));
DROP POLICY IF EXISTS hss_service_execution_records_order_access ON public.hss_service_execution_records;
CREATE POLICY hss_service_execution_records_order_access
ON public.hss_service_execution_records
FOR ALL
TO authenticated
USING (order_id IN (SELECT id FROM public.hss_service_orders WHERE deleted_at IS NULL))
WITH CHECK (order_id IN (SELECT id FROM public.hss_service_orders WHERE deleted_at IS NULL));
DROP POLICY IF EXISTS hss_service_evidence_files_order_access ON public.hss_service_evidence_files;
CREATE POLICY hss_service_evidence_files_order_access
ON public.hss_service_evidence_files
FOR ALL
TO authenticated
USING (order_id IN (SELECT id FROM public.hss_service_orders WHERE deleted_at IS NULL))
WITH CHECK (order_id IN (SELECT id FROM public.hss_service_orders WHERE deleted_at IS NULL));
DROP POLICY IF EXISTS hss_service_order_status_logs_order_access ON public.hss_service_order_status_logs;
CREATE POLICY hss_service_order_status_logs_order_access
ON public.hss_service_order_status_logs
FOR ALL
TO authenticated
USING (order_id IN (SELECT id FROM public.hss_service_orders WHERE deleted_at IS NULL))
WITH CHECK (order_id IN (SELECT id FROM public.hss_service_orders WHERE deleted_at IS NULL));
DROP POLICY IF EXISTS hss_service_reviews_order_access ON public.hss_service_reviews;
CREATE POLICY hss_service_reviews_order_access
ON public.hss_service_reviews
FOR ALL
TO authenticated
USING (order_id IN (SELECT id FROM public.hss_service_orders WHERE deleted_at IS NULL))
WITH CHECK (order_id IN (SELECT id FROM public.hss_service_orders WHERE deleted_at IS NULL));
COMMENT ON TABLE public.hss_service_orders IS '居家上门服务订单主表';
COMMENT ON TABLE public.hss_service_assignments IS '居家上门服务派单表';
COMMENT ON TABLE public.hss_service_execution_records IS '居家上门服务执行记录表';
COMMENT ON TABLE public.hss_service_evidence_files IS '居家上门服务证据文件表';
COMMENT ON TABLE public.hss_service_order_status_logs IS '居家上门服务状态日志表';
COMMENT ON TABLE public.hss_service_reviews IS '居家上门服务评价表';
COMMIT;

View File

@@ -0,0 +1,17 @@
BEGIN;
ALTER TABLE public.ml_orders
ADD COLUMN IF NOT EXISTS cancelled_at TIMESTAMPTZ NULL,
ADD COLUMN IF NOT EXISTS consumer_deleted_at TIMESTAMPTZ NULL;
COMMENT ON COLUMN public.ml_orders.cancelled_at IS '订单取消时间,包含用户取消与支付超时取消';
COMMENT ON COLUMN public.ml_orders.consumer_deleted_at IS '消费者侧软删除时间,仅影响消费者订单列表展示';
CREATE INDEX IF NOT EXISTS idx_ml_orders_consumer_deleted_at
ON public.ml_orders(user_id, consumer_deleted_at, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_ml_orders_consumer_visible
ON public.ml_orders(user_id, created_at DESC)
WHERE consumer_deleted_at IS NULL;
COMMIT;

View File

@@ -0,0 +1,35 @@
BEGIN;
ALTER TABLE public.hss_service_orders
ADD COLUMN IF NOT EXISTS payment_status SMALLINT NOT NULL DEFAULT 1;
ALTER TABLE public.hss_service_orders
ADD COLUMN IF NOT EXISTS pay_expire_at TIMESTAMPTZ;
ALTER TABLE public.hss_service_orders
ADD COLUMN IF NOT EXISTS cancel_reason TEXT NOT NULL DEFAULT '';
ALTER TABLE public.hss_service_orders
ADD COLUMN IF NOT EXISTS cancelled_at TIMESTAMPTZ;
ALTER TABLE public.hss_service_orders
ADD COLUMN IF NOT EXISTS consumer_deleted_at TIMESTAMPTZ;
CREATE INDEX IF NOT EXISTS idx_hss_service_orders_user_visible
ON public.hss_service_orders(user_id, created_at DESC)
WHERE consumer_deleted_at IS NULL;
UPDATE public.hss_service_orders
SET pay_expire_at = created_at + INTERVAL '10 minutes'
WHERE pay_expire_at IS NULL
AND status = 'created'
AND payment_status = 1;
ALTER TABLE public.ml_orders
ADD COLUMN IF NOT EXISTS pay_expire_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS payment_status SMALLINT NOT NULL DEFAULT 1,
ADD COLUMN IF NOT EXISTS cancel_reason TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS cancelled_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS consumer_deleted_at TIMESTAMPTZ;
COMMIT;

View File

@@ -0,0 +1,77 @@
BEGIN;
ALTER TABLE public.ml_orders
ADD COLUMN IF NOT EXISTS pay_expire_at TIMESTAMPTZ;
ALTER TABLE public.ml_orders
DROP CONSTRAINT IF EXISTS chk_ml_order_status;
ALTER TABLE public.ml_orders
DROP CONSTRAINT IF EXISTS chk_ml_payment_status;
ALTER TABLE public.ml_orders
ADD CONSTRAINT chk_ml_order_status CHECK (order_status IN (1,2,3,4,5,6,7,8));
ALTER TABLE public.ml_orders
ADD CONSTRAINT chk_ml_payment_status CHECK (payment_status IN (1,2,3,4,5));
COMMENT ON COLUMN public.ml_orders.pay_expire_at IS '支付截止时间,超过后订单进入已超时';
UPDATE public.ml_orders
SET pay_expire_at = created_at + INTERVAL '10 minutes'
WHERE pay_expire_at IS NULL
AND order_status = 1
AND payment_status = 1;
UPDATE public.ml_orders
SET order_status = 8,
payment_status = 5,
cancel_reason = CASE
WHEN cancel_reason IS NULL OR cancel_reason = '' THEN '支付超时自动关闭'
ELSE cancel_reason
END,
updated_at = NOW()
WHERE order_status = 1
AND payment_status = 1
AND pay_expire_at IS NOT NULL
AND pay_expire_at <= NOW();
CREATE INDEX IF NOT EXISTS idx_ml_orders_pay_expire_at
ON public.ml_orders(pay_expire_at);
DROP VIEW IF EXISTS public.ml_orders_detail_view;
CREATE VIEW public.ml_orders_detail_view AS
SELECT
o.*,
u.username as customer_name,
u.phone as customer_phone,
m.username as merchant_name,
s.shop_name,
CASE
WHEN o.order_status = 1 THEN '待付款'
WHEN o.order_status = 2 THEN '待发货'
WHEN o.order_status = 3 THEN '待收货'
WHEN o.order_status = 4 THEN '已完成'
WHEN o.order_status = 5 THEN '已取消'
WHEN o.order_status = 6 THEN '退款中'
WHEN o.order_status = 7 THEN '已退款'
WHEN o.order_status = 8 THEN '已超时'
ELSE '未知'
END as order_status_name,
CASE
WHEN o.payment_status = 1 THEN '未付款'
WHEN o.payment_status = 2 THEN '已付款'
WHEN o.payment_status = 3 THEN '部分退款'
WHEN o.payment_status = 4 THEN '全额退款'
WHEN o.payment_status = 5 THEN '已关闭'
ELSE '未知'
END as payment_status_name
FROM public.ml_orders o
LEFT JOIN public.ak_users u ON o.user_id = u.id
LEFT JOIN public.ak_users m ON o.merchant_id = m.id
LEFT JOIN public.ml_shops s ON o.merchant_id = s.merchant_id;
COMMENT ON VIEW public.ml_orders_detail_view IS '订单详情视图';
COMMIT;

View File

@@ -47,7 +47,13 @@
"optimization": { "optimization": {
"subPackages": true "subPackages": true
}, },
"lazyCodeLoading": "requiredComponents" "lazyCodeLoading": "requiredComponents",
"requiredPrivateInfos": ["getLocation", "chooseLocation"],
"permission": {
"scope.userLocation": {
"desc": "用于获取您的服务地址,方便服务人员上门服务"
}
}
}, },
"mp-alipay": { "mp-alipay": {
"appid": "", "appid": "",

View File

@@ -198,7 +198,8 @@
{ {
"path": "payment", "path": "payment",
"style": { "style": {
"navigationBarTitleText": "收银台" "navigationBarTitleText": "",
"navigationStyle": "custom"
} }
}, },
{ {
@@ -211,8 +212,10 @@
{ {
"path": "orders", "path": "orders",
"style": { "style": {
"navigationBarTitleText": "我的订单", "navigationBarTitleText": "",
"enablePullDownRefresh": true "navigationStyle": "custom",
"enablePullDownRefresh": true,
"backgroundColor": "#f5f5f5"
} }
}, },
{ {

View File

@@ -1,4 +1,4 @@
{ {
"pages": [ "pages": [
{ {
"path": "pages/user/boot", "path": "pages/user/boot",
@@ -13,6 +13,24 @@
"navigationStyle": "custom" "navigationStyle": "custom"
} }
}, },
{
"path": "pages/address/address-edit",
"style": {
"navigationBarTitleText": "服务地址"
}
},
{
"path": "pages/address/address-list",
"style": {
"navigationBarTitleText": "选择服务地址"
}
},
{
"path": "pages/address/address-map-select",
"style": {
"navigationBarTitleText": "地图选点"
}
},
{ {
"path": "pages/mall/admin/homePage/index", "path": "pages/mall/admin/homePage/index",
"style": { "style": {
@@ -338,7 +356,8 @@
{ {
"path": "payment", "path": "payment",
"style": { "style": {
"navigationBarTitleText": "收银台" "navigationBarTitleText": "",
"navigationStyle": "custom"
} }
}, },
{ {
@@ -351,8 +370,10 @@
{ {
"path": "orders", "path": "orders",
"style": { "style": {
"navigationBarTitleText": "我的订单", "navigationBarTitleText": "",
"enablePullDownRefresh": true "navigationStyle": "custom",
"enablePullDownRefresh": true,
"backgroundColor": "#f5f5f5"
} }
}, },
{ {

View File

@@ -71,13 +71,321 @@
} }
} }
], ],
"subPackages": [], "subPackages": [
"globalStyle": { {
"navigationBarTextStyle": "black", "root": "pages/mall/consumer",
"navigationBarTitleText": "delivery", "pages": [
"navigationBarBackgroundColor": "#FFFFFF", {
"backgroundColor": "#F3F7F9" "path": "settings",
}, "style": {
"navigationBarTitleText": "设置"
}
},
{
"path": "edit-profile",
"style": {
"navigationBarTitleText": "编辑资料"
}
},
{
"path": "wallet",
"style": {
"navigationBarTitleText": "我的钱包"
}
},
{
"path": "withdraw",
"style": {
"navigationBarTitleText": "余额提现"
}
},
{
"path": "search",
"style": {
"navigationBarTitleText": "搜索",
"navigationStyle": "custom"
}
},
{
"path": "product-detail",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"path": "channel-detail",
"style": {
"navigationBarTitleText": "频道详情",
"navigationStyle": "custom"
}
},
{
"path": "shop-detail",
"style": {
"navigationBarTitleText": "店铺详情"
}
},
{
"path": "coupons",
"style": {
"navigationBarTitleText": "我的优惠券"
}
},
{
"path": "favorites",
"style": {
"navigationBarTitleText": "我的收藏"
}
},
{
"path": "footprint",
"style": {
"navigationBarTitleText": "我的足迹"
}
},
{
"path": "address",
"style": {
"navigationBarTitleText": "地址"
}
},
{
"path": "address-list",
"style": {
"navigationBarTitleText": "收货地址"
}
},
{
"path": "address-edit",
"style": {
"navigationBarTitleText": "编辑地址"
}
},
{
"path": "checkout",
"style": {
"navigationBarTitleText": "确认订单"
}
},
{
"path": "payment",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"path": "payment-success",
"style": {
"navigationBarTitleText": "支付成功",
"navigationStyle": "custom"
}
},
{
"path": "orders",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom",
"enablePullDownRefresh": true,
"backgroundColor": "#f5f5f5"
}
},
{
"path": "order-detail",
"style": {
"navigationBarTitleText": "订单详情"
}
},
{
"path": "logistics",
"style": {
"navigationBarTitleText": "物流详情"
}
},
{
"path": "review",
"style": {
"navigationBarTitleText": "评价晒单"
}
},
{
"path": "refund",
"style": {
"navigationBarTitleText": "退款/售后"
}
},
{
"path": "apply-refund",
"style": {
"navigationBarTitleText": "申请售后"
}
},
{
"path": "refund-review",
"style": {
"navigationBarTitleText": "服务评价"
}
},
{
"path": "chat",
"style": {
"navigationBarTitleText": "客服聊天",
"navigationStyle": "custom"
}
},
{
"path": "chat_new",
"style": {
"navigationBarTitleText": "客服聊天(新版)"
}
},
{
"path": "subscription/plan-list",
"style": {
"navigationBarTitleText": "软件订阅"
}
},
{
"path": "subscription/plan-detail",
"style": {
"navigationBarTitleText": "订阅详情"
}
},
{
"path": "subscription/subscribe-checkout",
"style": {
"navigationBarTitleText": "确认订阅"
}
},
{
"path": "subscription/my-subscriptions",
"style": {
"navigationBarTitleText": "我的订阅"
}
},
{
"path": "subscription/followed-shops",
"style": {
"navigationBarTitleText": "关注店铺"
}
},
{
"path": "points/index",
"style": {
"navigationBarTitleText": "积分管理"
}
},
{
"path": "points/signin",
"style": {
"navigationBarTitleText": "签到"
}
},
{
"path": "points/exchange",
"style": {
"navigationBarTitleText": "积分兑换"
}
},
{
"path": "points/exchange-records",
"style": {
"navigationBarTitleText": "兑换记录"
}
},
{
"path": "red-packets/index",
"style": {
"navigationBarTitleText": "我的红包"
}
},
{
"path": "bank-cards/index",
"style": {
"navigationBarTitleText": "银行卡管理"
}
},
{
"path": "bank-cards/add",
"style": {
"navigationBarTitleText": "添加银行卡"
}
},
{
"path": "home-service/index",
"style": {
"navigationBarTitleText": "居家上门服务",
"navigationStyle": "custom"
}
},
{
"path": "home-service/apply",
"style": {
"navigationBarTitleText": "提交服务申请",
"navigationStyle": "custom"
}
},
{
"path": "home-service/service-detail",
"style": {
"navigationBarTitleText": "预约服务",
"navigationStyle": "custom"
}
},
{
"path": "home-service/order-detail",
"style": {
"navigationBarTitleText": "服务单详情",
"navigationStyle": "custom"
}
},
{
"path": "home-service/feedback",
"style": {
"navigationBarTitleText": "验收反馈",
"navigationStyle": "custom"
}
},
{
"path": "bank-cards/verify",
"style": {
"navigationBarTitleText": "银行卡验证"
}
},
{
"path": "balance/index",
"style": {
"navigationBarTitleText": "余额"
}
},
{
"path": "my-reviews",
"style": {
"navigationBarTitleText": "我的评价"
}
},
{
"path": "message-detail",
"style": {
"navigationBarTitleText": "消息详情"
}
},
{
"path": "member/index",
"style": {
"navigationBarTitleText": "会员中心"
}
},
{
"path": "product-reviews",
"style": {
"navigationBarTitleText": "商品评价"
}
}
]
}
],
"tabBar": { "tabBar": {
"color": "#6B7280", "color": "#6B7280",
"selectedColor": "#0F766E", "selectedColor": "#0F766E",

View File

@@ -0,0 +1,636 @@
<template>
<view class="address-page">
<scroll-view class="address-scroll" scroll-y="true">
<view class="address-content">
<view class="current-location-card">
<text class="section-title">当前选择的位置</text>
<text v-if="hasLocation()" class="current-location-name">{{ displayLocationTitle }}</text>
<text v-if="hasLocation()" class="current-location-detail">{{ displayLocationDetail }}</text>
<text v-else class="current-location-placeholder">请选择小区、医院、养老院或街道位置</text>
</view>
<view class="form-card">
<view class="form-item">
<text class="form-label">联系人</text>
<input v-model="form.contactName" class="form-input" placeholder="请输入联系人姓名" />
</view>
<view class="form-item form-item-border">
<text class="form-label">手机号</text>
<input v-model="form.contactPhone" class="form-input" type="number" maxlength="11" placeholder="请输入联系电话" />
</view>
<view class="form-item form-item-border form-item-tappable" @click="chooseServiceLocation">
<view class="form-item-main">
<text class="form-label">所在位置</text>
<view v-if="hasLocation()" class="location-block">
<text class="location-title">{{ displayLocationTitle }}</text>
<text class="location-detail">{{ displayLocationDetail }}</text>
</view>
<text v-else class="location-placeholder">请选择小区/医院/养老院/街道</text>
</view>
<text class="form-action">重新选择</text>
</view>
<view class="map-entry-row" @click="goToMapSelect">
<text class="map-entry-text">自定义地图选点</text>
<text class="map-entry-arrow">></text>
</view>
<view class="form-item form-item-border">
<text class="form-label">详细门牌号</text>
<input v-model="form.houseNumber" class="form-input" placeholder="例如 3栋2单元1201 / 住院部3楼" />
</view>
<view class="form-item form-item-vertical">
<text class="form-label">服务备注</text>
<textarea v-model="form.remark" class="form-textarea" placeholder="例如 老人行动不便,请提前电话联系" maxlength="120"></textarea>
</view>
</view>
</view>
</scroll-view>
<view class="bottom-bar">
<button class="save-btn" @click="saveAddress">保存服务地址</button>
</view>
</view>
</template>
<script setup lang="uts">
import { computed, reactive } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
import { HomeServiceSelectedAddressType } from '@/types/home-service.uts'
const SELECTED_KEY = 'hss_selected_service_address'
const LIST_KEY = 'hss_service_address_list'
const MAP_DRAFT_KEY = 'hss_service_address_map_draft'
type ServiceAddressFormType = {
addressId: string
userId: string
isDefault: boolean
contactName: string
contactPhone: string
phone: string
addressName: string
locationName: string
addressDetail: string
locationAddress: string
houseNumber: string
doorNo: string
fullAddress: string
latitude: number
longitude: number
remark: string
coordinateType: string
createdAt: number
updatedAt: number
}
function createDefaultForm(): ServiceAddressFormType {
const now = Date.now()
const storedUserId = uni.getStorageSync('user_id') as string | null
return {
addressId: 'local-address-' + now,
userId: storedUserId != null ? storedUserId : '',
isDefault: true,
contactName: '',
contactPhone: '',
phone: '',
addressName: '',
locationName: '',
addressDetail: '',
locationAddress: '',
houseNumber: '',
doorNo: '',
fullAddress: '',
latitude: 0,
longitude: 0,
remark: '',
coordinateType: 'gcj02',
createdAt: now,
updatedAt: now
}
}
const form = reactive(createDefaultForm())
let editingAddressId = ''
const displayLocationTitle = computed((): string => {
if (form.locationName != '') {
return form.locationName
}
if (form.addressName != '') {
return form.addressName
}
if (form.locationAddress != '') {
return form.locationAddress
}
return '未选择位置'
})
const displayLocationDetail = computed((): string => {
if (form.locationAddress != '') {
return form.locationAddress
}
if (form.addressDetail != '') {
return form.addressDetail
}
return '请先通过地图选点获取位置'
})
function hasLocation(): boolean {
return form.locationName != '' || form.addressName != '' || form.locationAddress != '' || form.addressDetail != ''
}
function normalizeAddress(raw: HomeServiceSelectedAddressType): HomeServiceSelectedAddressType {
const phoneText = raw.phone != null && raw.phone != '' ? raw.phone : (raw.contactPhone != null ? raw.contactPhone : '')
const locationName = raw.locationName != null && raw.locationName != '' ? raw.locationName : (raw.addressName != null ? raw.addressName : '')
const locationAddress = raw.locationAddress != null && raw.locationAddress != '' ? raw.locationAddress : (raw.addressDetail != null ? raw.addressDetail : '')
const doorNo = raw.doorNo != null && raw.doorNo != '' ? raw.doorNo : (raw.houseNumber != null ? raw.houseNumber : '')
const fullAddressText = raw.fullAddress != null && raw.fullAddress != '' ? raw.fullAddress : locationAddress + ' ' + doorNo
return {
...raw,
addressId: raw.addressId != null && raw.addressId != '' ? raw.addressId : 'local-address-' + Date.now(),
userId: raw.userId != null ? raw.userId : '',
isDefault: raw.isDefault === true,
contactName: raw.contactName != null ? raw.contactName : '',
phone: phoneText,
contactPhone: phoneText,
locationName: locationName,
addressName: locationName,
locationAddress: locationAddress,
addressDetail: locationAddress,
doorNo: doorNo,
houseNumber: doorNo,
remark: raw.remark != null ? raw.remark : '',
coordinateType: raw.coordinateType != null && raw.coordinateType != '' ? raw.coordinateType : 'gcj02',
latitude: raw.latitude != null ? raw.latitude : 0,
longitude: raw.longitude != null ? raw.longitude : 0,
createdAt: raw.createdAt != null ? raw.createdAt : Date.now(),
updatedAt: raw.updatedAt != null ? raw.updatedAt : Date.now(),
fullAddress: fullAddressText.trim()
}
}
function applyAddress(address: HomeServiceSelectedAddressType): void {
const normalized = normalizeAddress(address)
form.addressId = normalized.addressId
form.userId = normalized.userId
form.isDefault = normalized.isDefault
form.contactName = normalized.contactName
form.contactPhone = normalized.contactPhone
form.phone = normalized.phone != null ? normalized.phone : normalized.contactPhone
form.addressName = normalized.addressName
form.locationName = normalized.locationName != null ? normalized.locationName : normalized.addressName
form.addressDetail = normalized.addressDetail
form.locationAddress = normalized.locationAddress != null ? normalized.locationAddress : normalized.addressDetail
form.houseNumber = normalized.houseNumber
form.doorNo = normalized.doorNo != null ? normalized.doorNo : normalized.houseNumber
form.fullAddress = normalized.fullAddress
form.latitude = normalized.latitude
form.longitude = normalized.longitude
form.remark = normalized.remark
form.coordinateType = normalized.coordinateType
form.createdAt = normalized.createdAt
form.updatedAt = normalized.updatedAt
editingAddressId = normalized.addressId
}
function readAddressList(): Array<HomeServiceSelectedAddressType> {
const stored = uni.getStorageSync(LIST_KEY)
if (stored == null) {
return []
}
try {
if (typeof stored === 'string') {
const storedText = (stored as string).trim()
if (storedText == '') {
return []
}
const parsed = JSON.parse(storedText) as Array<HomeServiceSelectedAddressType> | null
return parsed != null ? parsed : []
}
return stored as Array<HomeServiceSelectedAddressType>
} catch (error) {
console.error('解析服务地址列表失败', error)
uni.removeStorageSync(LIST_KEY)
return []
}
}
function writeAddressList(addresses: Array<HomeServiceSelectedAddressType>): void {
uni.setStorageSync(LIST_KEY, JSON.stringify(addresses))
}
function parseStoredAddress(rawValue: unknown): HomeServiceSelectedAddressType | null {
if (rawValue == null) {
return null
}
try {
if (typeof rawValue === 'string') {
const rawText = (rawValue as string).trim()
if (rawText == '') {
return null
}
const parsed = JSON.parse(rawText) as HomeServiceSelectedAddressType | null
return parsed
}
return rawValue as HomeServiceSelectedAddressType
} catch (error) {
console.error('解析服务地址对象失败', error)
return null
}
}
function ensureAddressListSeeded(): void {
const addresses = readAddressList()
if (addresses.length > 0) {
return
}
const selected = uni.getStorageSync(SELECTED_KEY) as HomeServiceSelectedAddressType | null
const normalizedSelected = parseStoredAddress(selected)
if (normalizedSelected != null) {
const seeded: Array<HomeServiceSelectedAddressType> = []
seeded.push(normalizeAddress(normalizedSelected))
writeAddressList(seeded)
}
}
function loadFromAddressId(addressId: string): void {
const addresses = readAddressList()
for (let i = 0; i < addresses.length; i++) {
if (addresses[i].addressId == addressId) {
applyAddress(addresses[i])
return
}
}
const selected = parseStoredAddress(uni.getStorageSync(SELECTED_KEY))
if (selected != null && selected.addressId == addressId) {
applyAddress(selected)
}
}
function loadCachedAddress(): void {
if (editingAddressId != '') {
loadFromAddressId(editingAddressId)
return
}
const cachedAddress = parseStoredAddress(uni.getStorageSync(SELECTED_KEY))
if (cachedAddress != null) {
applyAddress(cachedAddress)
}
}
function applyMapDraft(): void {
const draft = parseStoredAddress(uni.getStorageSync(MAP_DRAFT_KEY))
if (draft == null) {
return
}
const normalized = normalizeAddress(draft)
form.addressName = normalized.addressName
form.locationName = normalized.locationName != null ? normalized.locationName : normalized.addressName
form.addressDetail = normalized.addressDetail
form.locationAddress = normalized.locationAddress != null ? normalized.locationAddress : normalized.addressDetail
form.latitude = normalized.latitude
form.longitude = normalized.longitude
form.coordinateType = normalized.coordinateType
uni.removeStorageSync(MAP_DRAFT_KEY)
if (normalized.locationName != '' || normalized.locationAddress != '') {
uni.showToast({
title: '已回填地图位置',
icon: 'none'
})
}
}
function mapChooseLocationResult(name: string, address: string, latitude: number, longitude: number): void {
const title = name != '' ? name : address
const detail = address != '' ? address : title
form.addressName = title
form.locationName = title
form.addressDetail = detail
form.locationAddress = detail
form.latitude = latitude
form.longitude = longitude
form.coordinateType = 'gcj02'
}
function resolveLocationFailToast(error: unknown): string {
const errorText = String(error)
if (errorText.indexOf('cancel') >= 0) {
return ''
}
if (errorText.indexOf('auth deny') >= 0 || errorText.indexOf('authorize') >= 0 || errorText.indexOf('permission') >= 0 || errorText.indexOf('auth denied') >= 0) {
return '需要开启定位权限才能选择服务地址'
}
if (errorText.indexOf('location') >= 0 || errorText.indexOf('getLocation') >= 0) {
return '定位失败,请手动搜索或重新选择'
}
if (errorText.indexOf('map') >= 0 || errorText.indexOf('service') >= 0) {
return '地图服务暂不可用,请稍后重试'
}
return '位置选择失败,请重试'
}
function chooseServiceLocation(): void {
uni.chooseLocation({
success: (res) => {
const locationName = res.name != null ? res.name : ''
const locationAddress = res.address != null ? res.address : ''
mapChooseLocationResult(locationName, locationAddress, res.latitude, res.longitude)
},
fail: (error) => {
const toastText = resolveLocationFailToast(error)
if (toastText == '') {
return
}
uni.showToast({
title: toastText,
icon: 'none'
})
}
})
}
function goToMapSelect(): void {
uni.navigateTo({ url: '/pages/address/address-map-select' })
}
function isPhoneValid(phone: string): boolean {
return /^\d{11}$/.test(phone)
}
function buildAddressPayload(): HomeServiceSelectedAddressType {
const now = Date.now()
const storedUserId = uni.getStorageSync('user_id') as string | null
const phoneText = form.contactPhone != '' ? form.contactPhone : form.phone
const locationName = form.locationName != '' ? form.locationName : form.addressName
const locationAddress = form.locationAddress != '' ? form.locationAddress : form.addressDetail
const doorNo = form.doorNo != '' ? form.doorNo : form.houseNumber
return {
addressId: form.addressId != '' ? form.addressId : 'local-address-' + now,
userId: form.userId != '' ? form.userId : (storedUserId != null ? storedUserId : ''),
isDefault: true,
contactName: form.contactName,
contactPhone: phoneText,
phone: phoneText,
addressName: locationName,
locationName: locationName,
addressDetail: locationAddress,
locationAddress: locationAddress,
houseNumber: doorNo,
doorNo: doorNo,
fullAddress: (locationAddress + ' ' + doorNo).trim(),
latitude: form.latitude,
longitude: form.longitude,
remark: form.remark,
coordinateType: 'gcj02',
createdAt: form.createdAt > 0 ? form.createdAt : now,
updatedAt: now
}
}
function saveAddress(): void {
if (form.contactName == '') {
uni.showToast({ title: '请输入联系人姓名', icon: 'none' })
return
}
if (form.contactPhone == '') {
uni.showToast({ title: '请输入联系电话', icon: 'none' })
return
}
if (!isPhoneValid(form.contactPhone)) {
uni.showToast({ title: '请输入11位手机号', icon: 'none' })
return
}
if (!hasLocation()) {
uni.showToast({ title: '请选择所在位置', icon: 'none' })
return
}
if (form.houseNumber == '') {
uni.showToast({ title: '请输入详细门牌号', icon: 'none' })
return
}
const savedAddress = buildAddressPayload()
const addresses = readAddressList()
let updated = false
for (let i = 0; i < addresses.length; i++) {
if (addresses[i].addressId == savedAddress.addressId) {
addresses[i] = savedAddress
updated = true
break
}
}
if (!updated) {
addresses.unshift(savedAddress)
}
writeAddressList(addresses)
uni.setStorageSync(SELECTED_KEY, savedAddress)
uni.showToast({
title: '地址已保存',
icon: 'success'
})
setTimeout(() => {
uni.navigateBack()
}, 300)
}
onLoad((options) => {
ensureAddressListSeeded()
if (options == null) {
loadCachedAddress()
return
}
const addressId = options['id']
if (addressId != null && String(addressId) != '') {
editingAddressId = String(addressId)
loadFromAddressId(editingAddressId)
return
}
loadCachedAddress()
})
onShow(() => {
applyMapDraft()
})
</script>
<style scoped>
.address-page {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #f4f6f8;
display: flex;
flex-direction: column;
}
.address-scroll {
flex: 1;
min-height: 0;
}
.address-content {
padding: 24rpx 24rpx 180rpx;
gap: 24rpx;
display: flex;
flex-direction: column;
}
.current-location-card,
.form-card {
background: #ffffff;
border-radius: 28rpx;
padding: 24rpx;
box-shadow: 0 12rpx 28rpx rgba(15, 23, 42, 0.05);
}
.section-title,
.form-label,
.location-title,
.current-location-name,
.map-entry-text {
color: #1f2937;
}
.section-title {
font-size: 28rpx;
font-weight: 600;
margin-bottom: 12rpx;
}
.current-location-name,
.location-title {
font-size: 26rpx;
font-weight: 600;
line-height: 1.5;
}
.current-location-detail,
.current-location-placeholder,
.location-detail,
.location-placeholder,
.form-action,
.form-input,
.form-textarea,
.map-entry-arrow {
font-size: 24rpx;
color: #4b5563;
line-height: 1.6;
}
.current-location-placeholder,
.location-placeholder {
color: #9ca3af;
}
.current-location-detail,
.location-detail {
margin-top: 8rpx;
}
.form-item {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 18rpx 0;
gap: 20rpx;
}
.form-item-border {
border-top: 1rpx solid #eef2f7;
}
.form-item-vertical {
align-items: flex-start;
flex-direction: column;
border-top: 1rpx solid #eef2f7;
}
.form-item-tappable {
align-items: flex-start;
}
.form-item-main {
flex: 1;
display: flex;
flex-direction: column;
gap: 10rpx;
}
.form-label {
width: 132rpx;
font-size: 26rpx;
font-weight: 600;
flex-shrink: 0;
}
.form-input {
flex: 1;
text-align: right;
min-height: 44rpx;
}
.form-textarea {
width: 100%;
min-height: 150rpx;
margin-top: 12rpx;
background: #f8fafc;
border-radius: 20rpx;
padding: 20rpx;
box-sizing: border-box;
}
.location-block {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.form-action {
color: #d97706;
flex-shrink: 0;
}
.map-entry-row {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 18rpx 0 0;
border-top: 1rpx dashed #eef2f7;
margin-top: 8rpx;
}
.map-entry-text {
font-size: 24rpx;
font-weight: 600;
}
.map-entry-arrow {
color: #f97316;
}
.bottom-bar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 20rpx 24rpx 36rpx;
background: rgba(244, 246, 248, 0.96);
box-shadow: 0 -8rpx 24rpx rgba(15, 23, 42, 0.05);
}
.save-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
border-radius: 999rpx;
background: linear-gradient(135deg, #ff8a65 0%, #ff7043 100%);
color: #ffffff;
font-size: 30rpx;
font-weight: 600;
border: none;
}
</style>

View File

@@ -0,0 +1,390 @@
<template>
<view class="address-list-page">
<scroll-view class="address-scroll" scroll-y="true">
<view class="address-content">
<view v-if="addresses.length === 0" class="empty-state">
<text class="empty-icon">📍</text>
<text class="empty-title">还没有服务地址</text>
<text class="empty-text">新增一个常用服务地址,预约时会更快</text>
</view>
<view
v-for="item in addresses"
:key="item.addressId"
:class="['address-card', isSelected(item) ? 'address-card-selected' : '']"
@click="selectAddress(item)"
>
<view class="address-main">
<view class="address-top-row">
<text class="address-name">{{ item.contactName }}</text>
<text class="address-phone">{{ getPhone(item) }}</text>
<text v-if="item.isDefault" class="default-tag">默认</text>
<text v-if="isSelected(item)" class="selected-tag">当前选择</text>
</view>
<text class="address-location">{{ getLocationTitle(item) }}</text>
<text class="address-full">{{ getFullAddress(item) }}</text>
<text v-if="item.remark != ''" class="address-remark">备注:{{ item.remark }}</text>
</view>
<view class="address-actions">
<view class="action-btn" @click.stop="editAddress(item.addressId)">
<text class="action-text">编辑</text>
</view>
<view class="action-btn action-btn-delete" @click.stop="deleteAddress(item.addressId)">
<text class="action-text action-text-delete">删除</text>
</view>
</view>
</view>
</view>
</scroll-view>
<view class="bottom-bar">
<button class="add-btn" @click="addAddress">新建服务地址</button>
</view>
</view>
</template>
<script setup lang="uts">
import { ref } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { HomeServiceSelectedAddressType } from '@/types/home-service.uts'
const SELECTED_KEY = 'hss_selected_service_address'
const LIST_KEY = 'hss_service_address_list'
const addresses = ref<Array<HomeServiceSelectedAddressType>>([])
const selectedAddressId = ref('')
function normalizeAddress(raw: HomeServiceSelectedAddressType): HomeServiceSelectedAddressType {
const phoneText = raw.phone != null && raw.phone != '' ? raw.phone : (raw.contactPhone != null ? raw.contactPhone : '')
const locationName = raw.locationName != null && raw.locationName != '' ? raw.locationName : (raw.addressName != null ? raw.addressName : '')
const locationAddress = raw.locationAddress != null && raw.locationAddress != '' ? raw.locationAddress : (raw.addressDetail != null ? raw.addressDetail : '')
const doorNo = raw.doorNo != null && raw.doorNo != '' ? raw.doorNo : (raw.houseNumber != null ? raw.houseNumber : '')
const fullAddressText = raw.fullAddress != null && raw.fullAddress != '' ? raw.fullAddress : locationAddress + ' ' + doorNo
return {
...raw,
addressId: raw.addressId != null && raw.addressId != '' ? raw.addressId : 'local-address-' + Date.now(),
userId: raw.userId != null ? raw.userId : '',
isDefault: raw.isDefault === true,
contactName: raw.contactName != null ? raw.contactName : '',
phone: phoneText,
contactPhone: phoneText,
locationName: locationName,
addressName: locationName,
locationAddress: locationAddress,
addressDetail: locationAddress,
doorNo: doorNo,
houseNumber: doorNo,
remark: raw.remark != null ? raw.remark : '',
coordinateType: raw.coordinateType != null && raw.coordinateType != '' ? raw.coordinateType : 'gcj02',
latitude: raw.latitude != null ? raw.latitude : 0,
longitude: raw.longitude != null ? raw.longitude : 0,
createdAt: raw.createdAt != null ? raw.createdAt : Date.now(),
updatedAt: raw.updatedAt != null ? raw.updatedAt : Date.now(),
fullAddress: fullAddressText.trim()
}
}
function readAddressList(): Array<HomeServiceSelectedAddressType> {
const stored = uni.getStorageSync(LIST_KEY)
if (stored == null) {
return []
}
try {
let parsed: Array<HomeServiceSelectedAddressType> = []
if (typeof stored === 'string') {
const storedText = (stored as string).trim()
if (storedText == '') {
return []
}
const parsedValue = JSON.parse(storedText) as Array<HomeServiceSelectedAddressType> | null
if (parsedValue == null) {
return []
}
parsed = parsedValue
} else {
parsed = stored as Array<HomeServiceSelectedAddressType>
}
if (parsed == null || parsed.length == 0) {
return []
}
const normalized: Array<HomeServiceSelectedAddressType> = []
for (let i = 0; i < parsed.length; i++) {
if (parsed[i] != null) {
normalized.push(normalizeAddress(parsed[i]))
}
}
return normalized
} catch (error) {
console.error('解析服务地址列表失败', error)
uni.removeStorageSync(LIST_KEY)
return []
}
}
function writeAddressList(list: Array<HomeServiceSelectedAddressType>): void {
uni.setStorageSync(LIST_KEY, JSON.stringify(list))
}
function loadAddresses(): void {
const selected = uni.getStorageSync(SELECTED_KEY) as HomeServiceSelectedAddressType | null
if (selected != null) {
const normalizedSelected = normalizeAddress(selected)
selectedAddressId.value = normalizedSelected.addressId
}
const list = readAddressList()
if (list.length == 0 && selected != null) {
const seeded: Array<HomeServiceSelectedAddressType> = []
seeded.push(normalizeAddress(selected))
addresses.value = seeded
writeAddressList(seeded)
return
}
addresses.value = list
}
function isSelected(item: HomeServiceSelectedAddressType): boolean {
return selectedAddressId.value != '' && item.addressId == selectedAddressId.value
}
function getPhone(item: HomeServiceSelectedAddressType): string {
if (item.phone != null && item.phone != '') {
return item.phone
}
return item.contactPhone
}
function getLocationTitle(item: HomeServiceSelectedAddressType): string {
if (item.locationName != null && item.locationName != '') {
return item.locationName
}
return item.addressName
}
function getFullAddress(item: HomeServiceSelectedAddressType): string {
if (item.fullAddress != '') {
return item.fullAddress
}
const locationAddress = item.locationAddress != null && item.locationAddress != '' ? item.locationAddress : item.addressDetail
const doorNo = item.doorNo != null && item.doorNo != '' ? item.doorNo : item.houseNumber
return (locationAddress + ' ' + doorNo).trim()
}
function selectAddress(item: HomeServiceSelectedAddressType): void {
const normalized = normalizeAddress(item)
uni.setStorageSync(SELECTED_KEY, normalized)
selectedAddressId.value = normalized.addressId
uni.navigateBack()
}
function addAddress(): void {
uni.navigateTo({ url: '/pages/address/address-edit' })
}
function editAddress(addressId: string): void {
uni.navigateTo({ url: '/pages/address/address-edit?id=' + addressId })
}
function deleteAddress(addressId: string): void {
uni.showModal({
title: '提示',
content: '确定要删除该服务地址吗?',
success: (res) => {
if (!res.confirm) {
return
}
const nextList: Array<HomeServiceSelectedAddressType> = []
for (let i = 0; i < addresses.value.length; i++) {
if (addresses.value[i].addressId != addressId) {
nextList.push(addresses.value[i])
}
}
addresses.value = nextList
writeAddressList(nextList)
if (selectedAddressId.value == addressId) {
selectedAddressId.value = ''
uni.removeStorageSync(SELECTED_KEY)
if (nextList.length > 0) {
const nextSelected = normalizeAddress(nextList[0])
selectedAddressId.value = nextSelected.addressId
uni.setStorageSync(SELECTED_KEY, nextSelected)
}
}
uni.showToast({
title: '删除成功',
icon: 'success'
})
}
})
}
onShow(() => {
loadAddresses()
})
</script>
<style scoped>
.address-list-page {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #f4f6f8;
display: flex;
flex-direction: column;
}
.address-scroll {
flex: 1;
min-height: 0;
}
.address-content {
padding: 24rpx 24rpx 180rpx;
display: flex;
flex-direction: column;
gap: 20rpx;
}
.address-card {
background: #ffffff;
border-radius: 28rpx;
padding: 24rpx;
box-shadow: 0 12rpx 28rpx rgba(15, 23, 42, 0.05);
border: 2rpx solid transparent;
}
.address-card-selected {
border-color: rgba(249, 115, 22, 0.38);
box-shadow: 0 16rpx 30rpx rgba(249, 115, 22, 0.12);
}
.address-main {
display: flex;
flex-direction: column;
gap: 10rpx;
}
.address-top-row,
.address-actions,
.bottom-bar,
.empty-state {
display: flex;
flex-direction: row;
}
.address-top-row {
align-items: center;
flex-wrap: wrap;
gap: 12rpx;
}
.address-name,
.address-location,
.empty-title {
color: #1f2937;
font-weight: 600;
}
.address-name {
font-size: 28rpx;
}
.address-phone,
.address-full,
.address-remark,
.empty-text,
.action-text {
font-size: 24rpx;
color: #4b5563;
line-height: 1.6;
}
.address-location {
font-size: 26rpx;
}
.default-tag,
.selected-tag {
padding: 4rpx 12rpx;
border-radius: 999rpx;
font-size: 20rpx;
}
.default-tag {
background: #fff1eb;
color: #ff5000;
}
.selected-tag {
background: #fff7ed;
color: #ea580c;
}
.address-actions {
justify-content: flex-end;
gap: 16rpx;
margin-top: 18rpx;
}
.action-btn {
padding: 12rpx 22rpx;
border-radius: 999rpx;
background: #f8fafc;
}
.action-btn-delete {
background: #fff5f5;
}
.action-text-delete {
color: #dc2626;
}
.empty-state {
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 40rpx;
background: #ffffff;
border-radius: 28rpx;
box-shadow: 0 12rpx 28rpx rgba(15, 23, 42, 0.05);
text-align: center;
gap: 16rpx;
}
.empty-icon {
font-size: 64rpx;
opacity: 0.6;
}
.empty-title {
font-size: 30rpx;
}
.empty-text {
color: #6b7280;
}
.bottom-bar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 20rpx 24rpx 36rpx;
background: rgba(244, 246, 248, 0.96);
box-shadow: 0 -8rpx 24rpx rgba(15, 23, 42, 0.05);
}
.add-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
border-radius: 999rpx;
background: linear-gradient(135deg, #ff8a65 0%, #ff7043 100%);
color: #ffffff;
font-size: 30rpx;
font-weight: 600;
border: none;
}
</style>

View File

@@ -0,0 +1,289 @@
<template>
<view class="map-page">
<view class="search-card">
<input v-model="keyword" class="search-input" placeholder="搜索小区、医院、养老院、街道" @input="handleKeywordInput" />
</view>
<map
class="address-map"
:latitude="latitude"
:longitude="longitude"
:markers="markers"
:show-location="true"
:scale="16"
></map>
<scroll-view class="poi-scroll" scroll-y="true">
<view class="poi-card">
<text class="poi-title">附近地址</text>
<view
v-for="item in poiList"
:key="item.id"
:class="['poi-item', selectedPoiId == item.id ? 'poi-item-selected' : '']"
@click="selectPoi(item.id)"
>
<text class="poi-name">{{ item.name }}</text>
<text class="poi-address">{{ item.address }}</text>
</view>
</view>
</scroll-view>
<view class="bottom-bar">
<button class="confirm-btn" @click="confirmAddress">确认地址</button>
</view>
</view>
</template>
<script setup lang="uts">
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { HomeServiceSelectedAddressType } from '@/types/home-service.uts'
const SELECTED_KEY = 'hss_selected_service_address'
const MAP_DRAFT_KEY = 'hss_service_address_map_draft'
type MapPoiItemType = {
id: string
name: string
address: string
latitude: number
longitude: number
}
const keyword = ref('')
const latitude = ref(24.28859)
const longitude = ref(116.12264)
const selectedPoiId = ref('poi-current')
const poiList = ref<Array<MapPoiItemType>>([])
const markers = ref<Array<UTSJSONObject>>([])
function buildMarkers(): Array<UTSJSONObject> {
const result: Array<UTSJSONObject> = []
const selected = getSelectedPoi()
if (selected == null) {
return result
}
const marker = new UTSJSONObject()
marker.set('id', 1)
marker.set('latitude', selected.latitude)
marker.set('longitude', selected.longitude)
marker.set('title', selected.name)
result.push(marker)
return result
}
function createDefaultPoiList(selected: HomeServiceSelectedAddressType | null): Array<MapPoiItemType> {
const locationName = selected != null && selected.locationName != null && selected.locationName != '' ? selected.locationName : '当前定位'
const locationAddress = selected != null && selected.locationAddress != null && selected.locationAddress != '' ? selected.locationAddress : '请通过系统地图继续完善地址搜索'
const baseLatitude = selected != null && selected.latitude != 0 ? selected.latitude : latitude.value
const baseLongitude = selected != null && selected.longitude != 0 ? selected.longitude : longitude.value
const result: Array<MapPoiItemType> = []
result.push({
id: 'poi-current',
name: locationName,
address: locationAddress,
latitude: baseLatitude,
longitude: baseLongitude
})
result.push({
id: 'poi-near-1',
name: locationName + '附近入口',
address: locationAddress,
latitude: baseLatitude + 0.0006,
longitude: baseLongitude + 0.0006
})
result.push({
id: 'poi-near-2',
name: locationName + '周边推荐',
address: locationAddress,
latitude: baseLatitude - 0.0006,
longitude: baseLongitude - 0.0006
})
return result
}
function getSelectedPoi(): MapPoiItemType | null {
for (let i = 0; i < poiList.value.length; i++) {
if (poiList.value[i].id == selectedPoiId.value) {
return poiList.value[i]
}
}
return poiList.value.length > 0 ? poiList.value[0] : null
}
function selectPoi(poiId: string): void {
selectedPoiId.value = poiId
const selected = getSelectedPoi()
if (selected == null) {
return
}
latitude.value = selected.latitude
longitude.value = selected.longitude
markers.value = buildMarkers()
}
function handleKeywordInput(): void {
if (keyword.value.trim() == '') {
return
}
const base = getSelectedPoi()
if (base == null) {
return
}
const dynamicList: Array<MapPoiItemType> = []
dynamicList.push({
id: 'poi-search-1',
name: keyword.value.trim(),
address: base.address,
latitude: base.latitude,
longitude: base.longitude
})
dynamicList.push(base)
poiList.value = dynamicList
selectedPoiId.value = 'poi-search-1'
markers.value = buildMarkers()
}
function confirmAddress(): void {
const selected = getSelectedPoi()
if (selected == null) {
uni.showToast({ title: '请先选择地址', icon: 'none' })
return
}
const draft = {
addressId: '',
userId: '',
isDefault: true,
contactName: '',
contactPhone: '',
phone: '',
addressName: selected.name,
locationName: selected.name,
addressDetail: selected.address,
locationAddress: selected.address,
houseNumber: '',
doorNo: '',
fullAddress: selected.address,
latitude: selected.latitude,
longitude: selected.longitude,
remark: '',
coordinateType: 'gcj02',
createdAt: Date.now(),
updatedAt: Date.now()
} as HomeServiceSelectedAddressType
uni.setStorageSync(MAP_DRAFT_KEY, JSON.stringify(draft))
uni.navigateBack()
}
onLoad(() => {
const selected = uni.getStorageSync(SELECTED_KEY) as HomeServiceSelectedAddressType | null
if (selected != null && selected.latitude != 0) {
latitude.value = selected.latitude
longitude.value = selected.longitude
}
poiList.value = createDefaultPoiList(selected)
markers.value = buildMarkers()
})
</script>
<style scoped>
.map-page {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #f4f6f8;
display: flex;
flex-direction: column;
padding: 24rpx 24rpx 180rpx;
box-sizing: border-box;
}
.search-card,
.poi-card {
background: #ffffff;
border-radius: 28rpx;
padding: 24rpx;
box-shadow: 0 12rpx 28rpx rgba(15, 23, 42, 0.05);
}
.search-input {
background: #f8fafc;
border-radius: 999rpx;
padding: 18rpx 24rpx;
font-size: 26rpx;
color: #1f2937;
}
.address-map {
width: 100%;
height: 45vh;
border-radius: 28rpx;
overflow: hidden;
margin-top: 20rpx;
margin-bottom: 20rpx;
}
.poi-scroll {
flex: 1;
min-height: 0;
}
.poi-title,
.poi-name {
color: #1f2937;
}
.poi-title {
font-size: 28rpx;
font-weight: 600;
margin-bottom: 12rpx;
}
.poi-item {
padding: 18rpx 0;
border-top: 1rpx solid #eef2f7;
display: flex;
flex-direction: column;
gap: 8rpx;
}
.poi-item-selected {
border-left: 6rpx solid #f97316;
padding-left: 18rpx;
}
.poi-name {
font-size: 26rpx;
font-weight: 600;
}
.poi-address {
font-size: 24rpx;
color: #4b5563;
line-height: 1.6;
}
.bottom-bar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 20rpx 24rpx 36rpx;
background: rgba(244, 246, 248, 0.96);
box-shadow: 0 -8rpx 24rpx rgba(15, 23, 42, 0.05);
}
.confirm-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
border-radius: 999rpx;
background: linear-gradient(135deg, #ff8a65 0%, #ff7043 100%);
color: #ffffff;
font-size: 30rpx;
font-weight: 600;
border: none;
}
</style>

View File

@@ -1,35 +1,35 @@
<template> <template>
<view class="cart-search-page"> <view class="cart-search-page">
<view class="search-header" :style="{ paddingTop: statusBarHeight + 'px', paddingRight: searchHeaderRightPadding + 'px' }"> <view class="search-header" :style="{ paddingTop: statusBarHeight + 'px', paddingRight: searchHeaderRightPadding + 'px' }">
<view class="back-btn" @click="goBack"> <view class="back-btn" @click="goBack">
<text class="back-icon"></text> <text class="back-icon">鈥?/text>
</view> </view>
<view class="search-input-wrap"> <view class="search-input-wrap">
<text class="search-icon">⌕</text> <text class="search-icon">鈱?/text>
<input <input
class="search-input" class="search-input"
v-model="keyword" v-model="keyword"
placeholder="搜索购物车商品" placeholder="鎼滅储璐墿杞﹀晢鍝?
confirm-type="search" confirm-type="search"
:focus="true" :focus="true"
@confirm="doSearch" @confirm="doSearch"
/> />
<view v-if="keyword.length > 0" class="clear-keyword" @click="clearKeyword"> <view v-if="keyword.length > 0" class="clear-keyword" @click="clearKeyword">
<text class="clear-keyword-text">×</text> <text class="clear-keyword-text"></text>
</view> </view>
</view> </view>
<view class="search-btn" @click="doSearch"> <view class="search-btn" @click="doSearch">
<text class="search-btn-text">搜索</text> <text class="search-btn-text">鎼滅储</text>
</view> </view>
</view> </view>
<scroll-view v-if="!hasSearched" class="search-content" :scroll-y="true" :show-scrollbar="false"> <scroll-view v-if="!hasSearched" class="search-content" :scroll-y="true" :show-scrollbar="false">
<view class="history-section"> <view class="history-section">
<view class="section-header"> <view class="section-header">
<text class="section-title">历史搜索</text> <text class="section-title">鍘嗗彶鎼滅储</text>
<text class="clear-history" @click="clearSearchHistory">清空</text> <text class="clear-history" @click="clearSearchHistory">娓呯┖</text>
</view> </view>
<view v-if="searchHistory.length > 0" class="history-list"> <view v-if="searchHistory.length > 0" class="history-list">
@@ -44,13 +44,13 @@
</view> </view>
<view v-else class="empty-history"> <view v-else class="empty-history">
<text class="empty-history-text">暂无历史搜索</text> <text class="empty-history-text">鏆傛棤鍘嗗彶鎼滅储</text>
</view> </view>
</view> </view>
<view class="discover-section"> <view class="discover-section">
<view class="section-header"> <view class="section-header">
<text class="section-title">搜索发现</text> <text class="section-title">鎼滅储鍙戠幇</text>
</view> </view>
<view class="discover-grid"> <view class="discover-grid">
@@ -66,10 +66,10 @@
</view> </view>
</scroll-view> </scroll-view>
<scroll-view v-else class="search-result-content" :scroll-y="true" :show-scrollbar="false"> <scroll-view v-else class="search-result-content" :scroll-y="true" :show-scrollbar="false" :lower-threshold="120" @scrolltolower="handleResultScrollToLower">
<view v-if="matchedCartItems.length > 0" class="cart-match-section"> <view v-if="matchedCartItems.length > 0" class="cart-match-section">
<view class="result-section-title-wrap"> <view class="result-section-title-wrap">
<text class="result-section-title">购物车内相关商品</text> <text class="result-section-title">璐墿杞﹀唴鐩稿叧鍟嗗搧</text>
</view> </view>
<view class="cart-result-list"> <view class="cart-result-list">
@@ -79,7 +79,7 @@
class="cart-result-card" class="cart-result-card"
> >
<view class="item-select" @click="toggleSelect(item.id)"> <view class="item-select" @click="toggleSelect(item.id)">
<text v-if="item.selected" class="selected-icon">✓</text> <text v-if="item.selected" class="selected-icon">鉁?/text>
<text v-else class="unselected-icon"></text> <text v-else class="unselected-icon"></text>
</view> </view>
@@ -90,7 +90,7 @@
<text class="item-name" :lines="1">{{ item.name }}</text> <text class="item-name" :lines="1">{{ item.name }}</text>
<text class="item-spec">{{ item.spec }}</text> <text class="item-spec">{{ item.spec }}</text>
<view class="item-footer"> <view class="item-footer">
<text class="item-price">¥{{ item.price }}</text> <text class="item-price">{{ item.price }}</text>
<view class="quantity-control"> <view class="quantity-control">
<text class="quantity-btn" @click="decreaseQuantity(item.id)">-</text> <text class="quantity-btn" @click="decreaseQuantity(item.id)">-</text>
<text class="quantity-value">{{ item.quantity }}</text> <text class="quantity-value">{{ item.quantity }}</text>
@@ -112,31 +112,17 @@
<view class="recommend-search-section"> <view class="recommend-search-section">
<view class="recommend-title-wrap"> <view class="recommend-title-wrap">
<view class="line"></view> <view class="line"></view>
<text class="recommend-title">{{ matchedCartItems.length > 0 ? '为你搜索全站商品' : '为你搜索全部商品' }}</text> <text class="recommend-title">{{ matchedCartItems.length > 0 ? '涓轰綘鎼滅储鍏ㄧ珯鍟嗗搧' : '涓轰綘鎼滅储鍏ㄩ儴鍟嗗搧' }}</text>
<view class="line"></view> <view class="line"></view>
</view> </view>
<view class="recommend-grid"> <GuessYouLike
<view title="猜你喜欢"
v-for="product in recommendProducts" :pageSize="8"
:key="product.id" :excludeProductIds="matchedProductIds"
class="recommend-card" :loadMoreKey="guessLoadMoreKey"
@click="goToProductDetail(product)" @productClick="goToRecommendProductDetail"
> />
<image class="recommend-image" :src="product.image" mode="aspectFill" />
<view class="recommend-info">
<text class="recommend-shop-tag">{{ product.shopName }}</text>
<text class="recommend-name" :lines="2">{{ product.name }}</text>
<text class="recommend-sales">{{ product.salesText }}</text>
<view class="recommend-price-row">
<text class="recommend-price">¥{{ product.price }}</text>
<view class="recommend-cart-btn" @click.stop="addRecommendToCart(product)">
<text class="recommend-cart-icon">🛒</text>
</view>
</view>
</view>
</view>
</view>
<view class="bottom-safe-space"></view> <view class="bottom-safe-space"></view>
</view> </view>
@@ -146,19 +132,19 @@
<view class="settlement-inner"> <view class="settlement-inner">
<view class="settlement-left" @click="toggleSelectAllInSearch"> <view class="settlement-left" @click="toggleSelectAllInSearch">
<view class="select-circle" :class="{ 'select-circle-active': allSearchSelected }"> <view class="select-circle" :class="{ 'select-circle-active': allSearchSelected }">
<text v-if="allSearchSelected" class="select-check">✓</text> <text v-if="allSearchSelected" class="select-check">鉁?/text>
</view> </view>
<text class="select-all-text">全选</text> <text class="select-all-text">鍏ㄩ€?/text>
</view> </view>
<view class="settlement-right"> <view class="settlement-right">
<view class="total-info"> <view class="total-info">
<text class="total-label">合计:</text> <text class="total-label">鍚堣:</text>
<text class="total-price">¥{{ searchTotalPrice }}</text> <text class="total-price">{{ searchTotalPrice }}</text>
</view> </view>
<button class="checkout-btn" :class="{ 'checkout-btn-disabled': searchSelectedCount == 0 }" @click="goToCheckoutFromSearch"> <button class="checkout-btn" :class="{ 'checkout-btn-disabled': searchSelectedCount == 0 }" @click="goToCheckoutFromSearch">
去结算({{ searchSelectedCount }}) 鍘荤粨绠?{{ searchSelectedCount }})
</button> </button>
</view> </view>
</view> </view>
@@ -171,6 +157,7 @@ import { computed, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app' import { onLoad } from '@dcloudio/uni-app'
import { supabaseService, type CartItem as SupabaseCartItem, type Product } from '@/utils/supabaseService.uts' import { supabaseService, type CartItem as SupabaseCartItem, type Product } from '@/utils/supabaseService.uts'
import { goToLogin } from '@/utils/utils.uts' import { goToLogin } from '@/utils/utils.uts'
import GuessYouLike from '@/components/mall/GuessYouLike/GuessYouLike.uvue'
const CART_SEARCH_HISTORY_KEY = 'cart_search_history' const CART_SEARCH_HISTORY_KEY = 'cart_search_history'
@@ -211,19 +198,19 @@ const keyword = ref<string>('')
const hasSearched = ref<boolean>(false) const hasSearched = ref<boolean>(false)
const searchHistory = ref<Array<string>>([]) const searchHistory = ref<Array<string>>([])
const searchDiscoverList = ref<Array<string>>([ const searchDiscoverList = ref<Array<string>>([
'无人机', '鏃犱汉鏈?,
'水杯', '姘存澂',
'手机', '鎵嬫満',
'天然泉水', '澶╃劧娉夋按',
'按摩仪', '鎸夋懇浠?,
'摄像头', '鎽勫儚澶?,
'耳机', '鑰虫満',
'停车场设备', '鍋滆溅鍦鸿澶?,
'饮料', '楗枡',
'iPhone' 'iPhone'
]) ])
const cartItems = ref<Array<LocalCartItem>>([]) const cartItems = ref<Array<LocalCartItem>>([])
const recommendProducts = ref<Array<ProductItem>>([]) const guessLoadMoreKey = ref<number>(0)
const isLoading = ref<boolean>(false) const isLoading = ref<boolean>(false)
const statusBarHeight = ref<number>(0) const statusBarHeight = ref<number>(0)
const updatingItems = ref<Set<string>>(new Set()) const updatingItems = ref<Set<string>>(new Set())
@@ -233,7 +220,7 @@ const searchDiscoverWords = computed<Array<string>>(() => {
if (searchDiscoverList.value.length > 0) { if (searchDiscoverList.value.length > 0) {
return searchDiscoverList.value return searchDiscoverList.value
} }
return ['无人机', '水杯', '手机', '天然泉水'] return ['鏃犱汉鏈?, '姘存澂', '鎵嬫満', '澶╃劧娉夋按']
}) })
const safeLower = (value: string): string => { const safeLower = (value: string): string => {
@@ -246,8 +233,8 @@ const matchedCartItems = computed<Array<LocalCartItem>>(() => {
return [] return []
} }
return cartItems.value.filter((item: LocalCartItem) => { return cartItems.value.filter((item: LocalCartItem) => {
const title = safeLower(item.name) const title = safeLower(item.name)
const name = safeLower(item.name) const name = safeLower(item.name)
const productName = safeLower(item.productName) const productName = safeLower(item.productName)
const skuName = safeLower(item.skuName) const skuName = safeLower(item.skuName)
@@ -256,17 +243,28 @@ const matchedCartItems = computed<Array<LocalCartItem>>(() => {
const merchantName = safeLower(item.merchantName) const merchantName = safeLower(item.merchantName)
const brandName = safeLower(item.brandName) const brandName = safeLower(item.brandName)
return title.indexOf(q) >= 0 return title.indexOf(q) >= 0
|| name.indexOf(q) >= 0 || name.indexOf(q) >= 0
|| productName.indexOf(q) >= 0 || productName.indexOf(q) >= 0
|| skuName.indexOf(q) >= 0 || skuName.indexOf(q) >= 0
|| specName.indexOf(q) >= 0 || specName.indexOf(q) >= 0
|| shopName.indexOf(q) >= 0 || shopName.indexOf(q) >= 0
|| merchantName.indexOf(q) >= 0 || merchantName.indexOf(q) >= 0
|| brandName.indexOf(q) >= 0 || brandName.indexOf(q) >= 0
})
}) })
})
const matchedProductIds = computed<Array<string>>(() => {
const ids: Array<string> = []
for (let i = 0; i < matchedCartItems.value.length; i++) {
const item = matchedCartItems.value[i]
const productId = item.productId !== '' ? item.productId : item.id
if (productId !== '' && ids.indexOf(productId) < 0) {
ids.push(productId)
}
}
return ids
})
const searchSelectedItems = computed<Array<LocalCartItem>>(() => { const searchSelectedItems = computed<Array<LocalCartItem>>(() => {
return matchedCartItems.value.filter((item: LocalCartItem) => item.selected == true) return matchedCartItems.value.filter((item: LocalCartItem) => item.selected == true)
}) })
@@ -291,9 +289,9 @@ const allSearchSelected = computed((): boolean => {
const noCartResultText = computed((): string => { const noCartResultText = computed((): string => {
if (cartItems.value.length == 0) { if (cartItems.value.length == 0) {
return '购物车为空,暂无相关商品' return '璐墿杞︿负绌猴紝鏆傛棤鐩稿叧鍟嗗搧'
} }
return '您的购物车里没有相关商品' return '鎮ㄧ殑璐墿杞﹂噷娌℃湁鐩稿叧鍟嗗搧'
}) })
const searchHeaderRightPadding = computed((): number => { const searchHeaderRightPadding = computed((): number => {
@@ -326,7 +324,7 @@ const loadSearchHistory = () => {
const parsed = JSON.parse(cache) as Array<string> const parsed = JSON.parse(cache) as Array<string>
searchHistory.value = parsed searchHistory.value = parsed
} catch (e) { } catch (e) {
console.error('解析搜索历史失败:', e) console.error('瑙f瀽鎼滅储鍘嗗彶澶辫触:', e)
searchHistory.value = [] searchHistory.value = []
} }
} }
@@ -352,81 +350,6 @@ const clearSearchHistory = () => {
uni.removeStorageSync(CART_SEARCH_HISTORY_KEY) uni.removeStorageSync(CART_SEARCH_HISTORY_KEY)
} }
const mockRecommendProducts = (q: string): Array<ProductItem> => {
const text = q.trim() == '' ? '热卖好物' : q
return [
{
id: 'mock-1',
name: text + ' 便携款',
price: 99,
image: '/static/images/default.png',
shopName: '平台精选',
salesText: '已售 200+',
merchantId: '',
skuId: ''
},
{
id: 'mock-2',
name: text + ' 升级版',
price: 159,
image: '/static/images/default.png',
shopName: '品牌旗舰',
salesText: '好评 98%',
merchantId: '',
skuId: ''
},
{
id: 'mock-3',
name: text + ' 热销套装',
price: 239,
image: '/static/images/default.png',
shopName: '今日推荐',
salesText: '月销 500+',
merchantId: '',
skuId: ''
},
{
id: 'mock-4',
name: text + ' 家用精选',
price: 79,
image: '/static/images/default.png',
shopName: '官方自营',
salesText: '已售 1200+',
merchantId: '',
skuId: ''
}
]
}
const loadRecommendProducts = async (q: string) => {
isLoading.value = true
try {
const result = await supabaseService.searchProducts(q, 1, 8, 'sales')
if (result.data.length > 0) {
recommendProducts.value = result.data.map((product: Product): ProductItem => {
const saleCount = product.sale_count ?? product.sales ?? 0
return {
id: product.id,
name: product.name,
price: product.base_price ?? product.market_price ?? 0,
image: product.main_image_url ?? product.image_url ?? '/static/images/default.png',
shopName: product.shop_name ?? '平台精选',
salesText: '已售 ' + saleCount + '+',
merchantId: product.merchant_id ?? '',
skuId: ''
}
})
} else {
recommendProducts.value = mockRecommendProducts(q)
}
} catch (e) {
console.error('加载推荐商品失败:', e)
recommendProducts.value = mockRecommendProducts(q)
} finally {
isLoading.value = false
}
}
const loadCartData = async () => { const loadCartData = async () => {
isLoading.value = true isLoading.value = true
try { try {
@@ -438,7 +361,7 @@ const loadCartData = async () => {
memberDiscount = discountRaw as number memberDiscount = discountRaw as number
} }
} catch (e) { } catch (e) {
console.log('获取会员信息失败,使用默认折扣:', e) console.log('鑾峰彇浼氬憳淇℃伅澶辫触锛屼娇鐢ㄩ粯璁ゆ姌鎵?', e)
} }
const supabaseCartItems = await supabaseService.getCartItems() const supabaseCartItems = await supabaseService.getCartItems()
@@ -449,18 +372,18 @@ const loadCartData = async () => {
memberPrice = Math.round(originalPrice * memberDiscount * 100) / 100 memberPrice = Math.round(originalPrice * memberDiscount * 100) / 100
} }
const productName = item.product_name ?? '未知商品' const productName = item.product_name ?? '鏈煡鍟嗗搧'
const specName = item.product_specification ?? '标准规格' const specName = item.product_specification ?? '鏍囧噯瑙勬牸'
return { return {
id: item.id, id: item.id,
shopId: item.shop_id ?? 'default_shop', shopId: item.shop_id ?? 'default_shop',
shopName: item.shop_name ?? '商城优选', shopName: item.shop_name ?? '鍟嗗煄浼橀€?,
name: productName, name: productName,
productName: productName, productName: productName,
skuName: specName, skuName: specName,
specName: specName, specName: specName,
merchantName: item.shop_name ?? '商城优选', merchantName: item.shop_name ?? '鍟嗗煄浼橀€?,
brandName: '', brandName: '',
price: originalPrice, price: originalPrice,
originalPrice: originalPrice, originalPrice: originalPrice,
@@ -475,7 +398,7 @@ const loadCartData = async () => {
} }
}) })
} catch (e) { } catch (e) {
console.error('加载购物车搜索数据失败:', e) console.error('鍔犺浇璐墿杞︽悳绱㈡暟鎹け璐?', e)
cartItems.value = [] cartItems.value = []
} finally { } finally {
isLoading.value = false isLoading.value = false
@@ -486,7 +409,7 @@ const doSearch = async () => {
const q = keyword.value.trim() const q = keyword.value.trim()
if (q == '') { if (q == '') {
uni.showToast({ uni.showToast({
title: '请输入搜索关键词', title: '璇疯緭鍏ユ悳绱㈠叧閿瘝',
icon: 'none' icon: 'none'
}) })
return return
@@ -494,7 +417,6 @@ const doSearch = async () => {
hasSearched.value = true hasSearched.value = true
saveSearchHistory(q) saveSearchHistory(q)
await loadRecommendProducts(q)
} }
const useSearchWord = (word: string) => { const useSearchWord = (word: string) => {
@@ -509,7 +431,7 @@ const goBack = () => {
const clearKeyword = () => { const clearKeyword = () => {
keyword.value = '' keyword.value = ''
hasSearched.value = false hasSearched.value = false
recommendProducts.value = [] guessLoadMoreKey.value = 0
} }
const toggleSelect = async (itemId: string) => { const toggleSelect = async (itemId: string) => {
@@ -524,7 +446,7 @@ const toggleSelect = async (itemId: string) => {
if (!success) { if (!success) {
cartItems.value[index].selected = !newSelected cartItems.value[index].selected = !newSelected
cartItems.value = [...cartItems.value] cartItems.value = [...cartItems.value]
uni.showToast({ title: '网络异常,请重试', icon: 'none' }) uni.showToast({ title: '缃戠粶寮傚父锛岃閲嶈瘯', icon: 'none' })
} }
} }
@@ -543,7 +465,7 @@ const increaseQuantity = async (itemId: string) => {
if (!success) { if (!success) {
cartItems.value[index].quantity = newQuantity - 1 cartItems.value[index].quantity = newQuantity - 1
cartItems.value = [...cartItems.value] cartItems.value = [...cartItems.value]
uni.showToast({ title: '更新失败', icon: 'none' }) uni.showToast({ title: '鏇存柊澶辫触', icon: 'none' })
} }
} }
@@ -553,7 +475,7 @@ const decreaseQuantity = async (itemId: string) => {
if (index == -1) return if (index == -1) return
if (cartItems.value[index].quantity <= 1) { if (cartItems.value[index].quantity <= 1) {
uni.showToast({ title: '最少保留1件可返回购物车删除', icon: 'none' }) uni.showToast({ title: '鏈€灏戜繚鐣?浠讹紝鍙繑鍥炶喘鐗╄溅鍒犻櫎', icon: 'none' })
return return
} }
@@ -567,7 +489,7 @@ const decreaseQuantity = async (itemId: string) => {
if (!success) { if (!success) {
cartItems.value[index].quantity = newQuantity + 1 cartItems.value[index].quantity = newQuantity + 1
cartItems.value = [...cartItems.value] cartItems.value = [...cartItems.value]
uni.showToast({ title: '更新失败', icon: 'none' }) uni.showToast({ title: '鏇存柊澶辫触', icon: 'none' })
} }
} }
@@ -587,14 +509,14 @@ const toggleSelectAllInSearch = async () => {
item.selected = !checked item.selected = !checked
}) })
cartItems.value = [...cartItems.value] cartItems.value = [...cartItems.value]
uni.showToast({ title: '操作失败', icon: 'none' }) uni.showToast({ title: '鎿嶄綔澶辫触', icon: 'none' })
} }
} }
const goToCheckoutFromSearch = () => { const goToCheckoutFromSearch = () => {
if (searchSelectedCount.value == 0) { if (searchSelectedCount.value == 0) {
uni.showToast({ uni.showToast({
title: '请选择商品', title: '璇烽€夋嫨鍟嗗搧',
icon: 'none' icon: 'none'
}) })
return return
@@ -620,8 +542,8 @@ const goToCheckoutFromSearch = () => {
try { try {
uni.setStorageSync('checkout_items', JSON.stringify(selectedItems)) uni.setStorageSync('checkout_items', JSON.stringify(selectedItems))
} catch (e) { } catch (e) {
console.error('存储结算数据失败', e) console.error('瀛樺偍缁撶畻鏁版嵁澶辫触', e)
uni.showToast({ title: '系统异常,请重试', icon: 'none' }) uni.showToast({ title: '绯荤粺寮傚父锛岃閲嶈瘯', icon: 'none' })
return return
} }
@@ -653,38 +575,17 @@ const goToProductDetail = (product: ProductItem) => {
uni.navigateTo({ url }) uni.navigateTo({ url })
} }
const addRecommendToCart = async (product: ProductItem) => { const goToRecommendProductDetail = (productId: string) => {
const userId = supabaseService.getCurrentUserId() if (productId === '') {
if (userId == null || userId == '') {
goToLogin('/pages/main/cart-search/cart-search')
return return
} }
uni.navigateTo({
url: '/pages/mall/consumer/product-detail?id=' + encodeURIComponent(productId) + '&productId=' + encodeURIComponent(productId)
})
}
uni.showLoading({ title: '添加中...' }) const handleResultScrollToLower = () => {
try { guessLoadMoreKey.value = guessLoadMoreKey.value + 1
const skus = await supabaseService.getProductSkus(product.id)
if (skus.length > 0) {
uni.hideLoading()
uni.showToast({ title: '请选择规格', icon: 'none' })
setTimeout(() => {
goToProductDetail(product)
}, 400)
return
}
const success = await supabaseService.addToCart(product.id, 1, product.skuId, product.merchantId)
uni.hideLoading()
if (success) {
uni.showToast({ title: '已添加到购物车', icon: 'success' })
loadCartData()
} else {
uni.showToast({ title: '添加失败', icon: 'none' })
}
} catch (e) {
console.error('推荐商品加入购物车失败:', e)
uni.hideLoading()
uni.showToast({ title: '添加失败', icon: 'none' })
}
} }
onLoad((options: UTSJSONObject) => { onLoad((options: UTSJSONObject) => {
@@ -698,7 +599,7 @@ onLoad((options: UTSJSONObject) => {
navBarRight.value = (systemInfo.screenWidth - menuButton.left) + 10 navBarRight.value = (systemInfo.screenWidth - menuButton.left) + 10
} }
} catch (e) { } catch (e) {
console.log('获取胶囊按钮信息失败:', e) console.log('鑾峰彇鑳跺泭鎸夐挳淇℃伅澶辫触:', e)
navBarRight.value = 96 navBarRight.value = 96
} }
// #endif // #endif
@@ -1304,4 +1205,4 @@ onLoad((options: UTSJSONObject) => {
font-size: 14px; font-size: 14px;
} }
} }
</style> </style>

View File

@@ -174,40 +174,13 @@
</view> </view>
</view> </view>
<!-- 推荐商品 --> <GuessYouLike
<view v-if="currentCartType == 'goods' && recommendProducts.length > 0" class="recommend-section"> v-if="currentCartType == 'goods'"
<view class="section-header"> title="猜你喜欢"
<text class="section-title">猜你喜欢</text> :pageSize="8"
</view> :loadMoreKey="guessLoadMoreKey"
<view class="recommend-list"> @productClick="handleGuessProductClick"
<view />
v-for="product in recommendProducts"
:key="product.id"
class="recommend-item"
@click="navigateToProduct(product)"
>
<view class="recommend-image-wrapper">
<image
class="recommend-image"
:src="product.image"
mode="aspectFill"
/>
</view>
<text class="recommend-name" :lines="2">{{ product.name }}</text>
<view class="recommend-bottom">
<text class="recommend-price">¥{{ product.price }}</text>
<view class="recommend-add-btn" @click.stop="addToCart(product)">
<text class="recommend-add-icon">+</text>
</view>
</view>
</view>
</view>
<view class="recommend-load-more" @click="loadRecommendProducts(false)">
<text v-if="recommendLoading" class="recommend-load-text">正在加载更多...</text>
<text v-else-if="!recommendHasMore && recommendInitialized" class="recommend-load-text">没有更多了</text>
<text v-else class="recommend-load-text">上拉加载更多</text>
</view>
</view>
<!-- 底部占位符:确保内容不被原生 TabBar 遮挡 --> <!-- 底部占位符:确保内容不被原生 TabBar 遮挡 -->
<view class="tabbar-safe-area"></view> <view class="tabbar-safe-area"></view>
</scroll-view> </scroll-view>
@@ -280,6 +253,7 @@ import { ref, computed, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app' import { onShow } from '@dcloudio/uni-app'
import { supabaseService, type CartItem as SupabaseCartItem, type Product } from '@/utils/supabaseService.uts' import { supabaseService, type CartItem as SupabaseCartItem, type Product } from '@/utils/supabaseService.uts'
import { goToLogin } from '@/utils/utils.uts' import { goToLogin } from '@/utils/utils.uts'
import GuessYouLike from '@/components/mall/GuessYouLike/GuessYouLike.uvue'
type ModalSuccess = { confirm: boolean; cancel: boolean } type ModalSuccess = { confirm: boolean; cancel: boolean }
@@ -375,6 +349,7 @@ const recommendBottomLocked = ref<boolean>(false)
const recommendViewportHeight = ref<number>(0) const recommendViewportHeight = ref<number>(0)
const pageWindowHeight = ref<number>(0) const pageWindowHeight = ref<number>(0)
const isAndroidApp = ref<boolean>(false) const isAndroidApp = ref<boolean>(false)
const guessLoadMoreKey = ref<number>(0)
const serviceMockItems = ref<ServiceCartItem[]>([ const serviceMockItems = ref<ServiceCartItem[]>([
{ {
id: 'service-1', id: 'service-1',
@@ -725,27 +700,6 @@ const mergeRecommendProducts = (oldList: RecommendProduct[], newList: RecommendP
return result return result
} }
const mockRecommendProducts = (page: number, pageSize: number): RecommendProduct[] => {
const list: RecommendProduct[] = []
if (page >= 5) {
return list
}
for (let i = 0; i < pageSize; i++) {
const index = (page - 1) * pageSize + i + 1
list.push({
id: 'mock-recommend-' + index,
shopId: 'mock-shop-' + index,
shopName: '商城推荐',
name: '猜你喜欢 推荐商品 ' + index,
price: 9.9 + index,
image: '/static/images/default.png',
skuId: '',
merchant_id: ''
})
}
return list
}
const fetchRecommendProducts = async (page: number, pageSize: number): Promise<RecommendProduct[]> => { const fetchRecommendProducts = async (page: number, pageSize: number): Promise<RecommendProduct[]> => {
console.log('[cart推荐] fetchRecommendProducts 请求 page=', page, 'pageSize=', pageSize) console.log('[cart推荐] fetchRecommendProducts 请求 page=', page, 'pageSize=', pageSize)
const hotResp = await supabaseService.searchProducts('', page, pageSize, 'sales') const hotResp = await supabaseService.searchProducts('', page, pageSize, 'sales')
@@ -851,13 +805,7 @@ async function loadRecommendProducts(reset: boolean): Promise<void> {
recommendInitialized.value = true recommendInitialized.value = true
} catch (error) { } catch (error) {
console.error('加载推荐商品失败:', error) console.error('加载推荐商品失败:', error)
if (reset && recommendProducts.value.length === 0) { recommendHasMore.value = false
const mockList = mockRecommendProducts(1, recommendPageSize.value)
recommendProducts.value = mockList
recommendPage.value = 2
recommendHasMore.value = true
recommendInitialized.value = true
}
} finally { } finally {
recommendLoading.value = false recommendLoading.value = false
if (!reset && recommendPendingLoad.value && recommendHasMore.value) { if (!reset && recommendPendingLoad.value && recommendHasMore.value) {
@@ -869,59 +817,13 @@ async function loadRecommendProducts(reset: boolean): Promise<void> {
} }
function onRecommendScrollToLower(): void { function onRecommendScrollToLower(): void {
console.log('[cart推荐] scrolltolower 触发 currentCartType=', currentCartType.value, 'initialized=', recommendInitialized.value) if (currentCartType.value == 'goods') {
if (currentCartType.value != 'goods') { guessLoadMoreKey.value = guessLoadMoreKey.value + 1
console.log('[cart推荐] 跳过:当前不是 goods')
return
} }
recommendBottomLocked.value = true
loadRecommendProducts(false)
} }
function onRecommendScroll(event: any): void { function onRecommendScroll(event: any): void {
if (currentCartType.value != 'goods' || recommendLoading.value || !recommendHasMore.value) { return
return
}
try {
const eventObj = toRecommendScrollJson(event)
let detailObj: UTSJSONObject | null = null
if (eventObj != null) {
detailObj = toRecommendScrollJson(eventObj.get('detail'))
}
if (detailObj == null) {
return
}
const scrollTop = readRecommendScrollMetric(detailObj, 'scrollTop')
const scrollHeight = readRecommendScrollMetric(detailObj, 'scrollHeight')
let clientHeight = readRecommendScrollMetric(detailObj, 'clientHeight')
if (clientHeight <= 0) {
clientHeight = recommendViewportHeight.value
}
console.log('[cart推荐] scroll事件 scrollTop=', scrollTop, 'scrollHeight=', scrollHeight, 'clientHeight=', clientHeight)
if (scrollHeight <= 0 || clientHeight <= 0) {
return
}
const distanceToBottom = scrollHeight - scrollTop - clientHeight
if (distanceToBottom > 260) {
recommendBottomLocked.value = false
}
if (distanceToBottom <= 180) {
console.log('[cart推荐] scroll 兜底触底 distanceToBottom=', distanceToBottom)
if (recommendBottomLocked.value) {
recommendPendingLoad.value = true
return
}
recommendBottomLocked.value = true
loadRecommendProducts(false)
}
} catch (e) {
console.error('[cart推荐] 处理推荐滚动失败:', e)
}
} }
// 加载数据 // 加载数据
@@ -981,9 +883,6 @@ const loadCartData = async () => {
console.log('Transformed items count:', transformedItems.length); console.log('Transformed items count:', transformedItems.length);
cartItems.value = transformedItems cartItems.value = transformedItems
if (!recommendInitialized.value) {
await loadRecommendProducts(true)
}
} catch (error) { } catch (error) {
console.error('加载购物车数据失败:', error) console.error('加载购物车数据失败:', error)
cartItems.value = [] cartItems.value = []
@@ -996,6 +895,10 @@ onShow(() => {
loadCartData() loadCartData()
}) })
const handleGuessProductClick = (productId: string) => {
navigateToProduct({ id: productId, productId: productId, price: 0 })
}
// 商品操作 - 更新选中状态到Supabase // 商品操作 - 更新选中状态到Supabase
const toggleSelect = async (itemId: string) => { const toggleSelect = async (itemId: string) => {
// 乐观更新 // 乐观更新
@@ -2593,4 +2496,3 @@ const goToCheckout = () => {
background-color: transparent; background-color: transparent;
} }
</style> </style>

View File

@@ -436,68 +436,6 @@ function buildServiceImageText(categoryId: string): string {
return '服' return '服'
} }
function buildMockServiceProducts(): Array<HomeCareServiceProductType> {
// TODO: 后续替换为服务首页专用接口,当前仅在真实服务目录为空时兜底。
return [
{
id: 'svc-001',
title: '基础上门照护',
subtitle: '协助起居、日常陪护、健康观察',
categoryId: 'basic_care',
price: 99,
unit: '次',
tags: ['平台认证', '可预约'],
salesText: '已服务230+',
imageText: '护',
coverGradient: getServiceGradient('basic_care'),
detailPath: '/pages/mall/consumer/home-service/service-detail?id=svc-001',
bookingPath: '/pages/mall/consumer/home-service/service-detail?id=svc-001&mode=booking'
},
{
id: 'svc-002',
title: '居家康复指导',
subtitle: '术后恢复、动作训练、康复评估',
categoryId: 'rehab',
price: 129,
unit: '次',
tags: ['康复指导', '上门服务'],
salesText: '已服务180+',
imageText: '康',
coverGradient: getServiceGradient('rehab'),
detailPath: '/pages/mall/consumer/home-service/service-detail?id=svc-002',
bookingPath: '/pages/mall/consumer/home-service/service-detail?id=svc-002&mode=booking'
},
{
id: 'svc-mock-escort',
title: '陪诊陪护服务',
subtitle: '挂号陪同、检查陪同、取药协助',
categoryId: 'escort',
price: 168,
unit: '次',
tags: ['陪诊服务', '安心陪护'],
salesText: '已服务320+',
imageText: '陪',
coverGradient: getServiceGradient('escort'),
detailPath: '',
bookingPath: ''
},
{
id: 'svc-003',
title: '慢病随访服务',
subtitle: '血压血糖记录、健康建议、定期回访',
categoryId: 'chronic',
price: 79,
unit: '次',
tags: ['慢病管理', '健康随访'],
salesText: '已服务150+',
imageText: '访',
coverGradient: getServiceGradient('chronic'),
detailPath: '/pages/mall/consumer/home-service/service-detail?id=svc-003',
bookingPath: '/pages/mall/consumer/home-service/service-detail?id=svc-003&mode=booking'
}
]
}
function buildServiceProductsFromCatalog(catalog: Array<HomeServiceCatalogType>): Array<HomeCareServiceProductType> { function buildServiceProductsFromCatalog(catalog: Array<HomeServiceCatalogType>): Array<HomeCareServiceProductType> {
const result: Array<HomeCareServiceProductType> = [] const result: Array<HomeCareServiceProductType> = []
for (let i = 0; i < catalog.length; i++) { for (let i = 0; i < catalog.length; i++) {
@@ -525,14 +463,10 @@ async function loadServiceHomeData(): Promise<void> {
serviceLoading.value = true serviceLoading.value = true
try { try {
const catalog = await fetchHomeServiceCatalog() const catalog = await fetchHomeServiceCatalog()
if (catalog.length > 0) { allServiceProducts.value = buildServiceProductsFromCatalog(catalog)
allServiceProducts.value = buildServiceProductsFromCatalog(catalog)
} else {
allServiceProducts.value = buildMockServiceProducts()
}
} catch (error) { } catch (error) {
console.error('加载服务首页数据失败', error) console.error('加载服务首页数据失败', error)
allServiceProducts.value = buildMockServiceProducts() allServiceProducts.value = [] as Array<HomeCareServiceProductType>
} finally { } finally {
serviceLoading.value = false serviceLoading.value = false
} }
@@ -620,6 +554,8 @@ const hotProducts = ref<Product[]>([])
const recommendedProducts = ref<Product[]>([]) const recommendedProducts = ref<Product[]>([])
const hotKeywords = ref<string[]>([]) const hotKeywords = ref<string[]>([])
const defaultLoadLimit: number = 6 const defaultLoadLimit: number = 6
const recommendChannelLoadLimit: number = 16
const categoryChannelLoadLimit: number = 12
// 屏幕尺寸检测 // 屏幕尺寸检测
const isMobile = ref(false) const isMobile = ref(false)
@@ -922,10 +858,376 @@ function buildSimpleChannelCoverImages(startIndex: number): string[] {
return covers return covers
} }
function buildSimpleCategoryChannels(categoryId: string): SimpleCategoryChannel[] { function getRealProductImage(product: Product): string {
return [] if (product.main_image_url != null && product.main_image_url !== '') {
return product.main_image_url
}
if (product.images != null && product.images.length > 0 && product.images[0] !== '') {
return product.images[0]
}
if (product.image_url != null && product.image_url !== '') {
return product.image_url
}
return '/static/images/default.png'
} }
function getRealSalePrice(product: Product): number {
return product.base_price ?? product.price ?? 0
}
function getRealMarketPrice(product: Product): number {
return product.market_price ?? product.original_price ?? 0
}
function toChannelProduct(product: Product, labelPrefix: string): ChannelProduct {
const salePrice = getRealSalePrice(product)
const marketPrice = getRealMarketPrice(product)
const shortName = product.short_title != null && product.short_title !== ''
? product.short_title
: (product.name != null && product.name !== '' ? product.name : product.id)
return {
id: product.id,
name: product.name != null && product.name !== '' ? product.name : product.id,
shortName,
image: getRealProductImage(product),
price: salePrice,
marketPrice,
tag: labelPrefix
} as ChannelProduct
}
function getProductDiscountScore(product: Product): number {
const salePrice = getRealSalePrice(product)
const marketPrice = getRealMarketPrice(product)
if (marketPrice <= salePrice || marketPrice <= 0) {
return 0
}
const discountValue = marketPrice - salePrice
const discountRate = discountValue / marketPrice
return discountRate * 100000 + discountValue
}
function getProductQualityScore(product: Product): number {
let score = 0
if (product.is_featured == true) {
score = score + 100000
}
if (product.is_hot == true) {
score = score + 50000
}
score = score + (product.sale_count ?? 0)
return score
}
function getProductHotScore(product: Product): number {
let score = product.sale_count ?? 0
if (product.is_hot == true) {
score = score + 100000
}
if (product.is_featured == true) {
score = score + 50000
}
score = score + getProductDiscountScore(product)
return score
}
function cloneProductArray(source: Array<Product>): Array<Product> {
const result: Array<Product> = []
for (let i = 0; i < source.length; i++) {
result.push(source[i])
}
return result
}
function sortProductsByScoreDesc(source: Array<Product>, scoreType: string): Array<Product> {
const result = cloneProductArray(source)
for (let i = 0; i < result.length; i++) {
for (let j = i + 1; j < result.length; j++) {
let leftScore = 0
let rightScore = 0
if (scoreType == 'discount') {
leftScore = getProductDiscountScore(result[i])
rightScore = getProductDiscountScore(result[j])
} else if (scoreType == 'quality') {
leftScore = getProductQualityScore(result[i])
rightScore = getProductQualityScore(result[j])
} else {
leftScore = getProductHotScore(result[i])
rightScore = getProductHotScore(result[j])
}
if (rightScore > leftScore) {
const temp = result[i]
result[i] = result[j]
result[j] = temp
}
}
}
return result
}
function sortProductsByPriceAsc(source: Array<Product>): Array<Product> {
const result = cloneProductArray(source)
for (let i = 0; i < result.length; i++) {
for (let j = i + 1; j < result.length; j++) {
const leftPrice = getRealSalePrice(result[i])
const rightPrice = getRealSalePrice(result[j])
if (rightPrice < leftPrice) {
const temp = result[i]
result[i] = result[j]
result[j] = temp
}
}
}
return result
}
function filterProductsByMode(source: Array<Product>, mode: string): Array<Product> {
const result: Array<Product> = []
for (let i = 0; i < source.length; i++) {
const item = source[i]
const salePrice = getRealSalePrice(item)
const marketPrice = getRealMarketPrice(item)
if (mode == 'discount' && marketPrice > salePrice) {
result.push(item)
continue
}
if (mode == 'quality' && (item.is_featured == true || item.is_hot == true)) {
result.push(item)
continue
}
if (mode == 'cheap-9' && salePrice > 0 && salePrice <= 9.9) {
result.push(item)
continue
}
if (mode == 'cheap-19' && salePrice > 0 && salePrice <= 19.9) {
result.push(item)
continue
}
if (mode == 'live' && (item.is_hot == true || (item.sale_count ?? 0) > 0)) {
result.push(item)
}
}
return result
}
function mergeUniqueProductLists(first: Array<Product>, second: Array<Product>, third: Array<Product>): Array<Product> {
const result: Array<Product> = []
const seenIds: Array<string> = []
const sources: Array<Array<Product>> = [first, second, third]
for (let sourceIndex = 0; sourceIndex < sources.length; sourceIndex++) {
const source = sources[sourceIndex]
for (let i = 0; i < source.length; i++) {
const item = source[i]
const productId = item.id ?? ''
if (productId != '' && seenIds.indexOf(productId) != -1) {
continue
}
if (productId != '') {
seenIds.push(productId)
}
result.push(item)
}
}
return result
}
function appendChannelProducts(source: Array<Product>, result: Array<Product>, selectedIds: Array<string>, desiredCount: number, allowRepeat: boolean): void {
for (let i = 0; i < source.length; i++) {
if (result.length >= desiredCount) {
return
}
const item = source[i]
const productId = item.id ?? ''
let existsInResult = false
for (let j = 0; j < result.length; j++) {
if (result[j].id == productId) {
existsInResult = true
break
}
}
if (existsInResult) {
continue
}
if (!allowRepeat && productId != '' && selectedIds.indexOf(productId) != -1) {
continue
}
result.push(item)
if (!allowRepeat && productId != '') {
selectedIds.push(productId)
}
}
}
function selectChannelProducts(primary: Array<Product>, secondary: Array<Product>, fallback: Array<Product>, selectedIds: Array<string>, desiredCount: number): Array<Product> {
const result: Array<Product> = []
appendChannelProducts(primary, result, selectedIds, desiredCount, false)
appendChannelProducts(secondary, result, selectedIds, desiredCount, false)
appendChannelProducts(fallback, result, selectedIds, desiredCount, false)
appendChannelProducts(primary, result, selectedIds, desiredCount, true)
appendChannelProducts(secondary, result, selectedIds, desiredCount, true)
appendChannelProducts(fallback, result, selectedIds, desiredCount, true)
return result
}
function buildChannelFromTemplate(template: MarketingChannel, products: Array<Product>, labelPrefix: string): MarketingChannel {
const mappedProducts: Array<ChannelProduct> = []
for (let i = 0; i < products.length; i++) {
mappedProducts.push(toChannelProduct(products[i], labelPrefix))
}
return {
id: template.id,
title: template.title,
subtitle: template.subtitle,
badge: template.badge,
themeColor: template.themeColor,
bgColor: template.bgColor,
routeType: template.routeType,
layoutType: template.layoutType,
products: mappedProducts,
moreProducts: mappedProducts
} as MarketingChannel
}
function logChannelProducts(channelTitle: string, products: Array<Product>): void {
for (let i = 0; i < products.length; i++) {
const item = products[i]
console.log('[home-channel] ' + channelTitle + ' product:', item.id, item.name ?? '', getRealProductImage(item), getRealSalePrice(item), getRealMarketPrice(item))
}
}
function buildRealRecommendMarketingChannels(products: Array<Product>): MarketingChannel[] {
console.log('[home-channel] buildRealRecommendMarketingChannels input count:', products.length)
const templates = getRecommendMarketingChannels()
if (products.length == 0 || templates.length == 0) {
console.log('[home-channel] fallback to mock channel data')
return templates
}
const uniqueProducts = dedupeProducts(products)
if (uniqueProducts.length == 0) {
console.log('[home-channel] fallback to mock channel data')
return templates
}
const selectedIds: Array<string> = []
const discountCandidates = sortProductsByScoreDesc(filterProductsByMode(uniqueProducts, 'discount'), 'discount')
const qualityCandidates = sortProductsByScoreDesc(filterProductsByMode(uniqueProducts, 'quality'), 'quality')
const cheapCandidates = mergeUniqueProductLists(
sortProductsByPriceAsc(filterProductsByMode(uniqueProducts, 'cheap-9')),
sortProductsByPriceAsc(filterProductsByMode(uniqueProducts, 'cheap-19')),
sortProductsByPriceAsc(uniqueProducts)
)
const liveCandidates = mergeUniqueProductLists(
sortProductsByScoreDesc(filterProductsByMode(uniqueProducts, 'live'), 'hot'),
sortProductsByScoreDesc(discountCandidates, 'discount'),
sortProductsByScoreDesc(uniqueProducts, 'hot')
)
const hotFallback = sortProductsByScoreDesc(uniqueProducts, 'hot')
const cheapFallback = sortProductsByPriceAsc(uniqueProducts)
const subsidyProducts = selectChannelProducts(discountCandidates, hotFallback, hotFallback, selectedIds, 2)
const qualityProducts = selectChannelProducts(qualityCandidates, hotFallback, hotFallback, selectedIds, 2)
const cheapProducts = selectChannelProducts(cheapCandidates, cheapFallback, hotFallback, selectedIds, 2)
const liveProducts = selectChannelProducts(liveCandidates, discountCandidates, hotFallback, selectedIds, 2)
logChannelProducts('百亿补贴', subsidyProducts)
logChannelProducts('品质生活', qualityProducts)
logChannelProducts('9.9包邮', cheapProducts)
logChannelProducts('直播低价', liveProducts)
const mappedChannels: Array<MarketingChannel> = []
for (let i = 0; i < templates.length; i++) {
const template = templates[i]
if (template.id == 'subsidy') {
mappedChannels.push(buildChannelFromTemplate(template, subsidyProducts, '补贴价'))
continue
}
if (template.id == 'quality-life') {
mappedChannels.push(buildChannelFromTemplate(template, qualityProducts, '实惠'))
continue
}
if (template.id == 'cheap-mail') {
const cheapMappedProducts: Array<ChannelProduct> = []
for (let j = 0; j < cheapProducts.length; j++) {
const cheapProduct = cheapProducts[j]
const label = getRealSalePrice(cheapProduct) <= 9.9 ? '9.9包邮' : '特价'
cheapMappedProducts.push(toChannelProduct(cheapProduct, label))
}
mappedChannels.push({
id: template.id,
title: template.title,
subtitle: template.subtitle,
badge: template.badge,
themeColor: template.themeColor,
bgColor: template.bgColor,
routeType: template.routeType,
layoutType: template.layoutType,
products: cheapMappedProducts,
moreProducts: cheapMappedProducts
} as MarketingChannel)
continue
}
if (template.id == 'live-low-price') {
mappedChannels.push(buildChannelFromTemplate(template, liveProducts, '直播价'))
continue
}
mappedChannels.push(template)
}
return mappedChannels
}
function buildSimpleCategoryChannels(categoryId: string, products: Array<Product> = []): SimpleCategoryChannel[] {
const dedupedProducts = dedupeProducts(products)
if (dedupedProducts.length == 0) {
return [] as Array<SimpleCategoryChannel>
}
const hotProductsForCategory = sortProductsByScoreDesc(dedupedProducts, 'hot')
const qualityProductsForCategory = sortProductsByScoreDesc(dedupedProducts, 'quality')
const firstChannelCovers: Array<string> = []
const secondChannelCovers: Array<string> = []
for (let i = 0; i < hotProductsForCategory.length && firstChannelCovers.length < 2; i++) {
firstChannelCovers.push(getRealProductImage(hotProductsForCategory[i]))
}
for (let i = 0; i < qualityProductsForCategory.length && secondChannelCovers.length < 2; i++) {
secondChannelCovers.push(getRealProductImage(qualityProductsForCategory[i]))
}
while (firstChannelCovers.length < 2) {
firstChannelCovers.push('/static/images/default.png')
}
while (secondChannelCovers.length < 2) {
secondChannelCovers.push('/static/images/default.png')
}
return [
{
id: categoryId + '-rank',
title: '热销榜',
subtitle: '真实商品热度精选',
routeType: 'rank',
icon: '热',
coverImages: firstChannelCovers,
categoryId
} as SimpleCategoryChannel,
{
id: categoryId + '-quality',
title: '品质优选',
subtitle: '真实好物口碑推荐',
routeType: 'quality',
icon: '精',
coverImages: secondChannelCovers,
categoryId
} as SimpleCategoryChannel
]
}
async function loadCategoryChannelCards(categoryId: string): Promise<void> {
try {
const channelResult = await supabaseService.getMedicalMallProductsByCategory(categoryId, 1, categoryChannelLoadLimit)
categorySimpleChannels.value = buildSimpleCategoryChannels(categoryId, channelResult.data)
} catch (error) {
console.error('[home-channel] 加载分类频道卡片失败', categoryId, error)
categorySimpleChannels.value = [] as Array<SimpleCategoryChannel>
}
}
function buildVisibleRecommendChannels(): MarketingChannel[] { function buildVisibleRecommendChannels(): MarketingChannel[] {
const source = getRecommendMarketingChannels() const source = getRecommendMarketingChannels()
const visible: MarketingChannel[] = [] const visible: MarketingChannel[] = []
@@ -937,16 +1239,16 @@ function buildVisibleRecommendChannels(): MarketingChannel[] {
visible.push(channel) visible.push(channel)
} }
return visible return visible
} }
function applyChannelDisplay(categoryId: string): void { function applyChannelDisplay(categoryId: string): void {
if (categoryId === 'recommend') { if (categoryId === 'recommend') {
marketingChannels.value = buildVisibleRecommendChannels() marketingChannels.value = [] as Array<MarketingChannel>
categorySimpleChannels.value = [] categorySimpleChannels.value = [] as Array<SimpleCategoryChannel>
return return
} }
marketingChannels.value = [] marketingChannels.value = [] as Array<MarketingChannel>
categorySimpleChannels.value = buildSimpleCategoryChannels(categoryId) categorySimpleChannels.value = [] as Array<SimpleCategoryChannel>
} }
function buildChannelDetailUrl(channelId: string, routeType: string, categoryId: string): string { function buildChannelDetailUrl(channelId: string, routeType: string, categoryId: string): string {
@@ -1440,11 +1742,18 @@ async function loadHotProducts(page: number, limit: number): Promise<void> {
} }
} }
setHotProducts(products) setHotProducts(products)
if (currentFeedCategoryId.value === 'recommend' && page <= 1) {
marketingChannels.value = buildRealRecommendMarketingChannels(products)
}
hasMore.value = result.hasmore hasMore.value = result.hasmore
currentPage.value = page currentPage.value = page
} catch (error) { } catch (error) {
console.error('加载热销商品失败:', error) console.error('加载热销商品失败:', error)
hotProducts.value = [] hotProducts.value = []
if (currentFeedCategoryId.value === 'recommend') {
console.log('[home-channel] fallback to mock channel data')
marketingChannels.value = buildVisibleRecommendChannels()
}
hasMore.value = false hasMore.value = false
} }
} }
@@ -1476,14 +1785,18 @@ async function loadCategoryGoods(categoryId: string): Promise<void> {
await syncCategoryLayout(categoryId) await syncCategoryLayout(categoryId)
if (categoryId === 'recommend') { if (categoryId === 'recommend') {
try { try {
const result = await supabaseService.getMedicalMallSmartRecommendations(1, defaultLoadLimit) const result = await supabaseService.getMedicalMallSmartRecommendations(1, recommendChannelLoadLimit)
console.log('[home-channel] 推荐商品接口返回数量:', result.data.length)
failedProductImageIds.value = [] failedProductImageIds.value = []
setHotProducts(result.data) setHotProducts(result.data)
marketingChannels.value = buildRealRecommendMarketingChannels(result.data)
hasMore.value = result.hasmore hasMore.value = result.hasmore
currentPage.value = 1 currentPage.value = 1
} catch (error) { } catch (error) {
console.error('加载热销商品失败:', error) console.error('加载热销商品失败:', error)
hotProducts.value = [] hotProducts.value = []
console.log('[home-channel] fallback to mock channel data')
marketingChannels.value = buildVisibleRecommendChannels()
hasMore.value = false hasMore.value = false
} }
} else { } else {
@@ -1492,10 +1805,12 @@ async function loadCategoryGoods(categoryId: string): Promise<void> {
const result = await supabaseService.getMedicalMallProductsByCategory(categoryId, 1, defaultLoadLimit) const result = await supabaseService.getMedicalMallProductsByCategory(categoryId, 1, defaultLoadLimit)
failedProductImageIds.value = [] failedProductImageIds.value = []
setHotProducts(result.data) setHotProducts(result.data)
await loadCategoryChannelCards(categoryId)
hasMore.value = result.hasmore hasMore.value = result.hasmore
} catch (e) { } catch (e) {
console.error('分类商品加载失败', e) console.error('分类商品加载失败', e)
hotProducts.value = [] hotProducts.value = []
categorySimpleChannels.value = [] as Array<SimpleCategoryChannel>
hasMore.value = false hasMore.value = false
} finally { } finally {
loading.value = false loading.value = false
@@ -1520,12 +1835,16 @@ async function refreshHomeCategory(item: CategoryItem): Promise<void> {
secondaryCategoryDisplay.value = buildSecondaryCategoryDisplay(item.id) secondaryCategoryDisplay.value = buildSecondaryCategoryDisplay(item.id)
applyChannelDisplay(item.id) applyChannelDisplay(item.id)
try { try {
const result = await supabaseService.getMedicalMallSmartRecommendations(1, defaultLoadLimit) const result = await supabaseService.getMedicalMallSmartRecommendations(1, recommendChannelLoadLimit)
console.log('[home-channel] 推荐商品接口返回数量:', result.data.length)
setHotProducts(result.data) setHotProducts(result.data)
marketingChannels.value = buildRealRecommendMarketingChannels(result.data)
hasMore.value = result.hasmore hasMore.value = result.hasmore
} catch (error) { } catch (error) {
console.error('加载推荐商品失败:', error) console.error('加载推荐商品失败:', error)
hotProducts.value = [] hotProducts.value = []
console.log('[home-channel] fallback to mock channel data')
marketingChannels.value = buildVisibleRecommendChannels()
hasMore.value = false hasMore.value = false
} finally { } finally {
loading.value = false loading.value = false
@@ -1545,10 +1864,12 @@ async function refreshHomeCategory(item: CategoryItem): Promise<void> {
try { try {
const result = await supabaseService.getMedicalMallProductsByCategory(item.id, 1, defaultLoadLimit) const result = await supabaseService.getMedicalMallProductsByCategory(item.id, 1, defaultLoadLimit)
setHotProducts(result.data) setHotProducts(result.data)
await loadCategoryChannelCards(item.id)
hasMore.value = result.hasmore hasMore.value = result.hasmore
} catch (error) { } catch (error) {
console.error('分类商品加载失败', error) console.error('分类商品加载失败', error)
hotProducts.value = [] hotProducts.value = []
categorySimpleChannels.value = [] as Array<SimpleCategoryChannel>
hasMore.value = false hasMore.value = false
} finally { } finally {
loading.value = false loading.value = false
@@ -1857,7 +2178,8 @@ const switchSort = (sortId: string) => {
} }
hasMore.value = true // 重置加载更多状态 hasMore.value = true // 重置加载更多状态
// 重新加载热销商品,排序由 Supabase 服务处理 // 重新加载热销商品,排序由 Supabase 服务处理
loadHotProducts(1, defaultLoadLimit) const nextLimit = currentFeedCategoryId.value === 'recommend' ? recommendChannelLoadLimit : defaultLoadLimit
loadHotProducts(1, nextLimit)
} }
// 切换筛选器 // 切换筛选器
@@ -1909,12 +2231,13 @@ const loadMore = async () => {
showLoadMore.value = true showLoadMore.value = true
loading.value = true loading.value = true
try { try {
const pageLimit = currentFeedCategoryId.value === 'recommend' ? recommendChannelLoadLimit : defaultLoadLimit
const nextPage = currentPage.value + 1 const nextPage = currentPage.value + 1
const currentCount = hotProducts.value.length const currentCount = hotProducts.value.length
console.log('开始加载更多,当前数量:', currentCount, '页码:', nextPage, '分类:', currentFeedCategoryId.value) console.log('开始加载更多,当前数量:', currentCount, '页码:', nextPage, '分类:', currentFeedCategoryId.value)
if (currentFeedCategoryId.value === 'recommend') { if (currentFeedCategoryId.value === 'recommend') {
const result = await fetchSortedProductsPage(nextPage, defaultLoadLimit) const result = await fetchSortedProductsPage(nextPage, pageLimit)
const newProducts = result.data const newProducts = result.data
if (newProducts.length == 0) { if (newProducts.length == 0) {
@@ -2025,7 +2348,7 @@ const onScan = (): void => {
}) })
}, },
fail: (err) => { fail: (err) => {
console.error('扫码失败:', err) console.error('扫码失败:', err)
} }
}) })
} }

View File

@@ -195,29 +195,12 @@
</scroll-view> </scroll-view>
</view> </view>
<view class="recommend-section"> <GuessYouLike
<view class="recommend-header"> title="猜你喜欢"
<text class="recommend-title">猜你喜欢</text> :pageSize="8"
<text class="recommend-subtitle">精选推荐,继续逛一逛</text> :loadMoreKey="guessLoadMoreKey"
</view> @productClick="handleGuessProductClick"
<view class="recommend-grid"> />
<view v-for="item in recommendProducts" :key="item.id" class="recommend-card" @click="goToRecommendProduct(item)">
<image :src="item.image" class="recommend-image" mode="aspectFill" />
<view class="recommend-info">
<text class="recommend-name">{{ item.name }}</text>
<view class="recommend-meta-row">
<text class="recommend-price">¥{{ item.price }}</text>
<text class="recommend-tag">{{ item.tag }}</text>
</view>
</view>
</view>
</view>
<view class="recommend-load-more" @click="loadRecommendProducts(false)">
<text v-if="recommendLoading" class="recommend-load-text">正在加载更多...</text>
<text v-else-if="!recommendHasMore && recommendInitialized" class="recommend-load-text">没有更多了</text>
<text v-else class="recommend-load-text">上拉加载更多</text>
</view>
</view>
<view class="profile-bottom-safe"></view> <view class="profile-bottom-safe"></view>
</view> </view>
@@ -229,6 +212,7 @@
import { UserType } from '@/types/mall-types.uts' import { UserType } from '@/types/mall-types.uts'
import supabaseService from '@/utils/supabaseService.uts' import supabaseService from '@/utils/supabaseService.uts'
import { goToLogin } from '@/utils/utils.uts' import { goToLogin } from '@/utils/utils.uts'
import GuessYouLike from '@/components/mall/GuessYouLike/GuessYouLike.uvue'
type UserStatsType = { type UserStatsType = {
points: number points: number
@@ -290,6 +274,9 @@ type PendingReceiptGoodsType = {
type ModalSuccessResult = { confirm: boolean; cancel: boolean } type ModalSuccessResult = { confirm: boolean; cancel: boolean }
export default { export default {
components: {
GuessYouLike
},
data() { data() {
return { return {
userInfo: { userInfo: {
@@ -345,6 +332,7 @@ export default {
recommendBottomLocked: false, recommendBottomLocked: false,
recommendViewportHeight: 0, recommendViewportHeight: 0,
pageWindowHeight: 0, pageWindowHeight: 0,
guessLoadMoreKey: 0,
statusBarHeight: 0, statusBarHeight: 0,
isAndroidApp: false, isAndroidApp: false,
capsuleTop: 0, capsuleTop: 0,
@@ -376,7 +364,6 @@ export default {
this.initPage() this.initPage()
this.loadUserProfile() this.loadUserProfile()
this.loadOrders() this.loadOrders()
this.loadRecommendProducts(true)
// 监听订单更新事件 // 监听订单更新事件
uni.$on('orderUpdated', this.handleOrderUpdated) uni.$on('orderUpdated', this.handleOrderUpdated)
@@ -474,24 +461,6 @@ export default {
return result return result
}, },
mockRecommendProducts(page: number, pageSize: number): Array<RecommendProductType> {
const list: Array<RecommendProductType> = []
if (page >= 5) {
return list
}
for (let i: number = 0; i < pageSize; i++) {
const index = (page - 1) * pageSize + i + 1
list.push({
id: 'mock-recommend-' + index,
name: '猜你喜欢 推荐商品 ' + index,
image: '/static/images/default.png',
price: 9.9 + index,
tag: '已售' + (100 + index) + '+'
} as RecommendProductType)
}
return list
},
async fetchRecommendProducts(page: number, pageSize: number): Promise<Array<RecommendProductType>> { async fetchRecommendProducts(page: number, pageSize: number): Promise<Array<RecommendProductType>> {
console.log('[profile推荐] fetchRecommendProducts 请求 page=', page, 'pageSize=', pageSize) console.log('[profile推荐] fetchRecommendProducts 请求 page=', page, 'pageSize=', pageSize)
const result = await supabaseService.searchProducts('', page, pageSize, 'sales') const result = await supabaseService.searchProducts('', page, pageSize, 'sales')
@@ -578,14 +547,7 @@ export default {
this.recommendInitialized = true this.recommendInitialized = true
} catch (e) { } catch (e) {
console.error('加载推荐商品失败:', e) console.error('加载推荐商品失败:', e)
this.recommendHasMore = false
if (reset && this.recommendProducts.length === 0) {
const mockList = this.mockRecommendProducts(1, this.recommendPageSize)
this.recommendProducts = mockList
this.recommendPage = 2
this.recommendHasMore = true
this.recommendInitialized = true
}
} finally { } finally {
this.recommendLoading = false this.recommendLoading = false
if (!reset && this.recommendPendingLoad && this.recommendHasMore) { if (!reset && this.recommendPendingLoad && this.recommendHasMore) {
@@ -597,55 +559,20 @@ export default {
}, },
onRecommendScrollToLower() { onRecommendScrollToLower() {
console.log('[profile推荐] scrolltolower 触发') this.guessLoadMoreKey = this.guessLoadMoreKey + 1
this.recommendBottomLocked = true
this.loadRecommendProducts(false)
}, },
onRecommendScroll(event: any) { onRecommendScroll(event: any) {
if (this.recommendLoading || !this.recommendHasMore) { return
},
handleGuessProductClick(productId: string) {
if (productId == null || productId === '') {
return return
} }
try { uni.navigateTo({
const eventObj = this.toRecommendScrollJson(event) url: `/pages/mall/consumer/product-detail?id=${productId}&productId=${productId}`
let detailObj: UTSJSONObject | null = null })
if (eventObj != null) {
detailObj = this.toRecommendScrollJson(eventObj.get('detail'))
}
if (detailObj == null) {
return
}
const scrollTop = this.readRecommendScrollMetric(detailObj, 'scrollTop')
const scrollHeight = this.readRecommendScrollMetric(detailObj, 'scrollHeight')
let clientHeight = this.readRecommendScrollMetric(detailObj, 'clientHeight')
if (clientHeight <= 0) {
clientHeight = this.recommendViewportHeight
}
console.log('[profile推荐] scroll事件 scrollTop=', scrollTop, 'scrollHeight=', scrollHeight, 'clientHeight=', clientHeight)
if (scrollHeight <= 0 || clientHeight <= 0) {
return
}
const distanceToBottom = scrollHeight - scrollTop - clientHeight
if (distanceToBottom > 260) {
this.recommendBottomLocked = false
}
if (distanceToBottom <= 180) {
console.log('[profile推荐] scroll 兜底触底 distanceToBottom=', distanceToBottom)
if (this.recommendBottomLocked) {
this.recommendPendingLoad = true
return
}
this.recommendBottomLocked = true
this.loadRecommendProducts(false)
}
} catch (e) {
console.error('[profile推荐] 处理推荐滚动失败', e)
}
}, },
resetGuestProfileState() { resetGuestProfileState() {
@@ -1045,9 +972,6 @@ export default {
const userId = supabaseService.getCurrentUserId() const userId = supabaseService.getCurrentUserId()
if (userId == null || userId === '') { if (userId == null || userId === '') {
this.resetGuestProfileState() this.resetGuestProfileState()
if (!this.recommendInitialized) {
this.loadRecommendProducts(true)
}
return return
} }
@@ -1055,9 +979,6 @@ export default {
this.loadUserProfile() this.loadUserProfile()
this.loadOrders() this.loadOrders()
this.updateCouponCount() // 更新优惠券数量 this.updateCouponCount() // 更新优惠券数量
if (!this.recommendInitialized) {
this.loadRecommendProducts(true)
}
}, },
async updateCouponCount() { async updateCouponCount() {
@@ -1545,13 +1466,14 @@ export default {
}, },
payOrder(order: OrderItemType) { payOrder(order: OrderItemType) {
const paymentAmount = order.actual_amount
const userId = supabaseService.getCurrentUserId() const userId = supabaseService.getCurrentUserId()
if (userId == null || userId === '') { if (userId == null || userId === '') {
goToLogin(`/pages/mall/consumer/payment?orderId=${order.id}`) goToLogin(`/pages/mall/consumer/payment?orderId=${order.id}&amount=${paymentAmount}`)
return return
} }
uni.navigateTo({ uni.navigateTo({
url: `/pages/mall/consumer/payment?orderId=${order.id}` url: `/pages/mall/consumer/payment?orderId=${order.id}&amount=${paymentAmount}`
}) })
}, },
@@ -2693,4 +2615,3 @@ export default {
} }
} }
</style> </style>

View File

@@ -17,6 +17,15 @@
<input class="input" v-model="regionString" placeholder="省市区县、乡镇等" placeholder-class="placeholder" /> <input class="input" v-model="regionString" placeholder="省市区县、乡镇等" placeholder-class="placeholder" />
<text class="arrow-icon"></text> <text class="arrow-icon"></text>
</view> </view>
<view class="location-action-row">
<view class="location-action-btn" @click="fillCurrentLocation">
<text class="location-action-text">获取当前位置</text>
</view>
<view class="location-action-btn" @click="pickLocation">
<text class="location-action-text">地图选点</text>
</view>
</view>
<text v-if="locationHint != ''" class="location-hint">{{ locationHint }}</text>
<view class="form-item detail-item"> <view class="form-item detail-item">
<text class="label">详细地址</text> <text class="label">详细地址</text>
<textarea class="textarea" v-model="formData.detail" placeholder="街道、楼牌号等" placeholder-class="placeholder" maxlength="100"></textarea> <textarea class="textarea" v-model="formData.detail" placeholder="街道、楼牌号等" placeholder-class="placeholder" maxlength="100"></textarea>
@@ -85,6 +94,9 @@ type Address = {
detail: string detail: string
isDefault: boolean isDefault: boolean
label?: string label?: string
latitude?: number
longitude?: number
coordinateType?: string
} }
const isEdit = ref(false) const isEdit = ref(false)
@@ -92,6 +104,9 @@ const addressId = ref('')
const regionString = ref('') const regionString = ref('')
const tags = ['家', '公司', '学校'] const tags = ['家', '公司', '学校']
const smartInput = ref('') const smartInput = ref('')
const locationHint = ref('')
const latitude = ref(0)
const longitude = ref(0)
type AddressForm = { type AddressForm = {
name: string name: string
@@ -120,6 +135,9 @@ const loadAddress = async (id: string) => {
formData.isDefault = address.is_default formData.isDefault = address.is_default
formData.label = address.label ?? '' formData.label = address.label ?? ''
regionString.value = `${address.province} ${address.city} ${address.district}`.trim() regionString.value = `${address.province} ${address.city} ${address.district}`.trim()
latitude.value = address.latitude ?? 0
longitude.value = address.longitude ?? 0
locationHint.value = latitude.value != 0 || longitude.value != 0 ? `已定位:${latitude.value}, ${longitude.value}` : ''
} else { } else {
// 如果Supabase没有找到尝试从本地存储加载 // 如果Supabase没有找到尝试从本地存储加载
const storedAddresses = uni.getStorageSync('addresses') const storedAddresses = uni.getStorageSync('addresses')
@@ -133,6 +151,9 @@ const loadAddress = async (id: string) => {
formData.isDefault = localAddress.isDefault formData.isDefault = localAddress.isDefault
formData.label = localAddress.label ?? '' formData.label = localAddress.label ?? ''
regionString.value = `${localAddress.province} ${localAddress.city} ${localAddress.district}`.trim() regionString.value = `${localAddress.province} ${localAddress.city} ${localAddress.district}`.trim()
latitude.value = localAddress.latitude ?? 0
longitude.value = localAddress.longitude ?? 0
locationHint.value = latitude.value != 0 || longitude.value != 0 ? `已定位:${latitude.value}, ${longitude.value}` : ''
} }
} }
} }
@@ -151,6 +172,9 @@ const loadAddress = async (id: string) => {
formData.isDefault = address.isDefault formData.isDefault = address.isDefault
formData.label = address.label ?? '' formData.label = address.label ?? ''
regionString.value = `${address.province} ${address.city} ${address.district}`.trim() regionString.value = `${address.province} ${address.city} ${address.district}`.trim()
latitude.value = address.latitude ?? 0
longitude.value = address.longitude ?? 0
locationHint.value = latitude.value != 0 || longitude.value != 0 ? `已定位:${latitude.value}, ${longitude.value}` : ''
} }
} catch (e) { } catch (e) {
console.error('解析本地地址数据失败', e) console.error('解析本地地址数据失败', e)
@@ -159,6 +183,42 @@ const loadAddress = async (id: string) => {
} }
} }
const applyLocation = (latitudeValue: number, longitudeValue: number, addressText: string, locationName: string) => {
latitude.value = latitudeValue
longitude.value = longitudeValue
locationHint.value = `已定位:${latitudeValue}, ${longitudeValue}`
if (addressText != '') {
regionString.value = addressText
}
if (locationName != '' && formData.detail == '') {
formData.detail = locationName
}
}
const fillCurrentLocation = () => {
uni.getLocation({
type: 'gcj02',
success: (res) => {
applyLocation(res.latitude, res.longitude, regionString.value, '')
uni.showToast({ title: '已获取当前位置', icon: 'success' })
},
fail: () => {
uni.showToast({ title: '定位失败,请手动输入地址', icon: 'none' })
}
})
}
const pickLocation = () => {
uni.chooseLocation({
success: (res) => {
applyLocation(res.latitude, res.longitude, res.address ?? '', res.name ?? '')
},
fail: () => {
uni.showToast({ title: '当前环境不支持地图选点,可手动输入', icon: 'none' })
}
})
}
onLoad((options) => { onLoad((options) => {
if (options == null) return if (options == null) return
const optionsObj = options as UTSJSONObject const optionsObj = options as UTSJSONObject
@@ -216,7 +276,10 @@ const saveAddress = async () => {
detail_address: formData.detail, detail_address: formData.detail,
postal_code: '', // 如果需要可以添加邮政编码字段 postal_code: '', // 如果需要可以添加邮政编码字段
is_default: formData.isDefault, is_default: formData.isDefault,
label: formData.label label: formData.label,
latitude: latitude.value,
longitude: longitude.value,
coordinate_type: 'gcj02'
} as AddAddressParams } as AddAddressParams
let success = false let success = false
@@ -231,8 +294,11 @@ const saveAddress = async () => {
district: district, district: district,
detail_address: formData.detail, detail_address: formData.detail,
postal_code: '', postal_code: '',
is_default: formData.isDefault, is_default: formData.isDefault,
label: formData.label label: formData.label,
latitude: latitude.value,
longitude: longitude.value,
coordinate_type: 'gcj02'
} as UpdateAddressParams } as UpdateAddressParams
success = await supabaseService.updateAddress(addressId.value, updateData) success = await supabaseService.updateAddress(addressId.value, updateData)
} else { } else {
@@ -271,7 +337,10 @@ const saveAddress = async () => {
district: district, district: district,
detail: formData.detail, detail: formData.detail,
isDefault: formData.isDefault, isDefault: formData.isDefault,
label: formData.label label: formData.label,
latitude: latitude.value,
longitude: longitude.value,
coordinateType: 'gcj02'
} }
} }
} else { } else {
@@ -284,7 +353,10 @@ const saveAddress = async () => {
district: district, district: district,
detail: formData.detail, detail: formData.detail,
isDefault: formData.isDefault, isDefault: formData.isDefault,
label: formData.label label: formData.label,
latitude: latitude.value,
longitude: longitude.value,
coordinateType: 'gcj02'
} }
addresses.push(newAddress) addresses.push(newAddress)
} }
@@ -437,6 +509,38 @@ const deleteAddress = () => {
border-radius: 16px; /* 详细地址区域也增加圆角 */ border-radius: 16px; /* 详细地址区域也增加圆角 */
} }
.location-action-row {
display: flex;
flex-direction: row;
margin-bottom: 12px;
}
.location-action-btn {
flex: 1;
min-height: 40px;
justify-content: center;
align-items: center;
background-color: #f1f5f9;
border-radius: 20px;
margin-right: 10px;
}
.location-action-btn:last-child {
margin-right: 0;
}
.location-action-text {
font-size: 13px;
color: #2563eb;
}
.location-hint {
display: block;
margin-bottom: 12px;
font-size: 12px;
color: #64748b;
}
.detail-item .label { .detail-item .label {
margin-bottom: 8px; margin-bottom: 8px;
} }

View File

@@ -48,6 +48,9 @@ type Address = {
detail: string detail: string
isDefault: boolean isDefault: boolean
label?: string label?: string
latitude?: number
longitude?: number
coordinateType?: string
} }
const addresses = ref<Address[]>([]) const addresses = ref<Address[]>([])
@@ -71,7 +74,10 @@ const loadAddresses = async () => {
district: item.district, district: item.district,
detail: item.detail_address, detail: item.detail_address,
isDefault: item.is_default, isDefault: item.is_default,
label: '' label: '',
latitude: item.latitude ?? 0,
longitude: item.longitude ?? 0,
coordinateType: item.coordinate_type ?? 'gcj02'
} as Address } as Address
transformedAddresses.push(addr) transformedAddresses.push(addr)
} }
@@ -99,8 +105,8 @@ const loadAddresses = async () => {
onLoad((options) => { onLoad((options) => {
if (options == null) return if (options == null) return
const optionsObj = options as UTSJSONObject const selectMode = options['selectMode']
if ((optionsObj.getString('selectMode') ?? '') == 'true') { if (selectMode != null && String(selectMode) == 'true') {
selectionMode.value = true selectionMode.value = true
} }
}) })
@@ -166,12 +172,23 @@ const selectAddress = (item: Address) => {
if (selectionMode.value) { if (selectionMode.value) {
uni.$emit('addressSelected', { uni.$emit('addressSelected', {
id: item.id, id: item.id,
addressId: item.id,
userId: '',
recipient_name: item.name, recipient_name: item.name,
contactName: item.name,
phone: item.phone, phone: item.phone,
contactPhone: item.phone,
province: item.province, province: item.province,
city: item.city, city: item.city,
district: item.district, district: item.district,
detail: item.detail, detail: item.detail,
addressDetail: item.detail,
houseNumber: item.detail,
fullAddress: getFullAddress(item),
remark: item.label ?? '',
latitude: item.latitude ?? 0,
longitude: item.longitude ?? 0,
coordinateType: item.coordinateType ?? 'gcj02',
is_default: item.isDefault is_default: item.isDefault
}) })
uni.navigateBack() uni.navigateBack()

View File

@@ -149,6 +149,13 @@ async function loadData() {
bookingDays.value = getBookingDayOptions() bookingDays.value = getBookingDayOptions()
bookingSlots.value = getBookingTimeSlots() bookingSlots.value = getBookingTimeSlots()
services.value = await fetchHomeServiceCatalog() services.value = await fetchHomeServiceCatalog()
if (services.value.length > 0) {
selectService(services.value[0].id, services.value[0].name)
return
}
selectedServiceId.value = ''
form.serviceId = ''
form.serviceName = ''
} }
function selectService(serviceId: string, serviceName: string) { function selectService(serviceId: string, serviceName: string) {
@@ -192,6 +199,10 @@ function selectSlot(slotId: string, available: boolean) {
} }
async function submitApplication() { async function submitApplication() {
if (form.serviceId == '' || form.serviceName == '') {
uni.showToast({ title: '当前没有可预约的服务项目', icon: 'none' })
return
}
if (form.applicantName == '' || form.elderName == '' || form.phone == '' || form.address == '' || form.preferredTime == '') { if (form.applicantName == '' || form.elderName == '' || form.phone == '' || form.address == '' || form.preferredTime == '') {
uni.showToast({ title: '请补全申请信息', icon: 'none' }) uni.showToast({ title: '请补全申请信息', icon: 'none' })
return return
@@ -200,6 +211,10 @@ async function submitApplication() {
const parsedAge = parseInt(ageText.value) const parsedAge = parseInt(ageText.value)
form.age = isNaN(parsedAge) ? 0 : parsedAge form.age = isNaN(parsedAge) ? 0 : parsedAge
const created = await createHomeServiceApplication(form) const created = await createHomeServiceApplication(form)
if (created == null) {
uni.showToast({ title: '申请提交失败,请检查登录和预约信息', icon: 'none' })
return
}
uni.showToast({ title: '申请已提交', icon: 'success' }) uni.showToast({ title: '申请已提交', icon: 'success' })
uni.navigateTo({ url: '/pages/mall/consumer/home-service/order-detail?id=' + created.id }) uni.navigateTo({ url: '/pages/mall/consumer/home-service/order-detail?id=' + created.id })
} }

View File

@@ -53,6 +53,8 @@ import ServicePageScaffold from '@/components/homeService/ServicePageScaffold.uv
import ServicePanel from '@/components/homeService/ServicePanel.uvue' import ServicePanel from '@/components/homeService/ServicePanel.uvue'
import { fetchConsumerAcceptanceDetail, submitConsumerAcceptance } from '@/services/homeServiceService.uts' import { fetchConsumerAcceptanceDetail, submitConsumerAcceptance } from '@/services/homeServiceService.uts'
import { HomeServiceAcceptanceType } from '@/types/home-service.uts' import { HomeServiceAcceptanceType } from '@/types/home-service.uts'
import { getCurrentUser, getCurrentUserId } from '@/utils/store.uts'
import { goToLogin } from '@/utils/utils.uts'
const caseId = ref('') const caseId = ref('')
const detail = ref<HomeServiceAcceptanceType | null>(null) const detail = ref<HomeServiceAcceptanceType | null>(null)
@@ -62,17 +64,31 @@ const selectedTags = ref<Array<string>>([])
const scores = [1, 2, 3, 4, 5] const scores = [1, 2, 3, 4, 5]
const allTags = ['准时上门', '沟通清楚', '动作规范', '记录完整', '需进一步整改'] const allTags = ['准时上门', '沟通清楚', '动作规范', '记录完整', '需进一步整改']
async function ensureLogin(): Promise<boolean> {
const user = await getCurrentUser()
if (user == null || getCurrentUserId() == '') {
goToLogin('/pages/mall/consumer/home-service/feedback?id=' + caseId.value)
return false
}
return true
}
onLoad((options) => { onLoad((options) => {
const id = options['id'] const id = options['id']
if (id != null) { if (id != null) {
caseId.value = id as string caseId.value = id as string
fetchConsumerAcceptanceDetail(caseId.value).then((res) => { ensureLogin().then((ok) => {
if (res != null) { if (!ok) {
detail.value = res return
rating.value = res.rating
feedback.value = res.feedback
selectedTags.value = res.tags.slice(0)
} }
fetchConsumerAcceptanceDetail(caseId.value).then((res) => {
if (res != null) {
detail.value = res
rating.value = res.rating
feedback.value = res.feedback
selectedTags.value = res.tags.slice(0)
}
})
}) })
} }
}) })
@@ -87,6 +103,9 @@ function toggleTag(tag: string) {
} }
async function submitResult(approved: boolean) { async function submitResult(approved: boolean) {
if (!(await ensureLogin())) {
return
}
if (caseId.value == '' || feedback.value == '') { if (caseId.value == '' || feedback.value == '') {
uni.showToast({ title: '请填写反馈说明', icon: 'none' }) uni.showToast({ title: '请填写反馈说明', icon: 'none' })
return return

View File

@@ -89,6 +89,8 @@ import ServicePanel from '@/components/homeService/ServicePanel.uvue'
import ServiceStatusTag from '@/components/homeService/ServiceStatusTag.uvue' import ServiceStatusTag from '@/components/homeService/ServiceStatusTag.uvue'
import { fetchConsumerHomeServiceCases, fetchHomeServiceCatalog } from '@/services/homeServiceService.uts' import { fetchConsumerHomeServiceCases, fetchHomeServiceCatalog } from '@/services/homeServiceService.uts'
import { HomeServiceCatalogType, HomeServiceCaseType } from '@/types/home-service.uts' import { HomeServiceCatalogType, HomeServiceCaseType } from '@/types/home-service.uts'
import { getCurrentUser, getCurrentUserId } from '@/utils/store.uts'
import { goToLogin } from '@/utils/utils.uts'
import { import {
HomeServiceCategoryType, HomeServiceCategoryType,
HomeServiceItemType, HomeServiceItemType,
@@ -125,9 +127,23 @@ async function loadData() {
categoryGrid.value = getHomeServiceCategories() categoryGrid.value = getHomeServiceCategories()
promoCards.value = getHomeServicePromoCards() promoCards.value = getHomeServicePromoCards()
services.value = await fetchHomeServiceCatalog() services.value = await fetchHomeServiceCatalog()
const user = await getCurrentUser()
if (user == null || getCurrentUserId() == '') {
cases.value = [] as Array<HomeServiceCaseType>
return
}
cases.value = await fetchConsumerHomeServiceCases() cases.value = await fetchConsumerHomeServiceCases()
} }
async function ensureLogin(redirectUrl: string): Promise<boolean> {
const user = await getCurrentUser()
if (user == null || getCurrentUserId() == '') {
goToLogin(redirectUrl)
return false
}
return true
}
function switchCategory(categoryId: string) { function switchCategory(categoryId: string) {
selectedCategory.value = categoryId selectedCategory.value = categoryId
} }
@@ -141,11 +157,21 @@ function goDetail(serviceId: string) {
} }
function goBooking(serviceId: string) { function goBooking(serviceId: string) {
uni.navigateTo({ url: '/pages/mall/consumer/home-service/service-detail?id=' + serviceId + '&mode=booking' }) ensureLogin('/pages/mall/consumer/home-service/service-detail?id=' + serviceId + '&mode=booking').then((ok) => {
if (!ok) {
return
}
uni.navigateTo({ url: '/pages/mall/consumer/home-service/service-detail?id=' + serviceId + '&mode=booking' })
})
} }
function goOrderDetail(caseId: string) { function goOrderDetail(caseId: string) {
uni.navigateTo({ url: '/pages/mall/consumer/home-service/order-detail?id=' + caseId }) ensureLogin('/pages/mall/consumer/home-service/order-detail?id=' + caseId).then((ok) => {
if (!ok) {
return
}
uni.navigateTo({ url: '/pages/mall/consumer/home-service/order-detail?id=' + caseId })
})
} }
function goDetailByName(serviceName: string) { function goDetailByName(serviceName: string) {

View File

@@ -59,7 +59,17 @@
</view> </view>
</ServicePanel> </ServicePanel>
<ServicePanel title="服务过程" subtitle="当前以 mock 时间线展示预约受理、派单上门过程。"> <ServicePanel title="服务过程" subtitle="基于真实状态日志展示预约受理、派单上门与验收进度。">
<ServiceInfoList
:items="[
{ label: '签到时间:', value: detail.checkinTime != '' ? detail.checkinTime : '暂未签到' },
{ label: '签到地点:', value: detail.checkinAddress != '' ? detail.checkinAddress : '暂未记录' },
{ label: '开始服务:', value: detail.serviceStartedAt != '' ? detail.serviceStartedAt : '暂未开始' },
{ label: '完成服务:', value: detail.serviceFinishedAt != '' ? detail.serviceFinishedAt : '暂未完成' },
{ label: '执行摘要:', value: detail.executionSummary != '' ? detail.executionSummary : '服务人员暂未提交执行摘要' },
{ label: '证据数量:', value: detail.evidenceCount > 0 ? String(detail.evidenceCount) + ' 份' : '暂未上传' }
]"
></ServiceInfoList>
<ServiceTimeline :items="detail.timeline"></ServiceTimeline> <ServiceTimeline :items="detail.timeline"></ServiceTimeline>
</ServicePanel> </ServicePanel>
@@ -82,14 +92,29 @@ import ServiceStatusTag from '@/components/homeService/ServiceStatusTag.uvue'
import ServiceTimeline from '@/components/homeService/ServiceTimeline.uvue' import ServiceTimeline from '@/components/homeService/ServiceTimeline.uvue'
import { fetchConsumerHomeServiceCaseDetail } from '@/services/homeServiceService.uts' import { fetchConsumerHomeServiceCaseDetail } from '@/services/homeServiceService.uts'
import { HomeServiceCaseType } from '@/types/home-service.uts' import { HomeServiceCaseType } from '@/types/home-service.uts'
import { getCurrentUser, getCurrentUserId } from '@/utils/store.uts'
import { goToLogin } from '@/utils/utils.uts'
const caseId = ref('') const caseId = ref('')
const detail = ref<HomeServiceCaseType | null>(null) const detail = ref<HomeServiceCaseType | null>(null)
async function ensureLogin(): Promise<boolean> {
const user = await getCurrentUser()
if (user == null || getCurrentUserId() == '') {
goToLogin('/pages/mall/consumer/home-service/order-detail?id=' + caseId.value)
return false
}
return true
}
async function loadData() { async function loadData() {
if (caseId.value == '') { if (caseId.value == '') {
return return
} }
if (!(await ensureLogin())) {
detail.value = null
return
}
detail.value = await fetchConsumerHomeServiceCaseDetail(caseId.value) detail.value = await fetchConsumerHomeServiceCaseDetail(caseId.value)
} }
@@ -97,7 +122,12 @@ function goFeedback() {
if (caseId.value == '') { if (caseId.value == '') {
return return
} }
uni.navigateTo({ url: '/pages/mall/consumer/home-service/feedback?id=' + caseId.value }) ensureLogin().then((ok) => {
if (!ok) {
return
}
uni.navigateTo({ url: '/pages/mall/consumer/home-service/feedback?id=' + caseId.value })
})
} }
function bookAgain() { function bookAgain() {

View File

@@ -28,12 +28,20 @@
</view> </view>
<ServicePanel title="Step1 服务地址" subtitle="确认上门地址、楼层与入户条件。"> <ServicePanel title="Step1 服务地址" subtitle="确认上门地址、楼层与入户条件。">
<view class="form-item"> <view class="service-address-card" @click="selectAddress">
<text class="label">上门地址</text> <view class="service-address-main">
<view class="value-card" @click="selectAddress"> <text class="service-address-title">服务地址</text>
<text class="value-card-text">{{ addressLineText }}</text> <view v-if="selectedAddress != null" class="service-address-detail">
<text class="value-card-action">点击更换</text> <view class="service-address-contact-row">
<text class="service-address-contact">{{ selectedAddress.contactName }}</text>
<text class="service-address-phone">{{ getSelectedAddressPhone() }}</text>
</view>
<text class="service-address-full">{{ selectedAddress.fullAddress }}</text>
<text v-if="selectedAddress.remark != ''" class="service-address-remark">备注:{{ selectedAddress.remark }}</text>
</view>
<text v-else class="service-address-placeholder">请选择服务地址</text>
</view> </view>
<text class="service-address-arrow">></text>
</view> </view>
<view class="form-grid"> <view class="form-grid">
<view class="form-grid-item"> <view class="form-grid-item">
@@ -271,11 +279,13 @@
<script setup lang="uts"> <script setup lang="uts">
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app' import { onLoad, onShow } from '@dcloudio/uni-app'
import ServicePageHeader from '@/components/homeService/ServicePageHeader.uvue' import ServicePageHeader from '@/components/homeService/ServicePageHeader.uvue'
import ServicePanel from '@/components/homeService/ServicePanel.uvue' import ServicePanel from '@/components/homeService/ServicePanel.uvue'
import { createHomeServiceApplication, fetchHomeServiceCatalog } from '@/services/homeServiceService.uts' import { createHomeServiceApplication, fetchHomeServiceCatalog } from '@/services/homeServiceService.uts'
import { HomeServiceApplicationDraftType, HomeServiceCatalogType } from '@/types/home-service.uts' import { HomeServiceApplicationDraftType, HomeServiceCatalogType, HomeServiceSelectedAddressType } from '@/types/home-service.uts'
import { getCurrentUser, getCurrentUserId } from '@/utils/store.uts'
import { goToLogin } from '@/utils/utils.uts'
import { import {
BookingDayOptionType, BookingDayOptionType,
BookingTimeSlotType, BookingTimeSlotType,
@@ -390,6 +400,7 @@ const servicePackages = ref<Array<ServicePackageOptionType>>([])
const selectedDayId = ref('day-1') const selectedDayId = ref('day-1')
const selectedSlotId = ref('slot-1') const selectedSlotId = ref('slot-1')
const selectedAddress = ref<HomeServiceSelectedAddressType | null>(null)
const careAddress = ref(defaultCareAddress.address) const careAddress = ref(defaultCareAddress.address)
const careDetailAddress = ref(defaultCareAddress.detailAddress) const careDetailAddress = ref(defaultCareAddress.detailAddress)
@@ -499,10 +510,56 @@ function buildDefaultPackages(currentServiceId: string, basePrice: number, durat
} }
const addressLineText = computed((): string => { const addressLineText = computed((): string => {
if (selectedAddress.value != null && selectedAddress.value.fullAddress != '') {
return selectedAddress.value.fullAddress
}
const fullAddress = careAddress.value + ' ' + careDetailAddress.value const fullAddress = careAddress.value + ' ' + careDetailAddress.value
return fullAddress.trim() != '' ? fullAddress : '请选择上门服务地址' return fullAddress.trim() != '' ? fullAddress : '请选择服务地址'
}) })
function applySelectedAddress(address: HomeServiceSelectedAddressType | null): void {
selectedAddress.value = address
if (address == null) {
careAddress.value = ''
careDetailAddress.value = ''
return
}
const locationAddress = address.locationAddress != null && address.locationAddress != '' ? address.locationAddress : address.addressDetail
const doorNo = address.doorNo != null && address.doorNo != '' ? address.doorNo : address.houseNumber
careAddress.value = locationAddress
careDetailAddress.value = doorNo
if (contactName.value == '') {
contactName.value = address.contactName
}
const phoneText = address.phone != null && address.phone != '' ? address.phone : address.contactPhone
if (contactPhone.value == '') {
contactPhone.value = phoneText
}
}
function getSelectedAddressPhone(): string {
if (selectedAddress.value == null) {
return ''
}
if (selectedAddress.value.phone != null && selectedAddress.value.phone != '') {
return selectedAddress.value.phone
}
return selectedAddress.value.contactPhone
}
function loadCachedSelectedAddress(): void {
try {
const cachedAddress = uni.getStorageSync('hss_selected_service_address') as HomeServiceSelectedAddressType | null
if (cachedAddress != null && cachedAddress.fullAddress != '') {
applySelectedAddress(cachedAddress)
return
}
} catch (error) {
console.error('读取服务地址缓存失败', error)
}
applySelectedAddress(null)
}
const selectedTimeText = computed((): string => { const selectedTimeText = computed((): string => {
let selectedDayLabel = '' let selectedDayLabel = ''
for (let i = 0; i < bookingDays.value.length; i++) { for (let i = 0; i < bookingDays.value.length; i++) {
@@ -611,9 +668,20 @@ function buildDemandSummary(): string {
+ ',服务需求:' + selectedNeeds + ',服务需求:' + selectedNeeds
+ ',详细需求:' + demandDetailText.value + ',详细需求:' + demandDetailText.value
+ ',紧急联系人:' + emergencyName.value + ' ' + emergencyPhone.value + ',紧急联系人:' + emergencyName.value + ' ' + emergencyPhone.value
+ ',服务地址备注:' + (selectedAddress.value != null ? selectedAddress.value.remark : '')
+ ',备注:' + remarkText.value + ',备注:' + remarkText.value
} }
function setUnavailableServiceState() {
serviceTitle.value = '服务暂未配置'
serviceSubtitle.value = '当前服务目录未找到该项目,请稍后再试。'
servicePrice.value = 0
serviceDuration.value = '待配置'
serviceSuitableFor.value = '请联系管理员初始化服务目录。'
serviceImageText.value = '服务'
servicePackages.value = [] as Array<ServicePackageOptionType>
}
async function loadData() { async function loadData() {
bookingDays.value = getBookingDayOptions() bookingDays.value = getBookingDayOptions()
bookingSlots.value = getBookingTimeSlots() bookingSlots.value = getBookingTimeSlots()
@@ -633,15 +701,7 @@ async function loadData() {
} }
} }
if (matchedService == null) { if (matchedService == null) {
const fallbackItems = getHomeServiceItems(catalog) setUnavailableServiceState()
if (fallbackItems.length > 0) {
serviceTitle.value = fallbackItems[0].title
serviceSubtitle.value = fallbackItems[0].subtitle
servicePrice.value = fallbackItems[0].price
serviceSuitableFor.value = fallbackItems[0].suitableFor
serviceImageText.value = fallbackItems[0].imageText
}
servicePackages.value = buildDefaultPackages(serviceId.value, servicePrice.value, serviceDuration.value)
return return
} }
serviceTitle.value = matchedService.name serviceTitle.value = matchedService.name
@@ -656,17 +716,22 @@ async function loadData() {
servicePackages.value = buildDefaultPackages(serviceId.value, servicePrice.value, serviceDuration.value) servicePackages.value = buildDefaultPackages(serviceId.value, servicePrice.value, serviceDuration.value)
} }
function selectAddress() { async function ensureLogin(): Promise<boolean> {
if (careAddress.value == '' || careDetailAddress.value == '') { const user = await getCurrentUser()
careAddress.value = defaultCareAddress.address if (user == null || getCurrentUserId() == '') {
careDetailAddress.value = defaultCareAddress.detailAddress goToLogin('/pages/mall/consumer/home-service/service-detail?id=' + serviceId.value + '&mode=booking')
careArea.value = defaultCareAddress.area return false
careFloor.value = defaultCareAddress.floor
careHasElevator.value = defaultCareAddress.hasElevator
uni.showToast({ title: '已填入示例地址', icon: 'none' })
return
} }
uni.navigateTo({ url: '/pages/mall/consumer/address-list' }) return true
}
function selectAddress() {
ensureLogin().then((ok) => {
if (!ok) {
return
}
uni.navigateTo({ url: '/pages/address/address-list' })
})
} }
function handleAgencyReplace() { function handleAgencyReplace() {
@@ -746,8 +811,15 @@ function selectServicePackage(packageId: string) {
} }
async function submitBooking() { async function submitBooking() {
if (careAddress.value == '' || careDetailAddress.value == '') { if (!(await ensureLogin())) {
uni.showToast({ title: '请选择上门服务地址', icon: 'none' }) return
}
if (servicePackages.value.length == 0 || servicePrice.value <= 0) {
uni.showToast({ title: '当前服务暂不可预约', icon: 'none' })
return
}
if (selectedAddress.value == null) {
uni.showToast({ title: '请选择服务地址', icon: 'none' })
return return
} }
if (recipientName.value == '') { if (recipientName.value == '') {
@@ -781,10 +853,15 @@ async function submitBooking() {
phone: contactPhone.value, phone: contactPhone.value,
address: addressLineText.value, address: addressLineText.value,
preferredTime: selectedTimeText.value, preferredTime: selectedTimeText.value,
demandSummary: buildDemandSummary() demandSummary: buildDemandSummary(),
serviceAddressSnapshot: selectedAddress.value
} }
const created = await createHomeServiceApplication(draft) const created = await createHomeServiceApplication(draft)
if (created == null) {
uni.showToast({ title: '预约提交失败,请稍后重试', icon: 'none' })
return
}
uni.showToast({ title: '预约已提交', icon: 'success' }) uni.showToast({ title: '预约已提交', icon: 'success' })
uni.navigateTo({ url: '/pages/mall/consumer/home-service/order-detail?id=' + created.id }) uni.navigateTo({ url: '/pages/mall/consumer/home-service/order-detail?id=' + created.id })
} }
@@ -796,12 +873,14 @@ onLoad((options) => {
} }
const mode = options['mode'] const mode = options['mode']
if (mode != null && mode == 'booking') { if (mode != null && mode == 'booking') {
careAddress.value = defaultCareAddress.address
careDetailAddress.value = defaultCareAddress.detailAddress
careArea.value = defaultCareAddress.area careArea.value = defaultCareAddress.area
} }
loadData() loadData()
}) })
onShow(() => {
loadCachedSelectedAddress()
})
</script> </script>
<style scoped> <style scoped>
@@ -845,6 +924,81 @@ onLoad((options) => {
box-shadow: 0 12rpx 24rpx rgba(15, 23, 42, 0.06); box-shadow: 0 12rpx 24rpx rgba(15, 23, 42, 0.06);
} }
.service-address-card {
background: #ffffff;
border-radius: 24rpx;
padding: 24rpx;
flex-direction: row;
align-items: center;
justify-content: space-between;
border: 1rpx solid rgba(255, 125, 151, 0.16);
margin-bottom: 24rpx;
box-shadow: 0 10rpx 20rpx rgba(15, 23, 42, 0.04);
}
.service-address-main {
flex: 1;
margin-right: 20rpx;
}
.service-address-title {
font-size: 28rpx;
font-weight: 600;
color: #1f2937;
margin-bottom: 12rpx;
}
.service-address-detail {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.service-address-contact-row {
flex-direction: row;
align-items: center;
flex-wrap: wrap;
gap: 16rpx;
}
.service-address-contact,
.service-address-phone,
.service-address-full,
.service-address-remark,
.service-address-placeholder,
.service-address-arrow {
font-size: 24rpx;
}
.service-address-contact {
font-weight: 600;
color: #111827;
}
.service-address-phone {
color: #4b5563;
}
.service-address-full {
color: #374151;
line-height: 1.5;
}
.service-address-remark {
color: #6b7280;
line-height: 1.5;
}
.service-address-placeholder {
color: #9ca3af;
line-height: 1.5;
}
.service-address-arrow {
color: #c18b95;
font-weight: 600;
}
.summary-top-row, .summary-top-row,
.summary-tag-row, .summary-tag-row,
.summary-price-row, .summary-price-row,

View File

@@ -9,6 +9,7 @@
<view class="status-title-row"> <view class="status-title-row">
<text class="status-emoji">{{ getStatusIcon() }}</text> <text class="status-emoji">{{ getStatusIcon() }}</text>
<text class="status-text">{{ getStatusText() }}</text> <text class="status-text">{{ getStatusText() }}</text>
<text v-if="getPendingCountdownText() != ''" class="status-countdown">{{ getPendingCountdownText() }}</text>
</view> </view>
<text class="status-desc">{{ getStatusDesc() }}</text> <text class="status-desc">{{ getStatusDesc() }}</text>
</view> </view>
@@ -135,7 +136,7 @@
</view> </view>
</view> </view>
<view class="action-right"> <view class="action-right">
<view v-if="order?.order_status === 1" class="btn-group"> <view v-if="order?.order_status === 1 && !isTimeoutOrder()" class="btn-group">
<button class="btn" @click="cancelOrder">取消订单</button> <button class="btn" @click="cancelOrder">取消订单</button>
<button class="btn primary" @click="payOrder">立即支付</button> <button class="btn primary" @click="payOrder">立即支付</button>
</view> </view>
@@ -157,9 +158,11 @@
<button class="btn primary" @click="goToReview">评价订单</button> <button class="btn primary" @click="goToReview">评价订单</button>
</view> </view>
<view v-if="order?.order_status === 5" class="btn-group"> <view v-if="shouldShowCancelledActions()" class="btn-group">
<button class="btn primary" @click="rePurchase">重新购买</button> <button class="btn" @click="deleteOrder">删除订单</button>
</view> <button class="btn" @click="viewSimilar">看相似</button>
<button class="btn primary" @click="rePurchase">再次购买</button>
</view>
</view> </view>
</view> </view>
</view> </view>
@@ -167,16 +170,21 @@
</template> </template>
<script setup lang="uts"> <script setup lang="uts">
import { ref, onMounted, computed } from 'vue' import { ref } from 'vue'
import { onLoad, onBackPress } from '@dcloudio/uni-app' import { onBackPress, onHide, onLoad, onShow, onUnload } from '@dcloudio/uni-app'
import { supabaseService } from '@/utils/supabaseService.uts' import { supabaseService } from '@/utils/supabaseService.uts'
import { goToLogin } from '@/utils/utils.uts' import { goToLogin } from '@/utils/utils.uts'
import supa from '@/components/supadb/aksupainstance.uts' import supa from '@/components/supadb/aksupainstance.uts'
import { formatCountdownHMS, getOrderDisplayStatus, getRemainingSeconds, isOrderPayExpired, ORDER_STATUS_CANCELLED, ORDER_STATUS_PENDING, ORDER_TIMEOUT_CANCEL_REASON, PAYMENT_STATUS_TIMEOUT, PAYMENT_STATUS_UNPAID, type OrderStatusSource } from '@/utils/orderStatus.uts'
// 定义订单类型 // 定义订单类型
type OrderType = { type OrderType = {
order_no: string, order_no: string,
order_status: number, order_status: number,
payment_status: number,
cancel_reason: string,
pay_expire_at: string,
consumer_deleted_at: string,
total_amount: number, total_amount: number,
product_amount: number, product_amount: number,
shipping_fee: number, shipping_fee: number,
@@ -221,10 +229,51 @@ const orderItems = ref<OrderItemType[]>([])
const shopName = ref('店铺名称') const shopName = ref('店铺名称')
const deliveryAddress = ref<AddressType | null>(null) const deliveryAddress = ref<AddressType | null>(null)
const deliveryInfo = ref<DeliveryInfoType | null>(null) const deliveryInfo = ref<DeliveryInfoType | null>(null)
const nowTick = ref<number>(Date.now())
let detailTicker = 0
const toOrderStatusSource = (): OrderStatusSource | null => {
const currentOrder = order.value
if (currentOrder == null) return null
return {
order_status: currentOrder.order_status,
payment_status: currentOrder.payment_status,
pay_expire_at: currentOrder.pay_expire_at,
created_at: currentOrder.created_at,
cancel_reason: currentOrder.cancel_reason
}
}
const isTimeoutOrder = (): boolean => {
const source = toOrderStatusSource()
if (source == null) return false
return isOrderPayExpired(source)
}
const getPendingCountdownText = (): string => {
const source = toOrderStatusSource()
if (source == null) return ''
const currentTick = nowTick.value
if (currentTick < 0) return ''
if (getOrderDisplayStatus(source) != 'pending') return ''
return formatCountdownHMS(getRemainingSeconds(source))
}
const shouldShowCancelledActions = (): boolean => {
const source = toOrderStatusSource()
if (source == null) return false
return getOrderDisplayStatus(source) == 'cancelled'
}
// 辅助函数 - 必须在调用前定义 // 辅助函数 - 必须在调用前定义
const getStatusText = (): string => { const getStatusText = (): string => {
const status = order.value?.order_status ?? 0 const status = order.value?.order_status ?? 0
const source = toOrderStatusSource()
if (source != null) {
const displayStatus = getOrderDisplayStatus(source)
if (displayStatus == 'pending') return '待付款'
if (displayStatus == 'cancelled') return '已取消'
}
if (status == 1) return '待付款' if (status == 1) return '待付款'
if (status == 2) return '待发货' if (status == 2) return '待发货'
if (status == 3) return '待收货' if (status == 3) return '待收货'
@@ -237,6 +286,20 @@ const getStatusText = (): string => {
const getStatusDesc = (): string => { const getStatusDesc = (): string => {
const status = order.value?.order_status ?? 0 const status = order.value?.order_status ?? 0
const source = toOrderStatusSource()
if (source != null) {
const displayStatus = getOrderDisplayStatus(source)
if (displayStatus == 'pending') {
return '请在 ' + getPendingCountdownText() + ' 内支付'
}
if (displayStatus == 'cancelled') {
const currentReason = order.value?.cancel_reason ?? ''
if (currentReason.indexOf('超时') >= 0) {
return '订单超时未支付,已自动取消'
}
return '订单已取消'
}
}
if (status == 1) return '请尽快完成支付' if (status == 1) return '请尽快完成支付'
if (status == 2) return '商家正在打包商品' if (status == 2) return '商家正在打包商品'
if (status == 3) return '商品正在赶往您的地址' if (status == 3) return '商品正在赶往您的地址'
@@ -249,6 +312,7 @@ const getStatusDesc = (): string => {
const getStatusIcon = (): string => { const getStatusIcon = (): string => {
const status = order.value?.order_status ?? 0 const status = order.value?.order_status ?? 0
if (shouldShowCancelledActions()) return '⏰'
if (status === 1) return '💳' if (status === 1) return '💳'
if (status === 2) return '📦' if (status === 2) return '📦'
if (status === 3) return '🚚' if (status === 3) return '🚚'
@@ -257,8 +321,14 @@ const getStatusIcon = (): string => {
} }
const getStatusClass = (): string => { const getStatusClass = (): string => {
const source = toOrderStatusSource()
if (source != null) {
const displayStatus = getOrderDisplayStatus(source)
if (displayStatus == 'pending') return 'status-pending'
if (displayStatus == 'cancelled') return 'status-cancelled'
}
const status = order.value?.order_status ?? 0 const status = order.value?.order_status ?? 0
return `status-${status}` return 'status-' + status
} }
const getFullAddress = (addr: any): string => { const getFullAddress = (addr: any): string => {
@@ -389,6 +459,10 @@ const loadOrderDetail = async () => {
order.value = { order.value = {
order_no: (dataObj.get('order_no') ?? '') as string, order_no: (dataObj.get('order_no') ?? '') as string,
order_status: (dataObj.get('order_status') ?? 1) as number, order_status: (dataObj.get('order_status') ?? 1) as number,
payment_status: (dataObj.get('payment_status') ?? 1) as number,
cancel_reason: (dataObj.get('cancel_reason') ?? '') as string,
pay_expire_at: (dataObj.get('pay_expire_at') ?? '') as string,
consumer_deleted_at: (dataObj.get('consumer_deleted_at') ?? '') as string,
total_amount: (dataObj.get('total_amount') ?? 0) as number, total_amount: (dataObj.get('total_amount') ?? 0) as number,
product_amount: (dataObj.get('product_amount') ?? 0) as number, product_amount: (dataObj.get('product_amount') ?? 0) as number,
shipping_fee: (dataObj.get('shipping_fee') ?? 0) as number, shipping_fee: (dataObj.get('shipping_fee') ?? 0) as number,
@@ -424,6 +498,15 @@ const loadOrderDetail = async () => {
orderItems.value.push(orderItem) orderItems.value.push(orderItem)
} }
} }
if (order.value.consumer_deleted_at != '') {
order.value = null
uni.showToast({ title: '订单不存在', icon: 'none' })
setTimeout(() => {
uni.redirectTo({ url: '/pages/mall/consumer/orders' })
}, 600)
return
}
const addressRaw = dataObj.get('shipping_address') const addressRaw = dataObj.get('shipping_address')
console.log('[loadOrderDetail] 收货地址数据:', addressRaw) console.log('[loadOrderDetail] 收货地址数据:', addressRaw)
@@ -470,6 +553,7 @@ const loadOrderDetail = async () => {
} }
console.log('[loadOrderDetail] 订单详情加载成功,商品数量:', orderItems.value.length) console.log('[loadOrderDetail] 订单详情加载成功,商品数量:', orderItems.value.length)
syncTimeoutState()
} else { } else {
uni.showToast({ title: '订单不存在', icon: 'none' }) uni.showToast({ title: '订单不存在', icon: 'none' })
} }
@@ -481,6 +565,35 @@ const loadOrderDetail = async () => {
} }
} }
const syncTimeoutState = (): void => {
nowTick.value = Date.now()
const currentOrder = order.value
if (currentOrder == null) return
if (currentOrder.order_status == ORDER_STATUS_PENDING && currentOrder.payment_status == PAYMENT_STATUS_UNPAID && isTimeoutOrder()) {
currentOrder.order_status = ORDER_STATUS_CANCELLED
currentOrder.payment_status = PAYMENT_STATUS_TIMEOUT
if (currentOrder.cancel_reason == '') {
currentOrder.cancel_reason = ORDER_TIMEOUT_CANCEL_REASON
}
supabaseService.expireOrder(orderId.value)
}
}
const startDetailTicker = (): void => {
if (detailTicker > 0) return
syncTimeoutState()
detailTicker = setInterval(() => {
syncTimeoutState()
}, 1000)
}
const stopDetailTicker = (): void => {
if (detailTicker > 0) {
clearInterval(detailTicker)
detailTicker = 0
}
}
// 动作函数 // 动作函数
const contactService = () => { const contactService = () => {
const userId = supabaseService.getCurrentUserId() const userId = supabaseService.getCurrentUserId()
@@ -508,13 +621,49 @@ const contactService = () => {
} }
} }
const payOrder = () => { const payOrder = async () => {
if (isTimeoutOrder()) {
uni.showToast({ title: '订单已取消,不能继续支付', icon: 'none' })
loadOrderDetail()
return
}
const totalAmount = order.value?.total_amount ?? 0 const totalAmount = order.value?.total_amount ?? 0
const userId = supabaseService.getCurrentUserId() const userId = supabaseService.getCurrentUserId()
if (userId == null || userId === '') { if (userId == null || userId === '') {
goToLogin(`/pages/mall/consumer/payment?orderId=${orderId.value}&amount=${totalAmount}`) goToLogin(`/pages/mall/consumer/payment?orderId=${orderId.value}&amount=${totalAmount}`)
return return
} }
const latestOrder = await supabaseService.getOrderDetail(orderId.value)
if (latestOrder != null) {
const latestObj = JSON.parse(JSON.stringify(latestOrder)) as UTSJSONObject
const latestStatus = latestObj.getNumber('order_status') ?? 1
const latestPaymentStatus = latestObj.getNumber('payment_status') ?? 1
const latestCancelReason = latestObj.getString('cancel_reason') ?? ''
const latestPayExpireAt = latestObj.getString('pay_expire_at') ?? ''
if (order.value != null) {
order.value.order_status = latestStatus
order.value.payment_status = latestPaymentStatus
order.value.cancel_reason = latestCancelReason
order.value.pay_expire_at = latestPayExpireAt
}
if (isTimeoutOrder()) {
await supabaseService.expireOrder(orderId.value)
if (order.value != null) {
order.value.order_status = ORDER_STATUS_CANCELLED
order.value.payment_status = PAYMENT_STATUS_TIMEOUT
order.value.cancel_reason = ORDER_TIMEOUT_CANCEL_REASON
}
uni.showToast({ title: '订单已取消,不能继续支付', icon: 'none' })
return
}
if (latestStatus != ORDER_STATUS_PENDING || latestPaymentStatus != PAYMENT_STATUS_UNPAID) {
uni.showToast({ title: '订单状态已变更,不能继续支付', icon: 'none' })
return
}
}
uni.navigateTo({ uni.navigateTo({
url: `/pages/mall/consumer/payment?orderId=${orderId.value}&amount=${totalAmount}` url: `/pages/mall/consumer/payment?orderId=${orderId.value}&amount=${totalAmount}`
}) })
@@ -522,23 +671,14 @@ const payOrder = () => {
const doCancelOrder = async () => { const doCancelOrder = async () => {
try { try {
const updatePayload = new UTSJSONObject() const result = await supabaseService.cancelOrder(orderId.value)
updatePayload.set('order_status', 5) if (result) {
updatePayload.set('updated_at', new Date().toISOString())
const result = await supa
.from('ml_orders')
.update(updatePayload)
.eq('id', orderId.value)
.execute()
if (result.error == null) {
if (order.value != null) { if (order.value != null) {
order.value.order_status = 5 order.value.order_status = ORDER_STATUS_CANCELLED
order.value.payment_status = PAYMENT_STATUS_TIMEOUT
} }
uni.showToast({ title: '订单已取消' }) uni.showToast({ title: '订单已取消' })
} else { } else {
console.error('[doCancelOrder] 取消订单失败:', result.error)
uni.showToast({ title: '取消失败', icon: 'none' }) uni.showToast({ title: '取消失败', icon: 'none' })
} }
} catch (e) { } catch (e) {
@@ -547,6 +687,34 @@ const doCancelOrder = async () => {
} }
} }
const deleteOrder = async () => {
uni.showLoading({ title: '删除中...' })
try {
const success = await supabaseService.softDeleteOrderForConsumer(orderId.value)
uni.hideLoading()
if (!success) {
uni.showToast({ title: '删除失败', icon: 'none' })
return
}
uni.showToast({ title: '订单已删除', icon: 'success' })
setTimeout(() => {
uni.redirectTo({ url: '/pages/mall/consumer/orders' })
}, 600)
} catch (e) {
uni.hideLoading()
uni.showToast({ title: '删除失败', icon: 'none' })
}
}
const viewSimilar = () => {
if (orderItems.value.length == 0) {
uni.showToast({ title: '暂无相似商品', icon: 'none' })
return
}
const keyword = orderItems.value[0].product_name != '' ? orderItems.value[0].product_name : '商品'
uni.navigateTo({ url: `/pages/mall/consumer/search?keyword=${encodeURIComponent(keyword)}` })
}
const cancelOrder = () => { const cancelOrder = () => {
uni.showModal({ uni.showModal({
title: '提示', title: '提示',
@@ -801,6 +969,21 @@ onLoad((options) => {
} }
}) })
onShow(() => {
if (orderId.value != '') {
loadOrderDetail()
}
startDetailTicker()
})
onHide(() => {
stopDetailTicker()
})
onUnload(() => {
stopDetailTicker()
})
</script> </script>
<style scoped> <style scoped>
@@ -868,12 +1051,26 @@ onLoad((options) => {
letter-spacing: 1px; letter-spacing: 1px;
} }
.status-countdown {
font-size: 18px;
font-weight: bold;
margin-left: 8px;
}
.status-desc { .status-desc {
font-size: 14px; font-size: 14px;
opacity: 0.95; opacity: 0.95;
text-align: center; text-align: center;
} }
.status-pending {
background: linear-gradient(135deg, #ff9000, #ff5000);
}
.status-cancelled {
background: linear-gradient(135deg, #9aa5b1, #6b7280);
}
/* 分享免单入口 */ /* 分享免单入口 */
.share-free-entry { .share-free-entry {
margin-top: 20px; margin-top: 20px;

File diff suppressed because it is too large Load Diff

View File

@@ -28,6 +28,27 @@ const orderId = ref('')
const orderNo = ref('') const orderNo = ref('')
const amount = ref(0) const amount = ref(0)
const getOptionString = (options: UTSJSONObject, key: string): string => {
try {
const value = options.getString(key)
if (value != null) {
return value
}
} catch (e) {
}
try {
const normalized = JSON.parse(JSON.stringify(options)) as UTSJSONObject
const value = normalized.getString(key)
if (value != null) {
return value
}
} catch (e) {
}
return ''
}
// 定义 loadOrderInfo 函数(必须在 onMounted 之前) // 定义 loadOrderInfo 函数(必须在 onMounted 之前)
const loadOrderInfo = async () => { const loadOrderInfo = async () => {
try { try {
@@ -60,12 +81,12 @@ onLoad((options) => {
if (options == null) return if (options == null) return
const optionsObj = options as UTSJSONObject const optionsObj = options as UTSJSONObject
const orderIdValue = optionsObj.getString('orderId') ?? '' const orderIdValue = getOptionString(optionsObj, 'orderId')
if (orderIdValue != '') { if (orderIdValue != '') {
orderId.value = orderIdValue orderId.value = orderIdValue
orderNo.value = orderIdValue orderNo.value = orderIdValue
const amountValue = optionsObj.getString('amount') ?? '' const amountValue = getOptionString(optionsObj, 'amount')
if (amountValue != '') { if (amountValue != '') {
const amountStr = amountValue const amountStr = amountValue
console.log('[payment-success] amountStr:', amountStr) console.log('[payment-success] amountStr:', amountStr)

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
<template> <template>
<view class="search-page"> <view class="search-page">
<view class="search-navbar" :style="navbarStyle"> <view class="search-navbar" :style="navbarStyle">
<view class="back-btn" @click="goBack"> <view class="back-btn" @click="goBack">
<text class="back-icon"></text> <text class="back-icon"><</text>
</view> </view>
<view class="jd-search-box"> <view class="jd-search-box">
@@ -12,7 +12,7 @@
:value="searchKeyword" :value="searchKeyword"
@input="onInput" @input="onInput"
@confirm="onSearch" @confirm="onSearch"
:placeholder="(placeholderKeyword != null && placeholderKeyword !== '' && searchKeyword === '') ? placeholderKeyword : '请输入商品名称、店铺'" :placeholder="(placeholderKeyword != null && placeholderKeyword !== '' && searchKeyword === '') ? placeholderKeyword : '请输入商品名称、店铺'"
placeholder-class="jd-placeholder" placeholder-class="jd-placeholder"
:focus="autoFocus" :focus="autoFocus"
confirm-type="search" confirm-type="search"
@@ -23,7 +23,7 @@
</view> </view>
<view class="camera-wrap" @tap.stop="handleCameraSearch"> <view class="camera-wrap" @tap.stop="handleCameraSearch">
<text class="camera-icon">📷</text> <text class="camera-icon"></text>
</view> </view>
<view class="search-submit-btn" @tap.stop="onSearch"> <view class="search-submit-btn" @tap.stop="onSearch">
@@ -32,23 +32,25 @@
</view> </view>
</view> </view>
<!-- 错误状态(模拟服务器超时) --> <!-- 错误状态 -->
<view v-if="isError" class="error-state" @click="retryLoad"> <view v-if="isError" class="error-state" @click="retryLoad">
<view class="error-content"> <view class="error-content">
<text class="error-icon">⚠️</text> <text class="error-icon">!</text>
<text class="error-title">加载服务超时</text> <text class="error-title">加载服务超时</text>
<text class="error-desc">请点击屏幕重试</text> <text class="error-desc">请点击屏幕重试</text>
</view> </view>
</view> </view>
<!-- 主内容区域:使用 scroll-view 支持安卓端滚动 --> <!-- 涓诲唴瀹瑰尯鍩燂細浣跨敤 scroll-view 鏀寔瀹夊崜绔粴鍔?-->
<scroll-view <scroll-view
v-else v-else
class="main-content" class="main-content"
direction="vertical" direction="vertical"
:show-scrollbar="false" :show-scrollbar="false"
> :lower-threshold="120"
<!-- 初始状态(无搜索词) --> @scrolltolower="handleMainScrollToLower"
>
<!-- 初始状态 -->
<view v-if="searchKeyword == '' && showResults == false"> <view v-if="searchKeyword == '' && showResults == false">
<!-- 搜索历史 --> <!-- 搜索历史 -->
<view v-if="searchHistory.length > 0" class="search-history"> <view v-if="searchHistory.length > 0" class="search-history">
@@ -97,51 +99,21 @@
> >
<text class="hot-rank" :class="index < 3 ? 'top-three' : ''">{{ index + 1 }}</text> <text class="hot-rank" :class="index < 3 ? 'top-three' : ''">{{ index + 1 }}</text>
<text class="hot-text">{{ item.keyword }}</text> <text class="hot-text">{{ item.keyword }}</text>
<text v-if="item.hot == true" class="hot-icon">🔥</text> <text v-if="item.hot == true" class="hot-icon"></text>
</view> </view>
</view> </view>
</view> </view>
<!-- 推荐商品(猜你喜欢 --> <!-- 猜你喜欢 -->
<view class="guess-you-like"> <view class="guess-you-like">
<view class="section-header"> <GuessYouLike
<view class="title-with-icon"> title="猜你喜欢"
<text class="section-icon">✨</text> :pageSize="8"
<text class="section-title">猜你喜欢</text> :loadMoreKey="guessLoadMoreKey"
</view> @productClick="goToProductDetail"
<text class="refresh-btn" @click="refreshGuessList">换一批</text> />
</view> </view>
<view class="guess-grid">
<view
v-for="item in recommendList"
:key="(item.id != null ? item.id : item.name)"
class="guess-item"
@click="viewProductDetail(item)"
>
<image class="guess-img" :src="item.image" mode="aspectFill" />
<view v-if="item.cardTags.length > 0" class="card-tags-row">
<text v-for="(tag, index) in item.cardTags" :key="item.id + '-guess-tag-' + index" class="card-tag">{{ tag }}</text>
</view>
<text class="guess-name" :lines="2">{{ item.name }}</text>
<text v-if="item.highlight !== ''" class="card-highlight">{{ item.highlight }}</text>
<view v-if="item.serviceTags.length > 0" class="service-tags-row">
<text v-for="(tag, index) in item.serviceTags" :key="item.id + '-guess-service-' + index" class="service-tag">{{ tag }}</text>
</view>
<view class="guess-bottom">
<view class="price-stack">
<text class="guess-price">¥{{ item.price }}</text>
<text v-if="item.marketPrice !== ''" class="market-price">¥{{ item.marketPrice }}</text>
</view>
<view class="guess-add-btn" @click.stop="addToCart(item)">
<text class="guess-add-icon">+</text>
</view>
</view>
<text v-if="item.salesText !== ''" class="card-sales-text">{{ item.salesText }}</text>
</view>
</view>
</view> </view>
</view>
<!-- 搜索建议 --> <!-- 搜索建议 -->
<view v-if="searchKeyword != '' && showResults == false" class="search-suggestions"> <view v-if="searchKeyword != '' && showResults == false" class="search-suggestions">
@@ -152,7 +124,7 @@
class="suggestion-item" class="suggestion-item"
@click="selectSuggestion(suggestion)" @click="selectSuggestion(suggestion)"
> >
<view class="suggestion-icon">🔍</view> <view class="suggestion-icon"></view>
<text class="suggestion-text">{{ suggestion }}</text> <text class="suggestion-text">{{ suggestion }}</text>
</view> </view>
</view> </view>
@@ -176,7 +148,7 @@
<image class="shop-logo" :src="shop.logo" mode="aspectFill" /> <image class="shop-logo" :src="shop.logo" mode="aspectFill" />
<view class="shop-info"> <view class="shop-info">
<text class="shop-name-txt">{{ shop.name }}</text> <text class="shop-name-txt">{{ shop.name }}</text>
<text class="shop-products-txt">共{{ shop.productCount }}件商品</text> <text class="shop-products-txt">共 {{ shop.productCount }} 件商品</text>
</view> </view>
</view> </view>
</view> </view>
@@ -195,13 +167,13 @@
class="filter-tab" class="filter-tab"
:class="{ active: activeSort === 'sales' }" :class="{ active: activeSort === 'sales' }"
@click="switchSort('sales')" @click="switchSort('sales')"
>销量</text> >销量</text>
<text <text
class="filter-tab" class="filter-tab"
:class="{ active: activeSort === 'price' }" :class="{ active: activeSort === 'price' }"
@click="switchSort('price')" @click="switchSort('price')"
> >
价格 {{ activeSort === 'price' ? (priceSortAsc ? '↑' : '↓') : '' }} 价格 {{ activeSort === 'price' ? (priceSortAsc ? '↑' : '↓') : '' }}
</text> </text>
</view> </view>
</view> </view>
@@ -235,14 +207,14 @@
</view> </view>
</view> </view>
<!-- 空结果 - 仅在非加载状态且无结果时显示 --> <!-- 空结果 -->
<view v-if="!loading && searchResults.length === 0" class="empty-result"> <view v-if="!loading && searchResults.length === 0" class="empty-result">
<text class="empty-icon">🤔</text> <text class="empty-icon">!</text>
<text class="empty-text">未找到相关商品</text> <text class="empty-text">未找到相关商品</text>
<text class="empty-sub">换个关键词试试吧</text> <text class="empty-sub">换个关键词试试吧</text>
</view> </view>
<!-- 加载更多/加载中 - 在加载状态或有更多数据时显示 --> <!-- 加载更多 -->
<view v-if="loading" class="loading-more"> <view v-if="loading" class="loading-more">
<view class="loading-spinner"></view> <view class="loading-spinner"></view>
<text class="loading-text">加载中...</text> <text class="loading-text">加载中...</text>
@@ -252,41 +224,14 @@
<text class="no-more-text">--- 到底了 ---</text> <text class="no-more-text">--- 到底了 ---</text>
</view> </view>
<view v-if="searchResults.length > 0 && recommendList.length > 0" class="guess-you-like" style="margin-top: 16rpx;"> <view v-if="searchResults.length > 0" class="guess-you-like" style="margin-top: 16rpx;">
<view class="section-header"> <GuessYouLike
<view class="title-with-icon"> title="猜你喜欢"
<text class="section-icon">✨</text> :pageSize="8"
<text class="section-title">猜你喜欢</text> :excludeProductIds="searchResultProductIds"
</view> :loadMoreKey="guessLoadMoreKey"
</view> @productClick="goToProductDetail"
<view class="guess-grid"> />
<view
v-for="item in recommendList"
:key="(item.id != null ? item.id : '') + '_rec'"
class="guess-item"
@click="viewProductDetail(item)"
>
<image class="guess-img" :src="item.image" mode="aspectFill" />
<view v-if="item.cardTags.length > 0" class="card-tags-row">
<text v-for="(tag, index) in item.cardTags" :key="item.id + '-rec-tag-' + index" class="card-tag">{{ tag }}</text>
</view>
<text class="guess-name" :lines="2">{{ item.name }}</text>
<text v-if="item.highlight !== ''" class="card-highlight">{{ item.highlight }}</text>
<view v-if="item.serviceTags.length > 0" class="service-tags-row">
<text v-for="(tag, index) in item.serviceTags" :key="item.id + '-rec-service-' + index" class="service-tag">{{ tag }}</text>
</view>
<view class="guess-bottom">
<view class="price-stack">
<text class="guess-price">¥{{ item.price }}</text>
<text v-if="item.marketPrice !== ''" class="market-price">¥{{ item.marketPrice }}</text>
</view>
<view class="guess-add-btn" @click.stop="addToCart(item)">
<text class="guess-add-icon">+</text>
</view>
</view>
<text v-if="item.salesText !== ''" class="card-sales-text">{{ item.salesText }}</text>
</view>
</view>
</view> </view>
</view> </view>
@@ -302,6 +247,7 @@ import { onPageScroll, onReachBottom } from '@dcloudio/uni-app'
import { supabaseService } from '@/utils/supabaseService.uts' import { supabaseService } from '@/utils/supabaseService.uts'
import type { Product } from '@/utils/supabaseService.uts' import type { Product } from '@/utils/supabaseService.uts'
import { goToLogin } from '@/utils/utils.uts' import { goToLogin } from '@/utils/utils.uts'
import GuessYouLike from '@/components/mall/GuessYouLike/GuessYouLike.uvue'
// 状态定义 // 状态定义
const statusBarHeight = ref(0) const statusBarHeight = ref(0)
@@ -316,6 +262,7 @@ const loading = ref(false)
const hasMore = ref(true) const hasMore = ref(true)
const isError = ref(false) const isError = ref(false)
const autoFocus = ref(true) const autoFocus = ref(true)
const guessLoadMoreKey = ref<number>(0)
const navbarStyle = computed(() => { const navbarStyle = computed(() => {
const top = navBarTop.value > 0 ? navBarTop.value : statusBarHeight.value const top = navBarTop.value > 0 ? navBarTop.value : statusBarHeight.value
@@ -330,7 +277,6 @@ const handleCameraSearch = () => {
const activeSort = ref('default') // 当前排序方式: default, sales, price const activeSort = ref('default') // 当前排序方式: default, sales, price
const priceSortAsc = ref(false) // 价格排序是否为升序 const priceSortAsc = ref(false) // 价格排序是否为升序
type HotSearchItemType = { type HotSearchItemType = {
keyword: string keyword: string
hot: boolean hot: boolean
@@ -436,21 +382,21 @@ const guessList = ref<Array<GuessItemType>>([])
const allGuessItems = ref<Array<GuessItemType>>([]) const allGuessItems = ref<Array<GuessItemType>>([])
const DEFAULT_HOT_KEYWORDS: Array<string> = [ const DEFAULT_HOT_KEYWORDS: Array<string> = [
'大疆neo2', '澶х枂neo2',
'iPhone 15 Pro', 'iPhone 15 Pro',
'Nike Air Max 270', 'Nike Air Max 270',
'厨具', '厨具',
'老干妈', '老干妈',
'钢化膜', '钢化膜',
'手机壳', '手机壳',
'零食坚果', '零食坚果',
'新鲜水果', '新鲜水果',
'液态硅胶壳', '液态硅胶壳',
'充电宝', '充电宝',
'蓝牙耳机' '蓝牙耳机'
] ]
// 推荐商品区(用于 “猜你喜欢/推荐商品”) // 推荐商品区
const recommendList = ref<Array<GuessItemType>>([]) const recommendList = ref<Array<GuessItemType>>([])
const recommendPool = ref<Array<GuessItemType>>([]) const recommendPool = ref<Array<GuessItemType>>([])
const recommendPage = ref(0) const recommendPage = ref(0)
@@ -459,6 +405,16 @@ const recommendAppendSize = ref(20)
const loadingRecommend = ref(false) const loadingRecommend = ref(false)
const searchResults = ref<Array<SearchResultType>>([]) const searchResults = ref<Array<SearchResultType>>([])
const searchShopResults = ref<Array<ShopResultType>>([]) const searchShopResults = ref<Array<ShopResultType>>([])
const searchResultProductIds = computed((): Array<string> => {
const ids: Array<string> = []
for (let i = 0; i < searchResults.value.length; i++) {
const id = searchResults.value[i].id
if (id !== '' && ids.indexOf(id) < 0) {
ids.push(id)
}
}
return ids
})
const SEARCH_HISTORY_KEY = 'consumer_search_history' const SEARCH_HISTORY_KEY = 'consumer_search_history'
@@ -504,8 +460,8 @@ const MAX_EXPANDED_COUNT = 24
const clearHistory = () => { const clearHistory = () => {
uni.showModal({ uni.showModal({
title: '提示', title: '提示',
content: '确定清空搜索历史吗?', content: '确定清空搜索历史吗?',
success: (res) => { success: (res) => {
if (res.confirm) { if (res.confirm) {
searchHistory.value = [] searchHistory.value = []
@@ -525,8 +481,6 @@ const deleteHistoryItem = (index: number) => {
const toggleHistoryEdit = () => { isEditMode.value = !isEditMode.value } const toggleHistoryEdit = () => { isEditMode.value = !isEditMode.value }
const toggleHistoryExpanded = () => { historyExpanded.value = !historyExpanded.value } const toggleHistoryExpanded = () => { historyExpanded.value = !historyExpanded.value }
import { computed } from 'vue'
const visibleHistory = computed(() => { const visibleHistory = computed(() => {
if (historyExpanded.value) { if (historyExpanded.value) {
const maxLen = searchHistory.value.length < MAX_EXPANDED_COUNT ? searchHistory.value.length : MAX_EXPANDED_COUNT const maxLen = searchHistory.value.length < MAX_EXPANDED_COUNT ? searchHistory.value.length : MAX_EXPANDED_COUNT
@@ -577,7 +531,7 @@ const loadData = async (): Promise<void> => {
const hotResult = await supabaseService.getHotProducts(30) const hotResult = await supabaseService.getHotProducts(30)
hotProducts = hotResult as Product[] hotProducts = hotResult as Product[]
} catch (hotError) { } catch (hotError) {
console.error('获取热销商品失败,使用空列表:', hotError) console.error('获取热销商品失败,使用空列表:', hotError)
hotProducts = [] hotProducts = []
} }
@@ -656,7 +610,7 @@ const loadData = async (): Promise<void> => {
} }
const retryLoad = () => { const retryLoad = () => {
uni.showLoading({ title: '重新加载中' }) uni.showLoading({ title: '重新加载中...' })
setTimeout(() => { setTimeout(() => {
uni.hideLoading() uni.hideLoading()
loadData() loadData()
@@ -774,7 +728,7 @@ const performSearch = async (): Promise<void> => {
image: p.main_image_url ?? '/static/default.jpg', image: p.main_image_url ?? '/static/default.jpg',
price: p.base_price ?? 0, price: p.base_price ?? 0,
marketPrice: formatMarketPriceText(p), marketPrice: formatMarketPriceText(p),
specification: p.specification ?? '标准规格', specification: p.specification ?? '标准规格',
tag: tag, tag: tag,
sales: p.sale_count ?? 0, sales: p.sale_count ?? 0,
salesText: formatSalesText(p), salesText: formatSalesText(p),
@@ -848,7 +802,7 @@ const initPage = () => {
} }
} }
} catch (e) { } catch (e) {
console.error('初始化失败', e) console.error('初始化失败:', e)
isError.value = true isError.value = true
} }
} }
@@ -891,16 +845,16 @@ const onSearch = () => {
} }
if (effective === '') return if (effective === '') return
addToHistory(effective) addToHistory(effective)
// 如果搜索词来自 placeholder保输入框为空但执行搜索 // 如果搜索词来自 placeholder输入框为空但执行搜索
if (userInput === '') { if (userInput === '') {
// 保持 searchKeyword 为空 but perform search with effective // 保持 searchKeyword 为空,但使用 effective 搜索
searchKeyword.value = '' searchKeyword.value = ''
} }
// 将 searchKeyword 临时设置为 effective 以便 performSearch 使用performSearch 使用 searchKeyword // 临时将 searchKeyword 设置为 effective,供 performSearch 使用
const prev = searchKeyword.value const prev = searchKeyword.value
searchKeyword.value = effective searchKeyword.value = effective
performSearch() performSearch()
// 恢复输入框为空状态(如果用户未输入) // 如果用户没有手动输入,则恢复为空
if (userInput === '') searchKeyword.value = prev if (userInput === '') searchKeyword.value = prev
} }
@@ -934,12 +888,12 @@ const switchSort = (type: string) => {
priceSortAsc.value = !priceSortAsc.value priceSortAsc.value = !priceSortAsc.value
} else { } else {
activeSort.value = 'price' activeSort.value = 'price'
priceSortAsc.value = true // 默认升序 priceSortAsc.value = true // 默认升序
} }
} else { } else {
activeSort.value = type activeSort.value = type
} }
// 重新执行搜索以获取正确排序的数据 // 重新搜索以获取正确排序的数据
performSearch() performSearch()
} }
@@ -984,7 +938,7 @@ const loadMore = async (): Promise<void> => {
image: p.main_image_url ?? '/static/default.jpg', image: p.main_image_url ?? '/static/default.jpg',
price: p.base_price ?? 0, price: p.base_price ?? 0,
marketPrice: formatMarketPriceText(p), marketPrice: formatMarketPriceText(p),
specification: p.specification ?? '标准规格', specification: p.specification ?? '标准规格',
tag: tag, tag: tag,
sales: p.sale_count ?? 0, sales: p.sale_count ?? 0,
salesText: formatSalesText(p), salesText: formatSalesText(p),
@@ -1004,67 +958,26 @@ const loadMore = async (): Promise<void> => {
} }
} }
async function loadRecommendGoods(appendSize: number): Promise<void> {
if (loadingRecommend.value) return
loadingRecommend.value = true
try {
// 如果后端支持分页接口,可在此调用;当前使用本地 pool
if (appendSize == null || appendSize <= 0) {
// 不追加时确保至少保持初始量
recommendList.value = recommendPool.value.slice(0, recommendInitialSize.value)
recommendPage.value = 1
} else {
const startIndex = recommendList.value.length
const endIndex = startIndex + appendSize
const slice = recommendPool.value.slice(startIndex, endIndex)
// 如果池不够,尝试循环或留空
if (slice.length === 0) {
// 将池随机打乱并继续追加(循环播放)
const arr = recommendPool.value.slice()
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
const t = arr[i]; arr[i] = arr[j]; arr[j] = t
}
recommendPool.value = arr
const more = recommendPool.value.slice(0, appendSize)
recommendList.value = recommendList.value.concat(more)
} else {
recommendList.value = recommendList.value.concat(slice)
}
recommendPage.value += 1
}
} catch (e) {
console.error('加载推荐商品失败', e)
} finally {
loadingRecommend.value = false
}
}
function refreshGuessList(): void {
loadRecommendGoods(recommendAppendSize.value)
}
function handleReachBottom(): void { function handleReachBottom(): void {
if (showResults.value === true) { if (showResults.value === true) {
loadMore() if (hasMore.value) {
loadMore()
return
}
guessLoadMoreKey.value = guessLoadMoreKey.value + 1
return return
} }
loadRecommendGoods(recommendAppendSize.value) guessLoadMoreKey.value = guessLoadMoreKey.value + 1
}
function handleMainScrollToLower(): void {
handleReachBottom()
} }
onReachBottom(() => { onReachBottom(() => {
handleReachBottom() handleReachBottom()
}) })
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=${id}&price=${price}&name=${encodeURIComponent(name)}`
})
}
const viewShopDetail = (shop: ShopResultType) => { const viewShopDetail = (shop: ShopResultType) => {
uni.navigateTo({ uni.navigateTo({
url: `/pages/mall/consumer/shop-detail?id=${shop.id}` url: `/pages/mall/consumer/shop-detail?id=${shop.id}`
@@ -1077,40 +990,40 @@ const addToCart = async (product: SearchResultType | GuessItemType) => {
goToLogin('/pages/mall/consumer/search') goToLogin('/pages/mall/consumer/search')
return return
} }
uni.showLoading({ title: '检查商品...' }) uni.showLoading({ title: '检查商品...' })
try { try {
// 统一转换为 UTSJSONObject 访问属性 // 统一转换为 UTSJSONObject 访问属性
const prodObj = JSON.parse(JSON.stringify(product)) as UTSJSONObject const prodObj = JSON.parse(JSON.stringify(product)) as UTSJSONObject
const productId = prodObj.getString('id') ?? '' const productId = prodObj.getString('id') ?? ''
const merchantId = prodObj.getString('merchant_id') ?? '' const merchantId = prodObj.getString('merchant_id') ?? ''
// 检查商品是否有SKU // 检查商品是否有 SKU
const skus = await supabaseService.getProductSkus(productId) const skus = await supabaseService.getProductSkus(productId)
uni.hideLoading() uni.hideLoading()
if (skus.length > 0) { if (skus.length > 0) {
// 有规格,提示并跳转到商品详情选择规格 // 有规格时跳商品详情选择规格
uni.showToast({ title: '请选择规格', icon: 'none' }) uni.showToast({ title: '请选择规格', icon: 'none' })
setTimeout(() => { setTimeout(() => {
uni.navigateTo({ uni.navigateTo({
url: '/pages/mall/consumer/product-detail?id=' + productId url: '/pages/mall/consumer/product-detail?id=' + productId
}) })
}, 500) }, 500)
} else { } else {
// 无规格直接加入购物车 // 无规格直接加入购物车
uni.showLoading({ title: '添加中...' }) uni.showLoading({ title: '添加中...' })
const success = await supabaseService.addToCart(productId, 1, '', merchantId) const success = await supabaseService.addToCart(productId, 1, '', merchantId)
uni.hideLoading() uni.hideLoading()
if (success) { if (success) {
uni.showToast({ title: '已添加到购物车', icon: 'success' }) uni.showToast({ title: '已添加到购物车', icon: 'success' })
} else { } else {
uni.showToast({ title: '添加失败,请先登录', icon: 'none' }) uni.showToast({ title: '添加失败,请先登录', icon: 'none' })
} }
} }
} catch (e) { } catch (e) {
console.error('添加到购物车异常', e) console.error('添加到购物车异常', e)
uni.hideLoading() uni.hideLoading()
uni.showToast({ title: '操作异常', icon: 'none' }) uni.showToast({ title: '操作异常', icon: 'none' })
} }
} }
@@ -1119,31 +1032,31 @@ const openCamera = () => {
count: 1, count: 1,
sourceType: ['camera'], sourceType: ['camera'],
success: (res) => { success: (res) => {
console.log('拍摄图片路径', (res.tempFilePaths as string[])[0]) console.log('拍摄图片路径:', (res.tempFilePaths as string[])[0])
uni.showToast({ title: '已启用相机', icon: 'none' }) uni.showToast({ title: '已启用相机', icon: 'none' })
}, },
fail: (err) => { fail: (err) => {
console.error('启用相机失败', err) console.error('鍚敤鐩告満澶辫触', err)
} }
}) })
} }
const goBack = () => { const goBack = () => {
if (showResults.value) { if (showResults.value) {
// 如果在搜索结果页,先返回搜索初始页 // 如果在搜索结果页,先返回搜索初始页
showResults.value = false showResults.value = false
searchKeyword.value = '' searchKeyword.value = ''
} else { } else {
// 如果在搜索初始页,则返回上一页 // 如果在搜索初始页,则返回上一页
const pages = getCurrentPages() const pages = getCurrentPages()
if (pages.length > 1) { if (pages.length > 1) {
uni.navigateBack() uni.navigateBack()
} else { } else {
// 如果只有一页(由于深链接或重定向),返回首页 // 如果只有一个页面,则回首页
uni.switchTab({ uni.switchTab({
url: '/pages/main/index' url: '/pages/main/index'
}) })
} }
} }
} }
</script> </script>
@@ -1158,7 +1071,7 @@ const goBack = () => {
min-height: 100%; min-height: 100%;
} }
/* 店铺搜索结果 */ /* 搴楅摵鎼滅储缁撴灉 */
.shop-results-section { .shop-results-section {
background-color: #fff; background-color: #fff;
margin-bottom: 10px; margin-bottom: 10px;
@@ -1486,7 +1399,7 @@ const goBack = () => {
margin-left: 6rpx; margin-left: 6rpx;
} }
/* 猜你需要 */ /* 鐚滀綘闇€瑕?*/
.guess-you-like { .guess-you-like {
margin-bottom: 24rpx; margin-bottom: 24rpx;
background-color: #ffffff; background-color: #ffffff;
@@ -1840,7 +1753,7 @@ const goBack = () => {
font-weight: bold; font-weight: bold;
} }
/* 错误状态 */ /* 閿欒鐘舵€?*/
.error-state { .error-state {
flex: 1; flex: 1;
display: flex; display: flex;
@@ -1872,7 +1785,7 @@ const goBack = () => {
color: #999; color: #999;
} }
/* 加载更多 */ /* 鍔犺浇鏇村 */
.loading-more { .loading-more {
padding: 20px 0; padding: 20px 0;
display: flex; display: flex;
@@ -1932,3 +1845,5 @@ const goBack = () => {
} }
</style> </style>

View File

@@ -12,39 +12,80 @@ import {
HomeServiceTaskType, HomeServiceTaskType,
HomeServiceTimelineItemType HomeServiceTimelineItemType
} from '@/types/home-service.uts' } from '@/types/home-service.uts'
import {
confirmServiceOrder,
createServiceOrder,
getServiceOrderDetail,
listConsumerServiceOrders,
rejectServiceOrderAcceptance
} from '@/services/serviceOrderService.uts'
import supa from '@/components/supadb/aksupainstance.uts'
import {
getServiceOrderStatusText,
type ServiceOrderStatus,
type ServiceOrderTimelineItemType,
type ServiceOrderType
} from '@/types/service-order.uts'
const SERVICE_CATALOG: Array<HomeServiceCatalogType> = [ function plainObject(source: any): any {
{ return JSON.parse(JSON.stringify(source)) as any
id: 'svc-001', }
name: '基础上门护理',
category: '日常照护', function readString(source: any, key: string): string {
price: 168, const value = plainObject(source)[key]
durationText: '约 2 小时', if (value == null) {
summary: '覆盖生命体征监测、基础照护、风险提醒。', return ''
tags: ['适老化', '护理员上门', '支持家属陪同'],
suitableFor: '行动不便、术后恢复、慢病随访老人'
},
{
id: 'svc-002',
name: '康复训练指导',
category: '康复支持',
price: 260,
durationText: '约 3 小时',
summary: '提供肢体训练、步态练习和居家康复建议。',
tags: ['康复师', '步骤清晰', '可连续预约'],
suitableFor: '卒中恢复、术后康复、失能半失能老人'
},
{
id: 'svc-003',
name: '慢病健康随访',
category: '健康管理',
price: 128,
durationText: '约 90 分钟',
summary: '完成血压血糖监测、用药核对与健康宣教。',
tags: ['随访', '慢病', '可生成记录'],
suitableFor: '高血压、糖尿病等长期管理老人'
} }
] return typeof value == 'string' ? value : String(value)
}
function readNumber(source: any, key: string): number {
const value = plainObject(source)[key]
if (typeof value == 'number') {
return value
}
const parsed = Number(value)
return isNaN(parsed) ? 0 : parsed
}
function readStringArray(source: any, key: string): Array<string> {
const value = plainObject(source)[key]
if (Array.isArray(value)) {
const result = [] as Array<string>
for (let i = 0; i < value.length; i++) {
result.push(typeof value[i] == 'string' ? value[i] : String(value[i]))
}
return result
}
if (typeof value == 'string' && value != '') {
try {
const parsed = JSON.parse(value) as any
if (Array.isArray(parsed)) {
const result = [] as Array<string>
for (let i = 0; i < parsed.length; i++) {
result.push(typeof parsed[i] == 'string' ? parsed[i] : String(parsed[i]))
}
return result
}
} catch (error) {
return [] as Array<string>
}
}
return [] as Array<string>
}
function parseCatalogItem(source: any): HomeServiceCatalogType {
return {
id: readString(source, 'id'),
name: readString(source, 'name'),
category: readString(source, 'category'),
price: readNumber(source, 'price'),
durationText: readString(source, 'duration_text'),
summary: readString(source, 'summary'),
tags: readStringArray(source, 'tags_json'),
suitableFor: readString(source, 'suitable_for')
}
}
function createTimeline(title1: string, title2: string, title3: string): Array<HomeServiceTimelineItemType> { function createTimeline(title1: string, title2: string, title3: string): Array<HomeServiceTimelineItemType> {
return [ return [
@@ -243,31 +284,6 @@ const ADMIN_PLANS: Array<HomeServicePlanType> = [
} }
] ]
const CONSUMER_ACCEPTANCE: Array<HomeServiceAcceptanceType> = [
{
caseId: 'case-001',
caseNo: 'HS202605130001',
elderName: '李奶奶',
serviceName: '基础上门护理',
acceptanceStatus: 'waiting',
acceptanceStatusText: '待验收',
rating: 5,
feedback: '护理员按时到达,服务过程清晰,老人状态稳定。',
tags: ['准时上门', '沟通清楚', '动作规范']
},
{
caseId: 'case-002',
caseNo: 'HS202605130002',
elderName: '张爷爷',
serviceName: '慢病健康随访',
acceptanceStatus: 'waiting',
acceptanceStatusText: '待验收',
rating: 5,
feedback: '已查看记录,等待家属最终确认。',
tags: ['记录完整', '态度耐心']
}
]
const ADMIN_RECTIFICATIONS: Array<HomeServiceRectificationType> = [ const ADMIN_RECTIFICATIONS: Array<HomeServiceRectificationType> = [
{ {
caseId: 'case-001', caseId: 'case-001',
@@ -355,13 +371,6 @@ function clonePlan(item: HomeServicePlanType): HomeServicePlanType {
} }
} }
function cloneAcceptance(item: HomeServiceAcceptanceType): HomeServiceAcceptanceType {
return {
...item,
tags: item.tags.slice(0)
}
}
function cloneRectification(item: HomeServiceRectificationType): HomeServiceRectificationType { function cloneRectification(item: HomeServiceRectificationType): HomeServiceRectificationType {
return { return {
...item ...item
@@ -375,68 +384,196 @@ function cloneSettlement(item: HomeServiceSettlementType): HomeServiceSettlement
} }
export async function fetchHomeServiceCatalog(): Promise<Array<HomeServiceCatalogType>> { export async function fetchHomeServiceCatalog(): Promise<Array<HomeServiceCatalogType>> {
await delay() const response = await supa
return SERVICE_CATALOG.map((item) => ({ ...item, tags: item.tags.slice(0) })) .from('hss_service_catalog')
.select('id, name, category, price, duration_text, summary, tags_json, suitable_for, sort_no')
.eq('status', 1)
.is('deleted_at', null)
.order('sort_no', { ascending: true })
.execute()
if (response.error != null || response.data == null || !Array.isArray(response.data)) {
return [] as Array<HomeServiceCatalogType>
}
const result = [] as Array<HomeServiceCatalogType>
for (let i = 0; i < response.data.length; i++) {
result.push(parseCatalogItem(response.data[i]))
}
return result
} }
export async function fetchConsumerHomeServiceCases(): Promise<Array<HomeServiceCaseType>> { export async function fetchConsumerHomeServiceCases(): Promise<Array<HomeServiceCaseType>> {
await delay() const orders = await listConsumerServiceOrders()
return CASE_STORE.map((item) => cloneCase(item)) const result = [] as Array<HomeServiceCaseType>
for (let i = 0; i < orders.length; i++) {
result.push(mapOrderToCase(orders[i]))
}
return result
} }
export async function fetchConsumerHomeServiceCaseDetail(caseId: string): Promise<HomeServiceCaseType | null> { export async function fetchConsumerHomeServiceCaseDetail(caseId: string): Promise<HomeServiceCaseType | null> {
await delay() const detail = await getServiceOrderDetail(caseId)
const target = CASE_STORE.find((item) => item.id == caseId) if (detail != null) {
return target == null ? null : cloneCase(target) return mapOrderToCase(detail)
}
return null
} }
export async function createHomeServiceApplication(draft: HomeServiceApplicationDraftType): Promise<HomeServiceCaseType> { export async function createHomeServiceApplication(draft: HomeServiceApplicationDraftType): Promise<HomeServiceCaseType | null> {
await delay() const catalog = await fetchHomeServiceCatalog()
const matchedService = SERVICE_CATALOG.find((item) => item.id == draft.serviceId) let matchedService: HomeServiceCatalogType | null = null
const created: HomeServiceCaseType = { for (let i = 0; i < catalog.length; i++) {
id: 'case-' + String(CASE_STORE.length + 1).padStart(3, '0'), if (catalog[i].id == draft.serviceId) {
caseNo: 'HS20260513' + String(CASE_STORE.length + 1).padStart(4, '0'), matchedService = catalog[i]
status: 'submitted', break
statusText: '已提交', }
statusTone: 'primary', }
serviceName: draft.serviceName, if (matchedService != null && draft.serviceAddressSnapshot != null) {
serviceTime: draft.preferredTime, const createdOrder = await createServiceOrder({
applicantName: draft.applicantName, service: matchedService,
elderName: draft.elderName, address: {
age: draft.age, addressId: draft.serviceAddressSnapshot.addressId,
phone: draft.phone, contactName: draft.serviceAddressSnapshot.contactName,
address: draft.address, contactPhone: draft.serviceAddressSnapshot.contactPhone,
summary: draft.demandSummary, province: '',
currentStep: 1, city: '',
totalSteps: 8, district: '',
staffName: '待分配', detailAddress: draft.serviceAddressSnapshot.addressDetail,
staffPhone: '待分配', fullAddress: draft.serviceAddressSnapshot.fullAddress,
amount: matchedService != null ? matchedService.price : 0, latitude: draft.serviceAddressSnapshot.latitude,
timeline: [ longitude: draft.serviceAddressSnapshot.longitude,
{ coordinateType: draft.serviceAddressSnapshot.coordinateType,
id: 'new-1', remark: draft.serviceAddressSnapshot.remark
title: '已提交申请', },
time: '2026-05-13 10:00', recipientName: draft.elderName,
description: '系统已接收申请,等待受理。' recipientPhone: draft.phone,
} contactName: draft.applicantName,
] contactPhone: draft.phone,
appointmentTime: draft.preferredTime,
remark: draft.demandSummary
})
if (createdOrder != null) {
return mapOrderToCase(createdOrder)
}
}
return null
}
function getCaseStep(status: ServiceOrderStatus): number {
if (status == 'created') return 1
if (status == 'assigned') return 3
if (status == 'accepted') return 4
if (status == 'departed') return 5
if (status == 'arrived' || status == 'in_service') return 6
if (status == 'pending_acceptance') return 7
if (status == 'accepted_by_user' || status == 'reviewed' || status == 'settled') return 8
if (status == 'rejected' || status == 'cancelled' || status == 'exception') return 7
return 2
}
function getCaseTone(status: ServiceOrderStatus): string {
if (status == 'created' || status == 'assigned' || status == 'pending_acceptance') return 'warning'
if (status == 'accepted' || status == 'departed' || status == 'arrived' || status == 'in_service') return 'primary'
if (status == 'accepted_by_user' || status == 'reviewed' || status == 'settled') return 'success'
if (status == 'exception' || status == 'cancelled' || status == 'rejected') return 'danger'
return 'neutral'
}
function getTimelineDescription(log: ServiceOrderTimelineItemType): string {
if (log.remark != '') {
return log.remark
}
if (log.toStatus == 'created') return '平台已接收预约申请,等待后续处理。'
if (log.toStatus == 'assigned') return '系统已完成派单,正在通知服务人员。'
if (log.toStatus == 'accepted') return '服务人员已接单,正在准备上门。'
if (log.toStatus == 'departed') return '服务人员已出发,正在前往服务地点。'
if (log.toStatus == 'arrived') return '服务人员已到达服务地点。'
if (log.toStatus == 'in_service') return '服务已开始执行,请留意后续进度。'
if (log.toStatus == 'completed') return '服务执行已完成,正在整理结果。'
if (log.toStatus == 'pending_acceptance') return '服务记录已提交,等待家属验收。'
if (log.toStatus == 'accepted_by_user') return '家属已确认本次服务结果。'
if (log.toStatus == 'reviewed') return '家属已完成评价反馈。'
if (log.toStatus == 'settled') return '本次服务已完成结算归档。'
if (log.toStatus == 'cancelled') return '服务单已取消。'
if (log.toStatus == 'rejected') return '服务人员未接受该工单。'
return '服务过程状态已更新。'
}
function formatServiceAppointmentText(value: string): string {
if (value == '') {
return ''
}
if (value.indexOf('上午') >= 0 || value.indexOf('下午') >= 0 || value.indexOf('晚上') >= 0) {
return value
}
const parsed = Date.parse(value)
if (!isNaN(parsed)) {
const date = new Date(parsed)
let year = date.getFullYear()
const currentYear = new Date().getFullYear()
if (year < currentYear - 1) {
year = currentYear
}
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hour = String(date.getHours()).padStart(2, '0')
const minute = String(date.getMinutes()).padStart(2, '0')
return year + '-' + month + '-' + day + ' ' + hour + ':' + minute
}
const monthDayMatch = value.match(/(\d{2})\/(\d{2})/)
const timeMatch = value.match(/(\d{2}:\d{2}(\s*-\s*\d{2}:\d{2})?)/)
if (monthDayMatch != null) {
const month = monthDayMatch[1] ?? ''
const day = monthDayMatch[2] ?? ''
const timeText = timeMatch != null ? (timeMatch[1] ?? '') : ''
if (month != '' && day != '') {
return String(new Date().getFullYear()) + '-' + month + '-' + day + (timeText != '' ? ' ' + timeText.replace(/\s+/g, '') : '')
}
}
return value.replace('T', ' ')
}
function mapLogsToTimeline(logs: Array<ServiceOrderTimelineItemType>): Array<HomeServiceTimelineItemType> {
const timeline = [] as Array<HomeServiceTimelineItemType>
for (let i = 0; i < logs.length; i++) {
timeline.push({
id: logs[i].id,
title: getServiceOrderStatusText(logs[i].toStatus),
time: logs[i].createdAt,
description: getTimelineDescription(logs[i])
})
}
return timeline
}
function mapOrderToCase(order: ServiceOrderType): HomeServiceCaseType {
return {
id: order.id,
caseNo: order.orderNo,
status: order.status,
statusText: getServiceOrderStatusText(order.status),
statusTone: getCaseTone(order.status),
serviceName: order.serviceName,
serviceTime: formatServiceAppointmentText(order.appointmentTime),
applicantName: order.contactName,
elderName: order.recipientName,
age: 0,
phone: order.contactPhone,
address: order.addressSnapshot.fullAddress,
summary: order.remark,
currentStep: getCaseStep(order.status),
totalSteps: 8,
staffName: order.staffName == '' ? '待分配' : order.staffName,
staffPhone: order.staffPhone == '' ? '待分配' : order.staffPhone,
amount: order.serviceSnapshot.price,
checkinTime: order.executionRecord != null ? order.executionRecord.checkinTime : '',
checkinAddress: order.executionRecord != null ? order.executionRecord.checkinAddress : '',
serviceStartedAt: order.executionRecord != null ? order.executionRecord.serviceStartedAt : order.serviceStartedAt,
serviceFinishedAt: order.executionRecord != null ? order.executionRecord.serviceFinishedAt : order.completedAt,
executionSummary: order.executionRecord != null ? (order.executionRecord.summary != '' ? order.executionRecord.summary : order.executionRecord.remark) : '',
evidenceCount: order.evidenceFiles.length,
serviceAddressSnapshot: null,
timeline: mapLogsToTimeline(order.logs)
} }
CASE_STORE.unshift(created)
ADMIN_APPLICATIONS.unshift({
id: 'admin-app-' + String(ADMIN_APPLICATIONS.length + 1).padStart(3, '0'),
caseId: created.id,
caseNo: created.caseNo,
status: 'submitted',
statusText: '已提交',
statusTone: 'primary',
elderName: created.elderName,
serviceName: created.serviceName,
preferredTime: created.serviceTime,
assessmentResult: '待受理',
dispatcherName: '待分配',
staffName: '待分配'
})
return cloneCase(created)
} }
export async function fetchWorkerTasks(): Promise<Array<HomeServiceTaskType>> { export async function fetchWorkerTasks(): Promise<Array<HomeServiceTaskType>> {
@@ -738,9 +875,21 @@ export async function submitAdminServicePlan(
} }
export async function fetchConsumerAcceptanceDetail(caseId: string): Promise<HomeServiceAcceptanceType | null> { export async function fetchConsumerAcceptanceDetail(caseId: string): Promise<HomeServiceAcceptanceType | null> {
await delay() const detail = await getServiceOrderDetail(caseId)
const target = CONSUMER_ACCEPTANCE.find((item) => item.caseId == caseId) if (detail != null) {
return target == null ? null : cloneAcceptance(target) return {
caseId: detail.id,
caseNo: detail.orderNo,
elderName: detail.recipientName,
serviceName: detail.serviceName,
acceptanceStatus: detail.status,
acceptanceStatusText: getServiceOrderStatusText(detail.status),
rating: detail.review != null ? detail.review.rating : 5,
feedback: detail.review != null ? detail.review.content : '',
tags: detail.review != null ? detail.review.tags : [] as Array<string>
}
}
return null
} }
export async function submitConsumerAcceptance( export async function submitConsumerAcceptance(
@@ -750,40 +899,18 @@ export async function submitConsumerAcceptance(
feedback: string, feedback: string,
tags: Array<string> tags: Array<string>
): Promise<HomeServiceAcceptanceType | null> { ): Promise<HomeServiceAcceptanceType | null> {
await delay() if (approved) {
const target = CONSUMER_ACCEPTANCE.find((item) => item.caseId == caseId) const result = await confirmServiceOrder(caseId, rating, feedback, tags)
if (target == null) { if (result != null) {
return await fetchConsumerAcceptanceDetail(caseId)
}
return null return null
} }
const rejected = await rejectServiceOrderAcceptance(caseId, feedback)
target.acceptanceStatus = approved ? 'approved' : 'rejected' if (rejected != null) {
target.acceptanceStatusText = approved ? '已验收' : '已退回' return await fetchConsumerAcceptanceDetail(caseId)
target.rating = rating
target.feedback = feedback
target.tags = tags.slice(0)
const relatedCase = CASE_STORE.find((item) => item.id == caseId)
if (relatedCase != null) {
relatedCase.status = approved ? 'completed' : 'revision_required'
relatedCase.statusText = approved ? '已完成' : '待整改'
relatedCase.statusTone = approved ? 'success' : 'warning'
relatedCase.currentStep = approved ? 8 : 7
relatedCase.timeline.unshift({
id: 'case-accept-' + String(relatedCase.timeline.length + 1),
title: approved ? '家属已验收' : '家属退回整改',
time: '2026-05-13 17:10',
description: feedback
})
} }
return null
const relatedAdmin = ADMIN_APPLICATIONS.find((item) => item.caseId == caseId)
if (relatedAdmin != null) {
relatedAdmin.status = approved ? 'completed' : 'revision_required'
relatedAdmin.statusText = approved ? '已完成' : '待整改'
relatedAdmin.statusTone = approved ? 'success' : 'warning'
}
return cloneAcceptance(target)
} }
export async function fetchAdminRectificationDetail(caseId: string): Promise<HomeServiceRectificationType | null> { export async function fetchAdminRectificationDetail(caseId: string): Promise<HomeServiceRectificationType | null> {

View File

@@ -1,124 +1,135 @@
import supa from '@/components/supadb/aksupainstance.uts' import supa from '@/components/supadb/aksupainstance.uts'
import { getCurrentUserId } from '@/utils/store.uts' import { getCurrentUserId } from '@/utils/store.uts'
import { getDeliveryProfileByUserId } from '@/api/delivery.uts' import { getCurrentUser } from '@/utils/store.uts'
import { getServiceOrderStatusText, normalizeServiceOrderStatus, type ServiceOrderStatus } from '@/types/service-order.uts' import type { UserAddress } from '@/utils/supabaseService.uts'
import type { import type { HomeServiceCatalogType } from '@/types/home-service.uts'
DeliveryCheckinPayloadType, import {
DeliveryDashboardType, getServiceOrderStatusText,
DeliveryLocationType, normalizeServiceOrderStatus,
DeliveryOrderType, type ServiceOrderAddressSnapshotType,
DeliveryServiceRecordType type ServiceEvidenceFileType,
} from '@/types/delivery.uts' type ServiceExecutionRecordType,
type ServiceOrderStatus,
type ServiceOrderTimelineItemType,
type ServiceOrderType,
type ServiceReviewType
} from '@/types/service-order.uts'
function nowIso(): string { export type CreateServiceOrderParams = {
return new Date().toISOString() service: HomeServiceCatalogType
} address: ServiceOrderAddressSnapshotType
recipientName: string
recipientPhone: string
contactName: string
contactPhone: string
appointmentTime: string
remark: string
}
function nowText(): string {
return new Date().toISOString().replace('T', ' ').substring(0, 19)
}
function buildId(prefix: string): string { function buildId(prefix: string): string {
return prefix + '-' + String(Date.now()) + '-' + String(Math.floor(Math.random() * 100000)).padStart(5, '0') return prefix + '-' + String(Date.now()) + '-' + String(Math.floor(Math.random() * 100000)).padStart(5, '0')
} }
async function getCurrentStaffId(): Promise<string> { function buildOrderNo(): string {
const userId = getCurrentUserId() const date = new Date()
if (userId == '') { const y = String(date.getFullYear())
return '' const m = String(date.getMonth() + 1).padStart(2, '0')
} const d = String(date.getDate()).padStart(2, '0')
const profile = await getDeliveryProfileByUserId(userId) const h = String(date.getHours()).padStart(2, '0')
return profile != null ? profile.id : '' const i = String(date.getMinutes()).padStart(2, '0')
} const s = String(date.getSeconds()).padStart(2, '0')
return 'HS' + y + m + d + h + i + s + String(Math.floor(Math.random() * 900) + 100)
}
async function insertStatusLog(orderId: string, fromStatus: string, toStatus: ServiceOrderStatus, remark: string): Promise<void> { function isUuidLike(value: string): boolean {
const userId = getCurrentUserId() return /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/.test(value)
await supa.from('hss_service_order_status_logs').insert({ }
id: buildId('slog'),
order_id: orderId,
from_status: fromStatus,
to_status: toStatus,
operator_id: userId == '' ? null : userId,
operator_role: 'delivery',
remark,
created_at: nowIso()
}).execute()
}
function emptyOrder(): DeliveryOrderType { function normalizeUuidOrNull(value: string): string | null {
return { if (value == '') {
id: '', return null
orderNo: '',
serviceType: '',
serviceName: '',
serviceCategory: '',
serviceItems: [] as Array<any>,
elderId: '',
elderName: '',
elderNameMasked: '',
elderGender: '',
elderAge: 0,
elderPhone: '',
elderPhoneMasked: '',
fullElderName: '',
fullPhone: '',
contactRelation: '家属',
addressSummary: '',
address: '',
addressDetail: '',
fullAddress: '',
latitude: 0,
longitude: 0,
appointmentTime: '',
appointmentStartTime: '',
appointmentEndTime: '',
duration: 90,
estimatedDuration: 90,
price: 0,
staffIncome: 0,
distance: '',
actualStartTime: '',
actualEndTime: '',
status: 'pending_assignment' as any,
statusText: '',
statusTone: 'warning',
riskTags: [] as Array<string>,
healthTags: [] as Array<string>,
careLevel: '',
needFamilyPresent: false,
needMaterials: false,
remark: '',
merchantId: '',
merchantName: '',
deliveryStaffId: '',
deliveryStaffName: '',
acceptTime: '',
departTime: '',
arriveTime: '',
checkinTime: '',
finishTime: '',
cancelReason: '',
exceptionType: '',
exceptionDesc: '',
evidenceList: [] as Array<any>,
signatureUrl: '',
signatureName: '',
satisfactionStatus: '',
settlementStatus: '',
archiveStatus: '',
createdAt: '',
updatedAt: '',
contactName: '',
contactPhone: '',
notices: [] as Array<string>,
timeline: [] as Array<any>,
statusLog: [] as Array<any>,
serviceSummary: '',
progressNote: '',
distanceKm: '',
allowCheckinRadiusMeters: 100,
lastLocation: null,
trackPoints: [] as Array<any>,
serviceRecord: null,
abnormalReport: null
} as DeliveryOrderType
} }
return isUuidLike(value) ? value : null
}
function normalizeAppointmentTime(value: string): string | null {
const text = value.trim()
if (text == '') {
return null
}
if (/^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}(\s*-\s*\d{2}:\d{2})?$/.test(text)) {
return text
}
if (/^\d{4}-\d{2}-\d{2}(\s+(上午|下午|晚上))$/.test(text)) {
return text
}
if (/^\d{4}-\d{2}-\d{2}T/.test(text)) {
const parsed = Date.parse(text)
if (!isNaN(parsed)) {
const date = new Date(parsed)
const year = String(date.getFullYear())
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hour = String(date.getHours()).padStart(2, '0')
const minute = String(date.getMinutes()).padStart(2, '0')
return year + '-' + month + '-' + day + ' ' + hour + ':' + minute
}
return text.replace('T', ' ')
}
const monthDayMatch = text.match(/(\d{2})\/(\d{2})/)
if (monthDayMatch != null) {
const month = monthDayMatch[1] ?? ''
const day = monthDayMatch[2] ?? ''
if (month == '' || day == '') {
return text
}
const year = String(new Date().getFullYear())
const rangeMatch = text.match(/(\d{2}:\d{2})(\s*-\s*\d{2}:\d{2})?/)
if (rangeMatch != null) {
const startTime = rangeMatch[1] ?? ''
const endRange = rangeMatch[2] ?? ''
if (startTime != '') {
return year + '-' + month + '-' + day + ' ' + startTime + endRange.replace(/\s+/g, '')
}
}
const tailText = text.substring(text.indexOf(month + '/' + day) + 5).trim()
return year + '-' + month + '-' + day + (tailText != '' ? ' ' + tailText : '')
}
const explicitParsed = Date.parse(text)
if (!isNaN(explicitParsed) && /^\d{4}\//.test(text)) {
const date = new Date(explicitParsed)
const year = String(date.getFullYear())
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hour = String(date.getHours()).padStart(2, '0')
const minute = String(date.getMinutes()).padStart(2, '0')
return year + '-' + month + '-' + day + ' ' + hour + ':' + minute
}
const year = String(new Date().getFullYear())
return /^\d{2}-\d{2}\s+\d{2}:\d{2}/.test(text) ? year + '-' + text : text
}
function safeParseObject(value: string): UTSJSONObject {
if (value == '') {
return JSON.parse('{}') as UTSJSONObject
}
return JSON.parse(value) as UTSJSONObject
}
function safeParseArray(value: string): Array<string> {
if (value == '') {
return [] as Array<string>
}
const parsed = JSON.parse(value) as any
if (Array.isArray(parsed)) {
return parsed as Array<string>
}
return [] as Array<string>
}
function safeJsonField(source: any, key: string): string { function safeJsonField(source: any, key: string): string {
const plain = JSON.parse(JSON.stringify(source)) as any const plain = JSON.parse(JSON.stringify(source)) as any
@@ -127,374 +138,387 @@ function safeJsonField(source: any, key: string): string {
return '' return ''
} }
return JSON.stringify(value) return JSON.stringify(value)
} }
function statusToDeliveryStatus(status: ServiceOrderStatus): string { function plainObject(source: any): any {
if (status == 'assigned') return 'pending_assignment' return JSON.parse(JSON.stringify(source)) as any
if (status == 'accepted') return 'accepted' }
if (status == 'departed') return 'departed'
if (status == 'arrived') return 'arrived'
if (status == 'in_service') return 'in_service'
if (status == 'pending_acceptance') return 'pending_acceptance'
if (status == 'reviewed' || status == 'accepted_by_user' || status == 'settled') return 'completed'
if (status == 'rejected') return 'rejected'
if (status == 'cancelled') return 'cancelled'
if (status == 'exception') return 'abnormal'
return 'pending_assignment'
}
function statusTone(status: ServiceOrderStatus): string { function readString(source: any, key: string): string {
if (status == 'pending_acceptance' || status == 'assigned') return 'warning' const value = plainObject(source)[key]
if (status == 'accepted' || status == 'departed' || status == 'arrived' || status == 'in_service') return 'primary' if (value == null) {
if (status == 'accepted_by_user' || status == 'reviewed' || status == 'settled') return 'success' return ''
if (status == 'rejected' || status == 'cancelled' || status == 'exception') return 'danger'
return 'warning'
} }
return typeof value == 'string' ? value : String(value)
}
async function parseDeliveryOrder(orderId: string, item: any): Promise<DeliveryOrderType> { function readNumber(source: any, key: string): number {
const order = emptyOrder() const value = plainObject(source)[key]
const obj = JSON.parse(JSON.stringify(item)) as UTSJSONObject if (typeof value == 'number') {
const addressRaw = safeJsonField(item, 'address_snapshot_json') return value
const serviceRaw = safeJsonField(item, 'service_snapshot_json')
const addressObj = JSON.parse(addressRaw == '' ? '{}' : addressRaw) as UTSJSONObject
const serviceObj = JSON.parse(serviceRaw == '' ? '{}' : serviceRaw) as UTSJSONObject
const normalizedStatus = normalizeServiceOrderStatus(obj.getString('status') ?? '')
order.id = obj.getString('id') ?? ''
order.orderNo = obj.getString('order_no') ?? ''
order.serviceType = serviceObj.getString('category') ?? '居家服务'
order.serviceName = obj.getString('service_name') ?? ''
order.serviceCategory = serviceObj.getString('category') ?? ''
order.elderName = obj.getString('recipient_name') ?? ''
order.elderNameMasked = order.elderName
order.fullElderName = order.elderName
order.elderPhone = obj.getString('recipient_phone') ?? ''
order.elderPhoneMasked = order.elderPhone
order.fullPhone = order.elderPhone
order.contactName = obj.getString('contact_name') ?? ''
order.contactPhone = obj.getString('contact_phone') ?? ''
order.contactRelation = '家属'
order.address = addressObj.getString('fullAddress') ?? ''
order.addressDetail = addressObj.getString('detailAddress') ?? ''
order.fullAddress = order.address
order.latitude = addressObj.getNumber('latitude') ?? 0
order.longitude = addressObj.getNumber('longitude') ?? 0
order.appointmentTime = obj.getString('appointment_time') ?? ''
order.appointmentStartTime = order.appointmentTime
order.appointmentEndTime = order.appointmentTime
order.duration = 90
order.estimatedDuration = 90
order.price = serviceObj.getNumber('price') ?? 0
order.staffIncome = order.price
order.status = statusToDeliveryStatus(normalizedStatus) as any
order.statusText = getServiceOrderStatusText(normalizedStatus)
order.statusTone = statusTone(normalizedStatus)
order.remark = obj.getString('remark') ?? ''
order.deliveryStaffId = obj.getString('current_staff_id') ?? ''
order.acceptTime = obj.getString('accepted_at') ?? ''
order.departTime = obj.getString('departed_at') ?? ''
order.arriveTime = obj.getString('arrived_at') ?? ''
order.actualStartTime = obj.getString('service_started_at') ?? ''
order.startServiceTime = order.actualStartTime
order.finishTime = obj.getString('completed_at') ?? ''
order.createdAt = obj.getString('created_at') ?? ''
order.updatedAt = obj.getString('updated_at') ?? ''
order.allowCheckinRadiusMeters = 100
const logsResponse = await supa.from('hss_service_order_status_logs').select('*').eq('order_id', orderId).order('created_at', { ascending: false }).execute()
if (logsResponse.data != null) {
const rawLogs = logsResponse.data as any[]
for (let i = 0; i < rawLogs.length; i++) {
const logObj = JSON.parse(JSON.stringify(rawLogs[i])) as UTSJSONObject
order.timeline.push({
id: logObj.getString('id') ?? '',
title: getServiceOrderStatusText(normalizeServiceOrderStatus(logObj.getString('to_status') ?? 'created')),
time: logObj.getString('created_at') ?? '',
description: logObj.getString('remark') ?? ''
})
}
} }
const recordResponse = await supa.from('hss_service_execution_records').select('*').eq('order_id', orderId).order('created_at', { ascending: false }).execute() if (typeof value == 'string' && value != '') {
if (recordResponse.data != null) { const parsed = Number(value)
const raw = recordResponse.data as any[] return isNaN(parsed) ? 0 : parsed
if (raw.length > 0) {
const recordObj = JSON.parse(JSON.stringify(raw[0])) as UTSJSONObject
order.checkinTime = recordObj.getString('checkin_time') ?? ''
order.serviceRecord = {
id: recordObj.getString('id') ?? '',
orderId: orderId,
startTime: recordObj.getString('service_started_at') ?? '',
endTime: recordObj.getString('service_finished_at') ?? '',
actualDurationMinutes: recordObj.getNumber('actual_duration_minutes') ?? 0,
serviceItems: [] as Array<any>,
serviceContent: [] as Array<string>,
processNote: recordObj.getString('summary') ?? '',
elderStatus: '',
healthMetrics: { bloodPressure: '', heartRate: '', bloodSugar: '', bloodOxygen: '' },
materialsUsed: '',
abnormalNote: '',
photos: [] as Array<string>,
staffRemark: recordObj.getString('remark') ?? '',
familyConfirmation: { method: 'none', code: '', signatureName: '', signatureUrl: '', confirmedAt: '' },
createdAt: recordObj.getString('created_at') ?? '',
updatedAt: recordObj.getString('updated_at') ?? ''
} as any
}
}
const evidenceResponse = await supa.from('hss_service_evidence_files').select('*').eq('order_id', orderId).order('created_at', { ascending: false }).execute()
if (evidenceResponse.data != null) {
const rawEvidence = evidenceResponse.data as any[]
for (let i = 0; i < rawEvidence.length; i++) {
const evidenceObj = JSON.parse(JSON.stringify(rawEvidence[i])) as UTSJSONObject
order.evidenceList.push({
id: evidenceObj.getString('id') ?? '',
orderId: orderId,
phase: evidenceObj.getString('phase') ?? '',
fileType: evidenceObj.getString('file_type') ?? 'image',
name: evidenceObj.getString('storage_path') ?? '',
url: evidenceObj.getString('file_url') ?? '',
localPath: '',
status: 'success',
progress: 100,
retryable: false,
createdAt: evidenceObj.getString('created_at') ?? ''
})
}
}
return order
} }
return 0
}
export async function getDashboard(): Promise<DeliveryDashboardType> { function parseTimeline(item: any): ServiceOrderTimelineItemType {
const orders = await getOrdersByTab('all')
let pending = 0
let today = 0
let serving = 0
let completed = 0
let nextOrder: DeliveryOrderType | null = null
for (let i = 0; i < orders.length; i++) {
const item = orders[i]
if (item.status == 'pending_assignment') pending++
if (item.status == 'pending_assignment' || item.status == 'accepted' || item.status == 'departed' || item.status == 'arrived' || item.status == 'in_service') today++
if (item.status == 'in_service') serving++
if (item.status == 'completed' || item.status == 'pending_acceptance') completed++
if (nextOrder == null && item.status != 'completed' && item.status != 'cancelled' && item.status != 'abnormal') {
nextOrder = item
}
}
return { return {
pendingAssignmentCount: pending, id: readString(item, 'id'),
pendingAcceptCount: pending, orderId: readString(item, 'order_id'),
todayOrderCount: today, fromStatus: readString(item, 'from_status'),
pendingDepartCount: 0, toStatus: normalizeServiceOrderStatus(readString(item, 'to_status')),
servingCount: serving, operatorId: readString(item, 'operator_id'),
completedCount: completed, operatorRole: readString(item, 'operator_role'),
exceptionCount: 0, remark: readString(item, 'remark'),
expectedIncome: 0, createdAt: readString(item, 'created_at')
onlineStatus: 'online', }
nextOrder,
recentOrders: orders.slice(0, 5)
} as DeliveryDashboardType
} }
export async function getOrdersByTab(tab: string): Promise<Array<DeliveryOrderType>> { function parseReview(item: any): ServiceReviewType {
const staffId = await getCurrentStaffId() return {
if (staffId == '') { id: readString(item, 'id'),
return [] as Array<DeliveryOrderType> orderId: readString(item, 'order_id'),
userId: readString(item, 'user_id'),
rating: readNumber(item, 'rating') == 0 ? 5 : readNumber(item, 'rating'),
tags: safeParseArray(safeJsonField(item, 'tags_json')),
content: readString(item, 'content'),
createdAt: readString(item, 'created_at')
} }
const response = await supa.from('hss_service_orders').select('*').eq('current_staff_id', staffId).order('created_at', { ascending: false }).execute()
if (response.error != null || response.data == null) {
return [] as Array<DeliveryOrderType>
} }
const rawOrders = response.data as any[]
const result = [] as Array<DeliveryOrderType> function parseExecutionRecord(item: any): ServiceExecutionRecordType {
for (let i = 0; i < rawOrders.length; i++) { return {
const orderObj = JSON.parse(JSON.stringify(rawOrders[i])) as UTSJSONObject id: readString(item, 'id'),
const normalized = normalizeServiceOrderStatus(orderObj.getString('status') ?? '') orderId: readString(item, 'order_id'),
let matched = true assignmentId: readString(item, 'assignment_id'),
if (tab == 'pending') { checkinTime: readString(item, 'checkin_time'),
matched = normalized == 'assigned' checkinLatitude: readNumber(item, 'checkin_latitude'),
} else if (tab == 'today') { checkinLongitude: readNumber(item, 'checkin_longitude'),
matched = normalized == 'assigned' || normalized == 'accepted' || normalized == 'departed' || normalized == 'arrived' || normalized == 'in_service' || normalized == 'pending_acceptance' checkinAddress: readString(item, 'checkin_address'),
} else if (tab == 'history') { serviceStartedAt: readString(item, 'service_started_at'),
matched = normalized == 'pending_acceptance' || normalized == 'accepted_by_user' || normalized == 'reviewed' || normalized == 'settled' || normalized == 'exception' || normalized == 'cancelled' serviceFinishedAt: readString(item, 'service_finished_at'),
actualDurationMinutes: readNumber(item, 'actual_duration_minutes'),
serviceItemsJson: safeJsonField(item, 'service_items_json'),
summary: readString(item, 'summary'),
remark: readString(item, 'remark'),
trackPointsJson: safeJsonField(item, 'track_points_json'),
createdAt: readString(item, 'created_at'),
updatedAt: readString(item, 'updated_at')
}
}
function parseEvidenceFile(item: any): ServiceEvidenceFileType {
return {
id: readString(item, 'id'),
orderId: readString(item, 'order_id'),
executionRecordId: readString(item, 'execution_record_id'),
phase: readString(item, 'phase'),
fileType: readString(item, 'file_type'),
storagePath: readString(item, 'storage_path'),
fileUrl: readString(item, 'file_url'),
latitude: readNumber(item, 'latitude'),
longitude: readNumber(item, 'longitude'),
capturedAt: readString(item, 'captured_at'),
createdAt: readString(item, 'created_at')
}
}
function parseServiceOrder(item: any, logs: Array<ServiceOrderTimelineItemType>, review: ServiceReviewType | null): ServiceOrderType {
const addressSnapshot = safeJsonField(item, 'address_snapshot_json')
const serviceSnapshot = safeJsonField(item, 'service_snapshot_json')
const addressObj = plainObject(safeParseObject(addressSnapshot))
const serviceObj = plainObject(safeParseObject(serviceSnapshot))
return {
id: readString(item, 'id'),
orderNo: readString(item, 'order_no'),
userId: readString(item, 'user_id'),
serviceId: readString(item, 'service_id'),
serviceName: readString(item, 'service_name'),
serviceSnapshot: {
serviceId: readString(serviceObj, 'serviceId') != '' ? readString(serviceObj, 'serviceId') : readString(item, 'service_id'),
serviceName: readString(serviceObj, 'serviceName') != '' ? readString(serviceObj, 'serviceName') : readString(item, 'service_name'),
category: readString(serviceObj, 'category'),
price: readNumber(serviceObj, 'price'),
durationText: readString(serviceObj, 'durationText'),
summary: readString(serviceObj, 'summary'),
tags: safeParseArray(safeJsonField(serviceObj, 'tags')),
suitableFor: readString(serviceObj, 'suitableFor')
},
serviceAddressId: readString(item, 'service_address_id'),
addressSnapshot: {
addressId: readString(addressObj, 'addressId'),
contactName: readString(addressObj, 'contactName'),
contactPhone: readString(addressObj, 'contactPhone'),
province: readString(addressObj, 'province'),
city: readString(addressObj, 'city'),
district: readString(addressObj, 'district'),
detailAddress: readString(addressObj, 'detailAddress'),
fullAddress: readString(addressObj, 'fullAddress'),
latitude: readNumber(addressObj, 'latitude'),
longitude: readNumber(addressObj, 'longitude'),
coordinateType: readString(addressObj, 'coordinateType') == '' ? 'gcj02' : readString(addressObj, 'coordinateType'),
remark: readString(addressObj, 'remark')
},
recipientName: readString(item, 'recipient_name'),
recipientPhone: readString(item, 'recipient_phone'),
contactName: readString(item, 'contact_name'),
contactPhone: readString(item, 'contact_phone'),
appointmentTime: readString(item, 'appointment_time'),
remark: readString(item, 'remark'),
status: normalizeServiceOrderStatus(readString(item, 'status')),
currentAssignmentId: readString(item, 'current_assignment_id'),
currentStaffId: readString(item, 'current_staff_id'),
acceptedAt: readString(item, 'accepted_at'),
departedAt: readString(item, 'departed_at'),
arrivedAt: readString(item, 'arrived_at'),
serviceStartedAt: readString(item, 'service_started_at'),
completedAt: readString(item, 'completed_at'),
pendingAcceptanceAt: readString(item, 'pending_acceptance_at'),
acceptedByUserAt: readString(item, 'accepted_by_user_at'),
reviewedAt: readString(item, 'reviewed_at'),
createdAt: readString(item, 'created_at'),
updatedAt: readString(item, 'updated_at'),
staffName: '',
staffPhone: '',
logs,
executionRecord: null,
evidenceFiles: [] as Array<any>,
review
}
}
async function insertStatusLog(orderId: string, fromStatus: string, toStatus: ServiceOrderStatus, operatorId: string, operatorRole: string, remark: string): Promise<void> {
await supa.from('hss_service_order_status_logs').insert({
id: buildId('slog'),
order_id: orderId,
from_status: fromStatus,
to_status: toStatus,
operator_id: operatorId == '' ? null : operatorId,
operator_role: operatorRole,
remark,
created_at: new Date().toISOString()
}).execute()
}
export function buildAddressSnapshot(address: UserAddress, latitude: number, longitude: number): ServiceOrderAddressSnapshotType {
return {
addressId: address.id,
contactName: address.recipient_name,
contactPhone: address.phone,
province: address.province,
city: address.city,
district: address.district,
detailAddress: address.detail_address,
fullAddress: address.province + address.city + address.district + ' ' + address.detail_address,
latitude,
longitude,
coordinateType: 'gcj02',
remark: address.label ?? ''
}
}
export async function createServiceOrder(params: CreateServiceOrderParams): Promise<ServiceOrderType | null> {
const userId = getCurrentUserId()
if (userId == '') {
return null
}
const orderId = buildId('so')
const orderNo = buildOrderNo()
const now = new Date().toISOString()
const appointmentTime = normalizeAppointmentTime(params.appointmentTime)
const response = await supa.from('hss_service_orders').insert({
id: orderId,
order_no: orderNo,
user_id: userId,
service_id: params.service.id,
service_name: params.service.name,
service_snapshot_json: params.service as any,
service_address_id: normalizeUuidOrNull(params.address.addressId),
address_snapshot_json: params.address as any,
recipient_name: params.recipientName,
recipient_phone: params.recipientPhone,
contact_name: params.contactName,
contact_phone: params.contactPhone,
appointment_time: appointmentTime,
remark: params.remark,
status: 'created',
created_at: now,
updated_at: now
}).execute()
if (response.error != null) {
console.error('createServiceOrder failed', response.error)
return null
}
await insertStatusLog(orderId, '', 'created', userId, 'consumer', '创建服务订单')
const staffResponse = await supa
.from('ml_delivery_staff')
.select('id, station_id, nickname, phone, status, deleted_at')
.eq('status', 1)
.order('created_at', { ascending: true })
.execute()
if (staffResponse.data != null) {
const rawStaffList = staffResponse.data as any[]
if (rawStaffList.length > 0) {
const staffObj = plainObject(rawStaffList[0])
const assignmentId = buildId('sa')
await supa.from('hss_service_assignments').insert({
id: assignmentId,
order_id: orderId,
staff_id: readString(staffObj, 'id'),
station_id: readString(staffObj, 'station_id') == '' ? null : readString(staffObj, 'station_id'),
status: 'assigned',
assigned_at: now,
created_at: now,
updated_at: now
}).execute()
await supa.from('hss_service_orders').update({
status: 'assigned',
current_assignment_id: assignmentId,
current_staff_id: readString(staffObj, 'id'),
updated_at: now
}).eq('id', orderId).execute()
await insertStatusLog(orderId, 'created', 'assigned', userId, 'system', '系统已自动派单')
} }
if (tab == 'all' || matched) { }
result.push(await parseDeliveryOrder(orderObj.getString('id') ?? '', rawOrders[i])) return await getServiceOrderDetail(orderId)
}
export async function listConsumerServiceOrders(): Promise<Array<ServiceOrderType>> {
const userId = getCurrentUserId()
if (userId == '') {
return [] as Array<ServiceOrderType>
}
const response = await supa.from('hss_service_orders').select('*').eq('user_id', userId).order('created_at', { ascending: false }).execute()
if (response.error != null || response.data == null) {
return [] as Array<ServiceOrderType>
}
const list = response.data as any[]
const result = [] as Array<ServiceOrderType>
for (let i = 0; i < list.length; i++) {
const parsed = await getServiceOrderDetail(JSON.parse(JSON.stringify(list[i]))['id'] as string)
if (parsed != null) {
result.push(parsed)
} }
} }
return result return result
} }
export async function getOrderDetail(orderId: string): Promise<DeliveryOrderType | null> { export async function getServiceOrderDetail(orderId: string): Promise<ServiceOrderType | null> {
const response = await supa.from('hss_service_orders').select('*').eq('id', orderId).single().execute() const orderResponse = await supa.from('hss_service_orders').select('*').eq('id', orderId).limit(1).execute()
if (response.error != null || response.data == null) { if (orderResponse.error != null || orderResponse.data == null) {
return null return null
} }
return await parseDeliveryOrder(orderId, response.data) const rawOrders = orderResponse.data as any[]
if (rawOrders.length == 0) {
return null
}
const logsResponse = await supa.from('hss_service_order_status_logs').select('*').eq('order_id', orderId).order('created_at', { ascending: false }).execute()
const reviewResponse = await supa.from('hss_service_reviews').select('*').eq('order_id', orderId).order('created_at', { ascending: false }).execute()
const executionResponse = await supa.from('hss_service_execution_records').select('*').eq('order_id', orderId).order('created_at', { ascending: false }).limit(1).execute()
const evidenceResponse = await supa.from('hss_service_evidence_files').select('*').eq('order_id', orderId).order('created_at', { ascending: false }).execute()
const logs = [] as Array<ServiceOrderTimelineItemType>
if (logsResponse.data != null) {
const rawLogs = logsResponse.data as any[]
for (let i = 0; i < rawLogs.length; i++) {
logs.push(parseTimeline(rawLogs[i]))
}
}
let review: ServiceReviewType | null = null
if (reviewResponse.data != null) {
const rawReviews = reviewResponse.data as any[]
if (rawReviews.length > 0) {
review = parseReview(rawReviews[0])
}
}
const parsed = parseServiceOrder(rawOrders[0], logs, review)
if (executionResponse.data != null) {
const rawExecutionList = executionResponse.data as any[]
if (rawExecutionList.length > 0) {
parsed.executionRecord = parseExecutionRecord(rawExecutionList[0])
}
}
if (evidenceResponse.data != null) {
const rawEvidenceList = evidenceResponse.data as any[]
const evidenceFiles = [] as Array<ServiceEvidenceFileType>
for (let i = 0; i < rawEvidenceList.length; i++) {
evidenceFiles.push(parseEvidenceFile(rawEvidenceList[i]))
}
parsed.evidenceFiles = evidenceFiles
}
if (parsed.currentStaffId != '') {
const staffResponse = await supa.from('ml_delivery_staff').select('nickname, phone').eq('id', parsed.currentStaffId).limit(1).execute()
if (staffResponse.data != null) {
const rawStaffList = staffResponse.data as any[]
if (rawStaffList.length > 0) {
const staffObj = plainObject(rawStaffList[0])
parsed.staffName = readString(staffObj, 'nickname')
parsed.staffPhone = readString(staffObj, 'phone')
}
}
}
return parsed
} }
async function updateOrderStatus(orderId: string, nextStatus: ServiceOrderStatus, updateData: UTSJSONObject, remark: string): Promise<DeliveryOrderType | null> { export async function confirmServiceOrder(orderId: string, rating: number, content: string, tags: Array<string>): Promise<ServiceOrderType | null> {
const current = await getOrderDetail(orderId) const userId = getCurrentUserId()
if (userId == '') {
return null
}
const current = await getServiceOrderDetail(orderId)
if (current == null) { if (current == null) {
return null return null
} }
const map = JSON.parse('{}') as UTSJSONObject const acceptedAt = new Date().toISOString()
map.set('status', nextStatus) const updateResponse = await supa.from('hss_service_orders').update({
map.set('updated_at', nowIso()) status: 'accepted_by_user',
const iterator = updateData.keys() accepted_by_user_at: acceptedAt,
while (iterator.hasNext()) { updated_at: acceptedAt
const key = iterator.next() }).eq('id', orderId).execute()
map.set(key, updateData.get(key)) if (updateResponse.error != null) {
}
const response = await supa.from('hss_service_orders').update(map).eq('id', orderId).execute()
if (response.error != null) {
console.error('updateOrderStatus failed', response.error)
return null return null
} }
await insertStatusLog(orderId, normalizeServiceOrderStatus(current.status as string), nextStatus, remark) await insertStatusLog(orderId, current.status, 'accepted_by_user', userId, 'consumer', '用户确认验收')
return await getOrderDetail(orderId) await supa.from('hss_service_reviews').insert({
} id: buildId('srv'),
export async function acceptOrder(orderId: string): Promise<DeliveryOrderType | null> {
const assignmentResponse = await supa.from('hss_service_assignments').select('*').eq('order_id', orderId).order('created_at', { ascending: false }).execute()
if (assignmentResponse.data != null) {
const raw = assignmentResponse.data as any[]
if (raw.length > 0) {
const assignmentId = JSON.parse(JSON.stringify(raw[0]))['id'] as string
await supa.from('hss_service_assignments').update({ status: 'accepted', accepted_at: nowIso(), updated_at: nowIso() }).eq('id', assignmentId).execute()
}
}
const updateData = JSON.parse('{}') as UTSJSONObject
updateData.set('accepted_at', nowIso())
return await updateOrderStatus(orderId, 'accepted', updateData, '服务人员接单')
}
export async function departOrder(orderId: string, location: DeliveryLocationType | null): Promise<DeliveryOrderType | null> {
const updateData = JSON.parse('{}') as UTSJSONObject
updateData.set('departed_at', nowIso())
return await updateOrderStatus(orderId, 'departed', updateData, location == null ? '服务人员出发' : '服务人员出发:' + location.address)
}
export async function arriveOrder(orderId: string, location: DeliveryLocationType | null): Promise<DeliveryOrderType | null> {
const updateData = JSON.parse('{}') as UTSJSONObject
updateData.set('arrived_at', nowIso())
return await updateOrderStatus(orderId, 'arrived', updateData, location == null ? '服务人员到达' : '服务人员到达:' + location.address)
}
export async function checkinOrder(orderId: string, payload: DeliveryCheckinPayloadType): Promise<DeliveryOrderType | null> {
const current = await getOrderDetail(orderId)
if (current == null) {
return null
}
let assignmentId = ''
const assignmentResponse = await supa.from('hss_service_assignments').select('*').eq('order_id', orderId).order('created_at', { ascending: false }).execute()
if (assignmentResponse.data != null) {
const rawAssignments = assignmentResponse.data as any[]
if (rawAssignments.length > 0) {
assignmentId = JSON.parse(JSON.stringify(rawAssignments[0]))['id'] as string
}
}
let recordId = buildId('ser')
const recordResponse = await supa.from('hss_service_execution_records').select('*').eq('order_id', orderId).order('created_at', { ascending: false }).execute()
if (recordResponse.data != null) {
const records = recordResponse.data as any[]
if (records.length > 0) {
recordId = JSON.parse(JSON.stringify(records[0]))['id'] as string
await supa.from('hss_service_execution_records').update({
checkin_time: nowIso(),
checkin_latitude: payload.location.latitude,
checkin_longitude: payload.location.longitude,
checkin_address: payload.location.address,
remark: payload.note,
updated_at: nowIso()
}).eq('id', recordId).execute()
} else {
await supa.from('hss_service_execution_records').insert({
id: recordId,
order_id: orderId,
assignment_id: assignmentId,
checkin_time: nowIso(),
checkin_latitude: payload.location.latitude,
checkin_longitude: payload.location.longitude,
checkin_address: payload.location.address,
remark: payload.note,
created_at: nowIso(),
updated_at: nowIso()
}).execute()
}
}
for (let i = 0; i < payload.photos.length; i++) {
await supa.from('hss_service_evidence_files').insert({
id: buildId('sef'),
order_id: orderId,
execution_record_id: recordId,
phase: 'checkin',
file_type: 'image',
storage_path: payload.photos[i],
file_url: payload.photos[i],
latitude: payload.location.latitude,
longitude: payload.location.longitude,
captured_at: nowIso(),
created_at: nowIso()
}).execute()
}
return current
}
export async function startService(orderId: string): Promise<DeliveryOrderType | null> {
let recordId = ''
const recordResponse = await supa.from('hss_service_execution_records').select('*').eq('order_id', orderId).order('created_at', { ascending: false }).execute()
if (recordResponse.data != null) {
const records = recordResponse.data as any[]
if (records.length > 0) {
recordId = JSON.parse(JSON.stringify(records[0]))['id'] as string
await supa.from('hss_service_execution_records').update({ service_started_at: nowIso(), updated_at: nowIso() }).eq('id', recordId).execute()
}
}
const updateData = JSON.parse('{}') as UTSJSONObject
updateData.set('service_started_at', nowIso())
return await updateOrderStatus(orderId, 'in_service', updateData, '开始服务')
}
export async function saveServiceRecord(orderId: string, record: DeliveryServiceRecordType): Promise<DeliveryOrderType | null> {
let assignmentId = ''
const assignmentResponse = await supa.from('hss_service_assignments').select('*').eq('order_id', orderId).order('created_at', { ascending: false }).execute()
if (assignmentResponse.data != null) {
const assignments = assignmentResponse.data as any[]
if (assignments.length > 0) {
assignmentId = JSON.parse(JSON.stringify(assignments[0]))['id'] as string
}
}
await supa.from('hss_service_execution_records').upsert({
id: record.id,
order_id: orderId, order_id: orderId,
assignment_id: assignmentId, user_id: userId,
service_started_at: record.startTime, rating,
service_finished_at: record.endTime, tags_json: tags as any,
actual_duration_minutes: record.actualDurationMinutes, content,
service_items_json: record.serviceItems as any, created_at: acceptedAt
summary: record.processNote,
remark: record.staffRemark,
updated_at: nowIso(),
created_at: record.createdAt
}).execute() }).execute()
for (let i = 0; i < record.photos.length; i++) { await supa.from('hss_service_orders').update({
await supa.from('hss_service_evidence_files').insert({ status: 'reviewed',
id: buildId('sef'), reviewed_at: acceptedAt,
order_id: orderId, updated_at: acceptedAt
execution_record_id: record.id, }).eq('id', orderId).execute()
phase: 'service', await insertStatusLog(orderId, 'accepted_by_user', 'reviewed', userId, 'consumer', '用户提交评价')
file_type: 'image', return await getServiceOrderDetail(orderId)
storage_path: record.photos[i],
file_url: record.photos[i],
captured_at: nowIso(),
created_at: nowIso()
}).execute()
}
return await getOrderDetail(orderId)
} }
export async function finishOrder(orderId: string): Promise<DeliveryOrderType | null> { export async function rejectServiceOrderAcceptance(orderId: string, content: string): Promise<ServiceOrderType | null> {
const updateData = JSON.parse('{}') as UTSJSONObject const userId = getCurrentUserId()
updateData.set('completed_at', nowIso()) if (userId == '') {
updateData.set('pending_acceptance_at', nowIso()) return null
return await updateOrderStatus(orderId, 'pending_acceptance', updateData, '服务完成,等待用户验收') }
const current = await getServiceOrderDetail(orderId)
if (current == null) {
return null
}
const updateResponse = await supa.from('hss_service_orders').update({
status: 'exception',
updated_at: nowIso()
}).eq('id', orderId).execute()
if (updateResponse.error != null) {
return null
}
await insertStatusLog(orderId, current.status, 'exception', userId, 'consumer', content == '' ? '用户退回整改' : content)
return await getServiceOrderDetail(orderId)
}
export async function getCurrentConsumerUser() {
return await getCurrentUser()
} }

View File

@@ -16,6 +16,28 @@ export type HomeServiceTimelineItemType = {
description: string description: string
} }
export type HomeServiceSelectedAddressType = {
addressId: string
userId: string
isDefault: boolean
contactName: string
contactPhone: string
phone?: string
addressName: string
locationName?: string
addressDetail: string
locationAddress?: string
houseNumber: string
doorNo?: string
fullAddress: string
latitude: number
longitude: number
remark: string
coordinateType: string
createdAt: number
updatedAt: number
}
export type HomeServiceCaseType = { export type HomeServiceCaseType = {
id: string id: string
caseNo: string caseNo: string
@@ -35,6 +57,13 @@ export type HomeServiceCaseType = {
staffName: string staffName: string
staffPhone: string staffPhone: string
amount: number amount: number
checkinTime: string
checkinAddress: string
serviceStartedAt: string
serviceFinishedAt: string
executionSummary: string
evidenceCount: number
serviceAddressSnapshot?: HomeServiceSelectedAddressType | null
timeline: Array<HomeServiceTimelineItemType> timeline: Array<HomeServiceTimelineItemType>
} }
@@ -67,6 +96,7 @@ export type HomeServiceApplicationDraftType = {
address: string address: string
preferredTime: string preferredTime: string
demandSummary: string demandSummary: string
serviceAddressSnapshot?: HomeServiceSelectedAddressType | null
} }
export type HomeServiceAdminApplicationType = { export type HomeServiceAdminApplicationType = {

View File

@@ -33,6 +33,32 @@ export const SERVICE_ORDER_STATUS_LIST: Array<ServiceOrderStatus> = [
'exception' 'exception'
] ]
export type ServiceOrderAddressSnapshotType = {
addressId: string
contactName: string
contactPhone: string
province: string
city: string
district: string
detailAddress: string
fullAddress: string
latitude: number
longitude: number
coordinateType: string
remark: string
}
export type ServiceOrderServiceSnapshotType = {
serviceId: string
serviceName: string
category: string
price: number
durationText: string
summary: string
tags: Array<string>
suitableFor: string
}
export type ServiceOrderTimelineItemType = { export type ServiceOrderTimelineItemType = {
id: string id: string
orderId: string orderId: string
@@ -44,6 +70,85 @@ export type ServiceOrderTimelineItemType = {
createdAt: string createdAt: string
} }
export type ServiceExecutionRecordType = {
id: string
orderId: string
assignmentId: string
checkinTime: string
checkinLatitude: number
checkinLongitude: number
checkinAddress: string
serviceStartedAt: string
serviceFinishedAt: string
actualDurationMinutes: number
serviceItemsJson: string
summary: string
remark: string
trackPointsJson: string
createdAt: string
updatedAt: string
}
export type ServiceEvidenceFileType = {
id: string
orderId: string
executionRecordId: string
phase: string
fileType: string
storagePath: string
fileUrl: string
latitude: number
longitude: number
capturedAt: string
createdAt: string
}
export type ServiceReviewType = {
id: string
orderId: string
userId: string
rating: number
tags: Array<string>
content: string
createdAt: string
}
export type ServiceOrderType = {
id: string
orderNo: string
userId: string
serviceId: string
serviceName: string
serviceSnapshot: ServiceOrderServiceSnapshotType
serviceAddressId: string
addressSnapshot: ServiceOrderAddressSnapshotType
recipientName: string
recipientPhone: string
contactName: string
contactPhone: string
appointmentTime: string
remark: string
status: ServiceOrderStatus
currentAssignmentId: string
currentStaffId: string
acceptedAt: string
departedAt: string
arrivedAt: string
serviceStartedAt: string
completedAt: string
pendingAcceptanceAt: string
acceptedByUserAt: string
reviewedAt: string
createdAt: string
updatedAt: string
staffName: string
staffPhone: string
logs: Array<ServiceOrderTimelineItemType>
executionRecord: ServiceExecutionRecordType | null
evidenceFiles: Array<ServiceEvidenceFileType>
review: ServiceReviewType | null
}
export function getServiceOrderStatusText(status: ServiceOrderStatus): string { export function getServiceOrderStatusText(status: ServiceOrderStatus): string {
if (status == 'created') return '待处理' if (status == 'created') return '待处理'
if (status == 'paid') return '已支付' if (status == 'paid') return '已支付'

172
utils/orderStatus.uts Normal file
View File

@@ -0,0 +1,172 @@
export type UnifiedOrderSource = 'goods' | 'service'
export type OrderStatusSource = {
source?: UnifiedOrderSource | string
order_status: number
payment_status: number
pay_expire_at: string
created_at: string
cancel_reason: string
}
export const ORDER_STATUS_PENDING = 1
export const ORDER_STATUS_PAID_OR_SHIPPING = 2
export const ORDER_STATUS_RECEIVING = 3
export const ORDER_STATUS_COMPLETED = 4
export const ORDER_STATUS_CANCELLED = 5
export const ORDER_STATUS_REFUNDING = 6
export const ORDER_STATUS_REFUNDED = 7
export const ORDER_STATUS_TIMEOUT_LEGACY = 8
export const PAYMENT_STATUS_UNPAID = 1
export const PAYMENT_STATUS_PAID = 2
export const PAYMENT_STATUS_PARTIAL_REFUND = 3
export const PAYMENT_STATUS_REFUNDED = 4
export const PAYMENT_STATUS_TIMEOUT = 5
export const ORDER_PAY_TIMEOUT_SECONDS = 10 * 60
export const ORDER_TIMEOUT_CANCEL_REASON = '支付超时自动取消'
function parseDateMs(value: string): number {
if (value == '') {
return 0
}
const date = new Date(value)
if (isNaN(date.getTime())) {
return 0
}
return date.getTime()
}
function containsTimeoutReason(cancelReason: string): boolean {
return cancelReason.indexOf('超时') >= 0
}
function getNormalizedSource(order: OrderStatusSource): UnifiedOrderSource {
if (order.source == 'service') {
return 'service'
}
return 'goods'
}
function isPendingSourceOrder(order: OrderStatusSource): boolean {
return order.order_status == ORDER_STATUS_PENDING && order.payment_status == PAYMENT_STATUS_UNPAID
}
export function getOrderDeadlineMs(order: OrderStatusSource): number {
const payExpireAtMs = parseDateMs(order.pay_expire_at)
if (payExpireAtMs > 0) {
return payExpireAtMs
}
const createdAtMs = parseDateMs(order.created_at)
if (createdAtMs > 0) {
return createdAtMs + ORDER_PAY_TIMEOUT_SECONDS * 1000
}
return 0
}
export function getRemainingSeconds(order: OrderStatusSource): number {
const deadlineMs = getOrderDeadlineMs(order)
if (deadlineMs <= 0) {
return 0
}
const diff = Math.floor((deadlineMs - Date.now()) / 1000)
return diff > 0 ? diff : 0
}
export function getUnifiedDisplayState(order: OrderStatusSource): string {
const source = getNormalizedSource(order)
const deadlineMs = getOrderDeadlineMs(order)
const hasExpiredByDeadline = order.order_status == ORDER_STATUS_PENDING && deadlineMs > 0 && deadlineMs <= Date.now()
if (source == 'service') {
if (order.order_status == ORDER_STATUS_PENDING && order.payment_status == PAYMENT_STATUS_UNPAID && !hasExpiredByDeadline && !containsTimeoutReason(order.cancel_reason)) {
return 'pending_pay'
}
if (order.order_status == ORDER_STATUS_TIMEOUT_LEGACY || order.payment_status == PAYMENT_STATUS_TIMEOUT || containsTimeoutReason(order.cancel_reason)) {
return 'cancelled'
}
if (hasExpiredByDeadline) {
return 'expired'
}
if (order.order_status == 2 || order.order_status == 3 || order.order_status == 4) {
return 'processing'
}
if (order.order_status == ORDER_STATUS_COMPLETED) {
return 'completed'
}
if (order.order_status == ORDER_STATUS_REFUNDING || order.order_status == ORDER_STATUS_REFUNDED) {
return 'refund'
}
return 'unknown'
}
if (order.order_status == ORDER_STATUS_PENDING && order.payment_status == PAYMENT_STATUS_UNPAID && !hasExpiredByDeadline && !containsTimeoutReason(order.cancel_reason)) {
return 'pending_pay'
}
if (order.order_status == ORDER_STATUS_CANCELLED || order.order_status == ORDER_STATUS_TIMEOUT_LEGACY || order.payment_status == PAYMENT_STATUS_TIMEOUT || containsTimeoutReason(order.cancel_reason)) {
return 'cancelled'
}
if (hasExpiredByDeadline) {
return 'expired'
}
if (order.order_status == ORDER_STATUS_PAID_OR_SHIPPING || order.order_status == ORDER_STATUS_RECEIVING) {
return 'processing'
}
if (order.order_status == ORDER_STATUS_COMPLETED) {
return 'completed'
}
if (order.order_status == ORDER_STATUS_REFUNDING || order.order_status == ORDER_STATUS_REFUNDED) {
return 'refund'
}
return 'unknown'
}
export function isOrderPayExpired(order: OrderStatusSource): boolean {
const displayState = getUnifiedDisplayState(order)
return displayState == 'cancelled' || displayState == 'expired'
}
export function isPendingPayOrder(order: OrderStatusSource): boolean {
return getUnifiedDisplayState(order) == 'pending_pay' && isPendingSourceOrder(order)
}
export function getOrderDisplayStatus(order: OrderStatusSource): string {
const displayState = getUnifiedDisplayState(order)
if (displayState == 'pending_pay') {
return 'pending'
}
if (displayState == 'cancelled' || displayState == 'expired') {
return 'cancelled'
}
if (displayState == 'processing') {
return 'shipping'
}
if (displayState == 'completed') {
return 'completed'
}
if (displayState == 'refund') {
return 'refund'
}
return 'unknown'
}
function pad2(value: number): string {
return value < 10 ? '0' + value : '' + value
}
export function formatCountdownHM(seconds: number): string {
const safeSeconds = seconds > 0 ? seconds : 0
const totalMinutes = Math.floor(safeSeconds / 60)
const hours = Math.floor(totalMinutes / 60)
const minutes = totalMinutes % 60
return pad2(hours) + '时' + pad2(minutes) + '分钟'
}
export function formatCountdownHMS(seconds: number): string {
const safeSeconds = seconds > 0 ? seconds : 0
const hours = Math.floor(safeSeconds / 3600)
const minutes = Math.floor((safeSeconds % 3600) / 60)
const remainSeconds = safeSeconds % 60
return pad2(hours) + ':' + pad2(minutes) + ':' + pad2(remainSeconds)
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,4 @@
<<<<<<< HEAD
uni.api.esm.js:1042 POST http://192.168.1.62:18000/auth/v1/token?grant_type=password 401 (Unauthorized)(env: Windows,mp,1.06.2504030; lib: 3.15.2) uni.api.esm.js:1042 POST http://192.168.1.62:18000/auth/v1/token?grant_type=password 401 (Unauthorized)(env: Windows,mp,1.06.2504030; lib: 3.15.2)
(anonymous) @ uni.api.esm.js:1042 (anonymous) @ uni.api.esm.js:1042
invokeApi @ uni.api.esm.js:330 invokeApi @ uni.api.esm.js:330
@@ -12,239 +13,579 @@ s @ regeneratorRuntime.js?forceSync=true:1
_ @ regeneratorRuntime.js?forceSync=true:1 _ @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1 (anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1 (anonymous) @ regeneratorRuntime.js?forceSync=true:1
=======
mp.esm.js:529 [getOrderDetail] 开始获取订单详情orderId: so-1779679148063-14086
mp.esm.js:529 [AkSupaQueryBuilder] execute - 表: ml_orders filter: user_id=eq.b653fded-7d5e-4950-aa0d-725595543e3c&consumer_deleted_at=is.null&order_no=eq.so-1779679148063-14086
mp.esm.js:529 [ak-req] GET http://119.146.131.237:9126/rest/v1/ml_orders?select=*%2C%20ml_order_items(*)&limit=1&user_id=eq.b653fded-7d5e-4950-aa0d-725595543e3c&consumer_deleted_at=is.null&order_no=eq.so-1779679148063-14086
mp.esm.js:529 [ak-req] apikey: eyJhbG...7890 | Authorization: Bearer eyJhbG...YFxs | auth-mode: pre-set | prefer: count=exact
mp.esm.js:529 [getOrderDetail] response.error: null
mp.esm.js:529 [getOrderDetail] response.data: []
mp.esm.js:529 [getOrderDetail] 未找到订单
mp.esm.js:529 [confirmPayment] 开始支付, orderId: so-1779679148063-14086 method: wechat
mp.esm.js:529 [getOrderDetail] 开始获取订单详情orderId: so-1779679148063-14086
mp.esm.js:529 [AkSupaQueryBuilder] execute - 表: ml_orders filter: user_id=eq.b653fded-7d5e-4950-aa0d-725595543e3c&consumer_deleted_at=is.null&order_no=eq.so-1779679148063-14086
mp.esm.js:529 [ak-req] GET http://119.146.131.237:9126/rest/v1/ml_orders?select=*%2C%20ml_order_items(*)&limit=1&user_id=eq.b653fded-7d5e-4950-aa0d-725595543e3c&consumer_deleted_at=is.null&order_no=eq.so-1779679148063-14086
mp.esm.js:529 [ak-req] apikey: eyJhbG...7890 | Authorization: Bearer eyJhbG...YFxs | auth-mode: pre-set | prefer: count=exact
mp.esm.js:529 [getOrderDetail] response.error: null
mp.esm.js:529 [getOrderDetail] response.data: []
mp.esm.js:529 [getOrderDetail] 未找到订单
mp.esm.js:529 [payOrder] 订单不存在,无法支付(env: Windows,mp,1.06.2504030; lib: 3.15.2)
(anonymous) @ mp.esm.js:529
__f__ @ uni.api.esm.js:590
_callee73$ @ supabaseService.uts:5491
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
>>>>>>> d9103c9bf (完善下单逻辑及其ui展示修复支付倒计时显示错误bug)
fulfilled @ uni.mp.esm.js:1134
Promise.then (async)
step @ uni.mp.esm.js:1134
(anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134
<<<<<<< HEAD
request @ ak-req.uts:148
_callee9$ @ aksupa.uts:887
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134
signIn @ aksupa.uts:876
_callee3$ @ delivery.uts:1506
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134
loginDelivery @ delivery.uts:1500
_callee$ @ deliveryService.uts:42
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134
loginDelivery @ deliveryService.uts:41
_callee5$ @ login.uvue:495
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134
handleLogin @ login.uvue:477
=======
payOrder @ supabaseService.uts:5481
_callee6$ @ payment.uvue:1291
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
fulfilled @ uni.mp.esm.js:1134
Promise.then (async)
step @ uni.mp.esm.js:1134
fulfilled @ uni.mp.esm.js:1134
Promise.then (async)
step @ uni.mp.esm.js:1134
(anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134
confirmPayment @ payment.uvue:1232
>>>>>>> d9103c9bf (完善下单逻辑及其ui展示修复支付倒计时显示错误bug)
callWithErrorHandling @ vue.runtime.esm.js:1356
callWithAsyncErrorHandling @ vue.runtime.esm.js:1363
invoke @ vue.runtime.esm.js:6223
setTimeout (async)
invoker @ vue.runtime.esm.js:6232
<<<<<<< HEAD
Show 19 more frames
mp.esm.js:529 [ak-req] ★ 401 Unauthorized(env: Windows,mp,1.06.2504030; lib: 3.15.2)
(anonymous) @ mp.esm.js:529
__f__ @ uni.api.esm.js:590
_callee2$ @ ak-req.uts:336
=======
mp.esm.js:529 [confirmPayment] 支付结果: false
mp.esm.js:529 [confirmPayment] payOrder 返回 false(env: Windows,mp,1.06.2504030; lib: 3.15.2)
(anonymous) @ mp.esm.js:529
__f__ @ uni.api.esm.js:590
_callee6$ @ payment.uvue:1295
>>>>>>> d9103c9bf (完善下单逻辑及其ui展示修复支付倒计时显示错误bug)
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
fulfilled @ uni.mp.esm.js:1134
Promise.then (async)
step @ uni.mp.esm.js:1134
fulfilled @ uni.mp.esm.js:1134
Promise.then (async)
step @ uni.mp.esm.js:1134
<<<<<<< HEAD
(anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134
request @ ak-req.uts:148
_callee9$ @ aksupa.uts:887
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134
signIn @ aksupa.uts:876
_callee3$ @ delivery.uts:1506
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134
loginDelivery @ delivery.uts:1500
_callee$ @ deliveryService.uts:42
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134
loginDelivery @ deliveryService.uts:41
_callee5$ @ login.uvue:495
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134
handleLogin @ login.uvue:477
=======
fulfilled @ uni.mp.esm.js:1134
Promise.then (async)
step @ uni.mp.esm.js:1134
(anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134
confirmPayment @ payment.uvue:1232
>>>>>>> d9103c9bf (完善下单逻辑及其ui展示修复支付倒计时显示错误bug)
callWithErrorHandling @ vue.runtime.esm.js:1356
callWithAsyncErrorHandling @ vue.runtime.esm.js:1363
invoke @ vue.runtime.esm.js:6223
setTimeout (async)
invoker @ vue.runtime.esm.js:6232
<<<<<<< HEAD
Show 14 more frames
mp.esm.js:529 [ak-req] url: http://192.168.1.62:18000/auth/v1/token?grant_type=password(env: Windows,mp,1.06.2504030; lib: 3.15.2)
(anonymous) @ mp.esm.js:529
__f__ @ uni.api.esm.js:590
_callee2$ @ ak-req.uts:337
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
fulfilled @ uni.mp.esm.js:1134
Promise.then (async)
step @ uni.mp.esm.js:1134
=======
mp.esm.js:529 [getOrderDetail] 开始获取订单详情orderId: so-1779679148063-14086
mp.esm.js:529 [AkSupaQueryBuilder] execute - 表: ml_orders filter: user_id=eq.b653fded-7d5e-4950-aa0d-725595543e3c&consumer_deleted_at=is.null&order_no=eq.so-1779679148063-14086
mp.esm.js:529 [ak-req] GET http://119.146.131.237:9126/rest/v1/ml_orders?select=*%2C%20ml_order_items(*)&limit=1&user_id=eq.b653fded-7d5e-4950-aa0d-725595543e3c&consumer_deleted_at=is.null&order_no=eq.so-1779679148063-14086
mp.esm.js:529 [ak-req] apikey: eyJhbG...7890 | Authorization: Bearer eyJhbG...YFxs | auth-mode: pre-set | prefer: count=exact
mp.esm.js:529 [getOrderDetail] response.error: null
mp.esm.js:529 [getOrderDetail] response.data: []
mp.esm.js:529 [getOrderDetail] 未找到订单
mp.esm.js:529 [getOrderDetail] 开始获取订单详情orderId: so-1779679148063-14086
mp.esm.js:529 [AkSupaQueryBuilder] execute - 表: ml_orders filter: user_id=eq.b653fded-7d5e-4950-aa0d-725595543e3c&consumer_deleted_at=is.null&order_no=eq.so-1779679148063-14086
mp.esm.js:529 [ak-req] GET http://119.146.131.237:9126/rest/v1/ml_orders?select=*%2C%20ml_order_items(*)&limit=1&user_id=eq.b653fded-7d5e-4950-aa0d-725595543e3c&consumer_deleted_at=is.null&order_no=eq.so-1779679148063-14086
mp.esm.js:529 [ak-req] apikey: eyJhbG...7890 | Authorization: Bearer eyJhbG...YFxs | auth-mode: pre-set | prefer: count=exact
mp.esm.js:529 [getOrderDetail] response.error: null
mp.esm.js:529 [getOrderDetail] response.data: []
mp.esm.js:529 [getOrderDetail] 未找到订单
mp.esm.js:529 [AkSupaQueryBuilder] execute - 表: ml_orders filter: id=eq.so-1779679148063-14086&user_id=eq.b653fded-7d5e-4950-aa0d-725595543e3c
mp.esm.js:529 [ak-req] PATCH http://119.146.131.237:9126/rest/v1/ml_orders?id=eq.so-1779679148063-14086&user_id=eq.b653fded-7d5e-4950-aa0d-725595543e3c
mp.esm.js:529 [ak-req] apikey: eyJhbG...7890 | Authorization: Bearer eyJhbG...YFxs | auth-mode: pre-set | prefer: return=representation
uni.api.esm.js:1042 PATCH http://119.146.131.237:9126/rest/v1/ml_orders?id=eq.so-1779679148063-14086&user_id=eq.b653fded-7d5e-4950-aa0d-725595543e3c 400 (Bad Request)(env: Windows,mp,1.06.2504030; lib: 3.15.2)
(anonymous) @ uni.api.esm.js:1042
invokeApi @ uni.api.esm.js:330
promiseApi @ uni.api.esm.js:889
(anonymous) @ ak-req.uts:214
doOnce @ ak-req.uts:213
_loop$ @ ak-req.uts:312
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
s @ regeneratorRuntime.js?forceSync=true:1
_ @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
>>>>>>> d9103c9bf (完善下单逻辑及其ui展示修复支付倒计时显示错误bug)
fulfilled @ uni.mp.esm.js:1134
Promise.then (async)
step @ uni.mp.esm.js:1134
(anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134
request @ ak-req.uts:148
<<<<<<< HEAD
_callee9$ @ aksupa.uts:887
=======
_callee19$ @ aksupa.uts:1288
>>>>>>> d9103c9bf (完善下单逻辑及其ui展示修复支付倒计时显示错误bug)
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134
<<<<<<< HEAD
signIn @ aksupa.uts:876
_callee3$ @ delivery.uts:1506
=======
requestWithAutoRefresh @ aksupa.uts:1287
_callee14$ @ aksupa.uts:1141
>>>>>>> d9103c9bf (完善下单逻辑及其ui展示修复支付倒计时显示错误bug)
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134
<<<<<<< HEAD
loginDelivery @ delivery.uts:1500
_callee$ @ deliveryService.uts:42
=======
update @ aksupa.uts:1117
_callee$ @ aksupa.uts:477
>>>>>>> d9103c9bf (完善下单逻辑及其ui展示修复支付倒计时显示错误bug)
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134
<<<<<<< HEAD
loginDelivery @ deliveryService.uts:41
_callee5$ @ login.uvue:495
=======
execute @ aksupa.uts:369
_callee61$ @ supabaseService.uts:4508
>>>>>>> d9103c9bf (完善下单逻辑及其ui展示修复支付倒计时显示错误bug)
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134
<<<<<<< HEAD
handleLogin @ login.uvue:477
callWithErrorHandling @ vue.runtime.esm.js:1356
callWithAsyncErrorHandling @ vue.runtime.esm.js:1363
invoke @ vue.runtime.esm.js:6223
setTimeout (async)
invoker @ vue.runtime.esm.js:6232
Show 14 more frames
mp.esm.js:529 [ak-req] auth-mode: apikey-only(env: Windows,mp,1.06.2504030; lib: 3.15.2)
(anonymous) @ mp.esm.js:529
__f__ @ uni.api.esm.js:590
_callee2$ @ ak-req.uts:338
=======
markOrderPaymentCancelled @ supabaseService.uts:4491
_callee6$ @ payment.uvue:1271
>>>>>>> d9103c9bf (完善下单逻辑及其ui展示修复支付倒计时显示错误bug)
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
fulfilled @ uni.mp.esm.js:1134
Promise.then (async)
step @ uni.mp.esm.js:1134
fulfilled @ uni.mp.esm.js:1134
Promise.then (async)
step @ uni.mp.esm.js:1134
(anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134
<<<<<<< HEAD
request @ ak-req.uts:148
_callee9$ @ aksupa.uts:887
=======
confirmPayment @ payment.uvue:1232
callWithErrorHandling @ vue.runtime.esm.js:1356
callWithAsyncErrorHandling @ vue.runtime.esm.js:1363
invoke @ vue.runtime.esm.js:6223
setTimeout (async)
invoker @ vue.runtime.esm.js:6232
Show 21 more frames
mp.esm.js:529 [markOrderPaymentCancelled] 保留待付款订单失败: UniError: 请求失败: 400
at _construct (weapp:///http://127.0.0.1:60394/appservice/@babel/runtime/helpers/construct.js?forceSync=true:1:1227)
at new r (weapp:///http://127.0.0.1:60394/appservice/@babel/runtime/helpers/wrapNativeSuper.js?forceSync=true:1:1357)
at UniError2.<anonymous> (weapp:///http://127.0.0.1:60394/appservice/@babel/runtime/helpers/createSuper.js?forceSync=true:1:1176)
at new UniError2 (weapp:///http://127.0.0.1:60394/appservice/common/vendor.js?t=wechat&s=1779670571338&v=2c3f4623fbd33b44aa0c0065a9eb296b:890:22)
at Object.toUniError (weapp:///http://127.0.0.1:60394/appservice/utils/utils.js?t=wechat&s=1779670571338&v=44243ae0f2468b2c481e39a715a29204:77:18)
at AkSupa._callee19$ (weapp:///http://127.0.0.1:60394/appservice/components/supadb/aksupa.js?t=wechat&s=1779670571338&v=f87225c7384edc68270412f64fbd4e8a:1959:41)
at s (weapp:///http://127.0.0.1:60394/appservice/@babel/runtime/helpers/regeneratorRuntime.js?forceSync=true:1:1588)
at Generator.<anonymous> (weapp:///http://127.0.0.1:60394/appservice/@babel/runtime/helpers/regeneratorRuntime.js?forceSync=true:1:2925)
at Generator.next (weapp:///http://127.0.0.1:60394/appservice/@babel/runtime/helpers/regeneratorRuntime.js?forceSync=true:1:1951)
at fulfilled (weapp:///http://127.0.0.1:60394/appservice/common/vendor.js?t=wechat&s=1779670571338&v=2c3f4623fbd33b44aa0c0065a9eb296b:10009:24)(env: Windows,mp,1.06.2504030; lib: 3.15.2)
(anonymous) @ mp.esm.js:529
__f__ @ uni.api.esm.js:590
_callee61$ @ supabaseService.uts:4511
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
fulfilled @ uni.mp.esm.js:1134
Promise.then (async)
step @ uni.mp.esm.js:1134
(anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134
markOrderPaymentCancelled @ supabaseService.uts:4491
_callee6$ @ payment.uvue:1271
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
fulfilled @ uni.mp.esm.js:1134
Promise.then (async)
step @ uni.mp.esm.js:1134
fulfilled @ uni.mp.esm.js:1134
Promise.then (async)
step @ uni.mp.esm.js:1134
(anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134
confirmPayment @ payment.uvue:1232
callWithErrorHandling @ vue.runtime.esm.js:1356
callWithAsyncErrorHandling @ vue.runtime.esm.js:1363
invoke @ vue.runtime.esm.js:6223
setTimeout (async)
invoker @ vue.runtime.esm.js:6232
mp.esm.js:529 收到订单更新事件: UTSJSONObject {orderId: "so-1779679148063-14086", status: 1, paymentStatus: 1, cancelReason: "", payExpireAt: "", …}
mp.esm.js:529 [AkSupaQueryBuilder] execute - 表: ml_user_profiles filter: user_id=eq.b653fded-7d5e-4950-aa0d-725595543e3c
mp.esm.js:529 [AkSupaQueryBuilder] execute - 表: ml_orders filter: user_id=eq.b653fded-7d5e-4950-aa0d-725595543e3c&consumer_deleted_at=is.null
mp.esm.js:529 [AkSupaQueryBuilder] execute - 表: ml_user_coupons filter: user_id=eq.b653fded-7d5e-4950-aa0d-725595543e3c&status=eq.1&expire_at=gt.2026-05-25T03%3A20%3A04.256Z
mp.esm.js:529 [AkSupaQueryBuilder] execute - 表: ml_orders filter: user_id=eq.b653fded-7d5e-4950-aa0d-725595543e3c&consumer_deleted_at=is.null
mp.esm.js:529 [ak-req] GET http://119.146.131.237:9126/rest/v1/ml_user_profiles?select=*&limit=1&user_id=eq.b653fded-7d5e-4950-aa0d-725595543e3c
mp.esm.js:529 [ak-req] apikey: eyJhbG...7890 | Authorization: Bearer eyJhbG...YFxs | auth-mode: pre-set | prefer: count=exact
mp.esm.js:529 [ak-req] GET http://119.146.131.237:9126/rest/v1/ml_orders?select=*%2C%20ml_order_items(*)%2C%20ml_shops(shop_name)&order=created_at.desc&user_id=eq.b653fded-7d5e-4950-aa0d-725595543e3c&consumer_deleted_at=is.null
mp.esm.js:529 [ak-req] apikey: eyJhbG...7890 | Authorization: Bearer eyJhbG...YFxs | auth-mode: pre-set | prefer: (none)
mp.esm.js:529 [ak-req] GET http://119.146.131.237:9126/rest/v1/ml_user_coupons?select=id&limit=1&user_id=eq.b653fded-7d5e-4950-aa0d-725595543e3c&status=eq.1&expire_at=gt.2026-05-25T03%3A20%3A04.256Z
mp.esm.js:529 [ak-req] apikey: eyJhbG...7890 | Authorization: Bearer eyJhbG...YFxs | auth-mode: pre-set | prefer: count=exact
mp.esm.js:529 [ak-req] GET http://119.146.131.237:9126/rest/v1/ml_orders?select=*&limit=1&user_id=eq.b653fded-7d5e-4950-aa0d-725595543e3c&consumer_deleted_at=is.null
mp.esm.js:529 [ak-req] apikey: eyJhbG...7890 | Authorization: Bearer eyJhbG...YFxs | auth-mode: pre-set | prefer: count=exact
mp.esm.js:529 [AkSupaQueryBuilder] execute - 表: ml_user_balance filter: user_id=eq.b653fded-7d5e-4950-aa0d-725595543e3c
mp.esm.js:529 [ak-req] GET http://119.146.131.237:9126/rest/v1/ml_user_balance?select=*&limit=1&user_id=eq.b653fded-7d5e-4950-aa0d-725595543e3c
mp.esm.js:529 [ak-req] apikey: eyJhbG...7890 | Authorization: Bearer eyJhbG...YFxs | auth-mode: pre-set | prefer: count=exact
mp.esm.js:529 [AkSupaQueryBuilder] execute - 表: ml_orders filter: user_id=eq.b653fded-7d5e-4950-aa0d-725595543e3c&consumer_deleted_at=is.null&order_status=eq.1
mp.esm.js:529 [ak-req] GET http://119.146.131.237:9126/rest/v1/ml_orders?select=*&limit=1&user_id=eq.b653fded-7d5e-4950-aa0d-725595543e3c&consumer_deleted_at=is.null&order_status=eq.1
mp.esm.js:529 [ak-req] apikey: eyJhbG...7890 | Authorization: Bearer eyJhbG...YFxs | auth-mode: pre-set | prefer: count=exact
mp.esm.js:529 [AkSupaQueryBuilder] execute - 表: ml_user_points filter: user_id=eq.b653fded-7d5e-4950-aa0d-725595543e3c
mp.esm.js:529 [ak-req] GET http://119.146.131.237:9126/rest/v1/ml_user_points?select=points&limit=1&user_id=eq.b653fded-7d5e-4950-aa0d-725595543e3c
mp.esm.js:529 [ak-req] apikey: eyJhbG...7890 | Authorization: Bearer eyJhbG...YFxs | auth-mode: pre-set | prefer: count=exact,return=representation,single-object
mp.esm.js:529 [AkSupaQueryBuilder] execute - 表: ml_orders filter: user_id=eq.b653fded-7d5e-4950-aa0d-725595543e3c&consumer_deleted_at=is.null&order_status=eq.2
mp.esm.js:529 [ak-req] GET http://119.146.131.237:9126/rest/v1/ml_orders?select=*&limit=1&user_id=eq.b653fded-7d5e-4950-aa0d-725595543e3c&consumer_deleted_at=is.null&order_status=eq.2
mp.esm.js:529 [ak-req] apikey: eyJhbG...7890 | Authorization: Bearer eyJhbG...YFxs | auth-mode: pre-set | prefer: count=exact
mp.esm.js:529 [AkSupaQueryBuilder] execute - 表: ml_orders filter: user_id=eq.b653fded-7d5e-4950-aa0d-725595543e3c&consumer_deleted_at=is.null&order_status=eq.3
mp.esm.js:529 [ak-req] GET http://119.146.131.237:9126/rest/v1/ml_orders?select=*&limit=1&user_id=eq.b653fded-7d5e-4950-aa0d-725595543e3c&consumer_deleted_at=is.null&order_status=eq.3
mp.esm.js:529 [ak-req] apikey: eyJhbG...7890 | Authorization: Bearer eyJhbG...YFxs | auth-mode: pre-set | prefer: count=exact
mp.esm.js:529 [AkSupaQueryBuilder] execute - 表: ml_orders filter: user_id=eq.b653fded-7d5e-4950-aa0d-725595543e3c&consumer_deleted_at=is.null&order_status=eq.4
mp.esm.js:529 [ak-req] GET http://119.146.131.237:9126/rest/v1/ml_orders?select=*&limit=1&user_id=eq.b653fded-7d5e-4950-aa0d-725595543e3c&consumer_deleted_at=is.null&order_status=eq.4
mp.esm.js:529 [ak-req] apikey: eyJhbG...7890 | Authorization: Bearer eyJhbG...YFxs | auth-mode: pre-set | prefer: count=exact
mp.esm.js:529 [AkSupaQueryBuilder] execute - 表: ml_orders filter: user_id=eq.b653fded-7d5e-4950-aa0d-725595543e3c&consumer_deleted_at=is.null&order_status=in.(6,7)
mp.esm.js:529 [ak-req] GET http://119.146.131.237:9126/rest/v1/ml_orders?select=*&limit=1&user_id=eq.b653fded-7d5e-4950-aa0d-725595543e3c&consumer_deleted_at=is.null&order_status=in.(6,7)
mp.esm.js:529 [ak-req] apikey: eyJhbG...7890 | Authorization: Bearer eyJhbG...YFxs | auth-mode: pre-set | prefer: count=exact
mp.esm.js:529 [getOrders] response.error: null
mp.esm.js:529 [getOrders] 订单数量: 104
mp.esm.js:529 [AkSupaQueryBuilder] execute - 表: ml_orders filter: user_id=eq.b653fded-7d5e-4950-aa0d-725595543e3c&consumer_deleted_at=is.null&order_status=eq.5
mp.esm.js:529 [ak-req] GET http://119.146.131.237:9126/rest/v1/ml_orders?select=*&limit=1&user_id=eq.b653fded-7d5e-4950-aa0d-725595543e3c&consumer_deleted_at=is.null&order_status=eq.5
mp.esm.js:529 [ak-req] apikey: eyJhbG...7890 | Authorization: Bearer eyJhbG...YFxs | auth-mode: pre-set | prefer: count=exact
mp.esm.js:529 [AkSupaQueryBuilder] execute - 表: hss_service_orders filter: user_id=eq.b653fded-7d5e-4950-aa0d-725595543e3c
mp.esm.js:529 [ak-req] GET http://119.146.131.237:9126/rest/v1/hss_service_orders?select=status&limit=500&user_id=eq.b653fded-7d5e-4950-aa0d-725595543e3c
mp.esm.js:529 [ak-req] apikey: eyJhbG...7890 | Authorization: Bearer eyJhbG...YFxs | auth-mode: pre-set | prefer: count=exact
mp.esm.js:529 [getOrderDetail] 开始获取订单详情orderId: so-1779679148063-14086
mp.esm.js:529 [AkSupaQueryBuilder] execute - 表: ml_orders filter: user_id=eq.b653fded-7d5e-4950-aa0d-725595543e3c&consumer_deleted_at=is.null&order_no=eq.so-1779679148063-14086
mp.esm.js:529 [ak-req] GET http://119.146.131.237:9126/rest/v1/ml_orders?select=*%2C%20ml_order_items(*)&limit=1&user_id=eq.b653fded-7d5e-4950-aa0d-725595543e3c&consumer_deleted_at=is.null&order_no=eq.so-1779679148063-14086
mp.esm.js:529 [ak-req] apikey: eyJhbG...7890 | Authorization: Bearer eyJhbG...YFxs | auth-mode: pre-set | prefer: count=exact
mp.esm.js:529 [getOrderDetail] response.error: null
mp.esm.js:529 [getOrderDetail] response.data: []
mp.esm.js:529 [getOrderDetail] 未找到订单
mp.esm.js:529 [AkSupaQueryBuilder] execute - 表: ml_orders filter: id=eq.so-1779679148063-14086&user_id=eq.b653fded-7d5e-4950-aa0d-725595543e3c
mp.esm.js:529 [ak-req] PATCH http://119.146.131.237:9126/rest/v1/ml_orders?id=eq.so-1779679148063-14086&user_id=eq.b653fded-7d5e-4950-aa0d-725595543e3c
mp.esm.js:529 [ak-req] apikey: eyJhbG...7890 | Authorization: Bearer eyJhbG...YFxs | auth-mode: pre-set | prefer: return=representation
uni.api.esm.js:1042 PATCH http://119.146.131.237:9126/rest/v1/ml_orders?id=eq.so-1779679148063-14086&user_id=eq.b653fded-7d5e-4950-aa0d-725595543e3c 400 (Bad Request)(env: Windows,mp,1.06.2504030; lib: 3.15.2)
(anonymous) @ uni.api.esm.js:1042
invokeApi @ uni.api.esm.js:330
promiseApi @ uni.api.esm.js:889
(anonymous) @ ak-req.uts:214
doOnce @ ak-req.uts:213
_loop$ @ ak-req.uts:312
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
s @ regeneratorRuntime.js?forceSync=true:1
_ @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
fulfilled @ uni.mp.esm.js:1134 fulfilled @ uni.mp.esm.js:1134
Promise.then (async) Promise.then (async)
step @ uni.mp.esm.js:1134 step @ uni.mp.esm.js:1134
(anonymous) @ uni.mp.esm.js:1134 (anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134 __awaiter @ uni.mp.esm.js:1134
request @ ak-req.uts:148 request @ ak-req.uts:148
_callee9$ @ aksupa.uts:887 _callee19$ @ aksupa.uts:1288
s @ regeneratorRuntime.js?forceSync=true:1 >>>>>>> d9103c9bf (完善下单逻辑及其ui展示修复支付倒计时显示错误bug)
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134
signIn @ aksupa.uts:876
_callee3$ @ delivery.uts:1506
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134
loginDelivery @ delivery.uts:1500
_callee$ @ deliveryService.uts:42
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134
loginDelivery @ deliveryService.uts:41
_callee5$ @ login.uvue:495
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134
handleLogin @ login.uvue:477
callWithErrorHandling @ vue.runtime.esm.js:1356
callWithAsyncErrorHandling @ vue.runtime.esm.js:1363
invoke @ vue.runtime.esm.js:6223
setTimeout (async)
invoker @ vue.runtime.esm.js:6232
Show 19 more frames
mp.esm.js:529 [ak-req] ★ 401 Unauthorized(env: Windows,mp,1.06.2504030; lib: 3.15.2)
(anonymous) @ mp.esm.js:529
__f__ @ uni.api.esm.js:590
_callee2$ @ ak-req.uts:336
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
fulfilled @ uni.mp.esm.js:1134
Promise.then (async)
step @ uni.mp.esm.js:1134
fulfilled @ uni.mp.esm.js:1134
Promise.then (async)
step @ uni.mp.esm.js:1134
(anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134
request @ ak-req.uts:148
_callee9$ @ aksupa.uts:887
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134
signIn @ aksupa.uts:876
_callee3$ @ delivery.uts:1506
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134
loginDelivery @ delivery.uts:1500
_callee$ @ deliveryService.uts:42
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134
loginDelivery @ deliveryService.uts:41
_callee5$ @ login.uvue:495
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134
handleLogin @ login.uvue:477
callWithErrorHandling @ vue.runtime.esm.js:1356
callWithAsyncErrorHandling @ vue.runtime.esm.js:1363
invoke @ vue.runtime.esm.js:6223
setTimeout (async)
invoker @ vue.runtime.esm.js:6232
Show 14 more frames
mp.esm.js:529 [ak-req] url: http://192.168.1.62:18000/auth/v1/token?grant_type=password(env: Windows,mp,1.06.2504030; lib: 3.15.2)
(anonymous) @ mp.esm.js:529
__f__ @ uni.api.esm.js:590
_callee2$ @ ak-req.uts:337
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
fulfilled @ uni.mp.esm.js:1134
Promise.then (async)
step @ uni.mp.esm.js:1134
fulfilled @ uni.mp.esm.js:1134
Promise.then (async)
step @ uni.mp.esm.js:1134
(anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134
request @ ak-req.uts:148
_callee9$ @ aksupa.uts:887
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134
signIn @ aksupa.uts:876
_callee3$ @ delivery.uts:1506
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134
loginDelivery @ delivery.uts:1500
_callee$ @ deliveryService.uts:42
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134
loginDelivery @ deliveryService.uts:41
_callee5$ @ login.uvue:495
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134
handleLogin @ login.uvue:477
callWithErrorHandling @ vue.runtime.esm.js:1356
callWithAsyncErrorHandling @ vue.runtime.esm.js:1363
invoke @ vue.runtime.esm.js:6223
setTimeout (async)
invoker @ vue.runtime.esm.js:6232
Show 14 more frames
mp.esm.js:529 [ak-req] auth-mode: apikey-only(env: Windows,mp,1.06.2504030; lib: 3.15.2)
(anonymous) @ mp.esm.js:529
__f__ @ uni.api.esm.js:590
_callee2$ @ ak-req.uts:338
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
fulfilled @ uni.mp.esm.js:1134
Promise.then (async)
step @ uni.mp.esm.js:1134
fulfilled @ uni.mp.esm.js:1134
Promise.then (async)
step @ uni.mp.esm.js:1134
(anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134
request @ ak-req.uts:148
_callee9$ @ aksupa.uts:887
s @ regeneratorRuntime.js?forceSync=true:1 s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1 (anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1 (anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ uni.mp.esm.js:1134 (anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134 __awaiter @ uni.mp.esm.js:1134
<<<<<<< HEAD
signIn @ aksupa.uts:876 signIn @ aksupa.uts:876
_callee3$ @ delivery.uts:1506 _callee3$ @ delivery.uts:1506
=======
requestWithAutoRefresh @ aksupa.uts:1287
_callee14$ @ aksupa.uts:1141
>>>>>>> d9103c9bf (完善下单逻辑及其ui展示修复支付倒计时显示错误bug)
s @ regeneratorRuntime.js?forceSync=true:1 s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1 (anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1 (anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ uni.mp.esm.js:1134 (anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134 __awaiter @ uni.mp.esm.js:1134
<<<<<<< HEAD
loginDelivery @ delivery.uts:1500 loginDelivery @ delivery.uts:1500
_callee$ @ deliveryService.uts:42 _callee$ @ deliveryService.uts:42
=======
update @ aksupa.uts:1117
_callee$ @ aksupa.uts:477
>>>>>>> d9103c9bf (完善下单逻辑及其ui展示修复支付倒计时显示错误bug)
s @ regeneratorRuntime.js?forceSync=true:1 s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1 (anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1 (anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ uni.mp.esm.js:1134 (anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134 __awaiter @ uni.mp.esm.js:1134
<<<<<<< HEAD
loginDelivery @ deliveryService.uts:41 loginDelivery @ deliveryService.uts:41
_callee5$ @ login.uvue:495 _callee5$ @ login.uvue:495
=======
execute @ aksupa.uts:369
_callee52$ @ supabaseService.uts:4038
>>>>>>> d9103c9bf (完善下单逻辑及其ui展示修复支付倒计时显示错误bug)
s @ regeneratorRuntime.js?forceSync=true:1 s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1 (anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1 (anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ uni.mp.esm.js:1134 (anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134 __awaiter @ uni.mp.esm.js:1134
<<<<<<< HEAD
handleLogin @ login.uvue:477 handleLogin @ login.uvue:477
=======
cancelOrder @ supabaseService.uts:4018
_callee5$ @ payment.uvue:1153
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134
confirmCancelOrder @ payment.uvue:1141
>>>>>>> d9103c9bf (完善下单逻辑及其ui展示修复支付倒计时显示错误bug)
callWithErrorHandling @ vue.runtime.esm.js:1356 callWithErrorHandling @ vue.runtime.esm.js:1356
callWithAsyncErrorHandling @ vue.runtime.esm.js:1363 callWithAsyncErrorHandling @ vue.runtime.esm.js:1363
invoke @ vue.runtime.esm.js:6223 invoke @ vue.runtime.esm.js:6223
setTimeout (async) setTimeout (async)
invoker @ vue.runtime.esm.js:6232 invoker @ vue.runtime.esm.js:6232
<<<<<<< HEAD
Show 14 more frames Show 14 more frames
mp.esm.js:529 [ak-req] 发送 apikey: eyJhbG...7890(env: Windows,mp,1.06.2504030; lib: 3.15.2) mp.esm.js:529 [ak-req] 发送 apikey: eyJhbG...7890(env: Windows,mp,1.06.2504030; lib: 3.15.2)
(anonymous) @ mp.esm.js:529 (anonymous) @ mp.esm.js:529
__f__ @ uni.api.esm.js:590 __f__ @ uni.api.esm.js:590
_callee2$ @ ak-req.uts:339 _callee2$ @ ak-req.uts:339
=======
Show 26 more frames
mp.esm.js:529 取消订单失败: UniError: 请求失败: 400
at _construct (weapp:///http://127.0.0.1:60394/appservice/@babel/runtime/helpers/construct.js?forceSync=true:1:1227)
at new r (weapp:///http://127.0.0.1:60394/appservice/@babel/runtime/helpers/wrapNativeSuper.js?forceSync=true:1:1357)
at UniError2.<anonymous> (weapp:///http://127.0.0.1:60394/appservice/@babel/runtime/helpers/createSuper.js?forceSync=true:1:1176)
at new UniError2 (weapp:///http://127.0.0.1:60394/appservice/common/vendor.js?t=wechat&s=1779670571338&v=2c3f4623fbd33b44aa0c0065a9eb296b:890:22)
at Object.toUniError (weapp:///http://127.0.0.1:60394/appservice/utils/utils.js?t=wechat&s=1779670571338&v=44243ae0f2468b2c481e39a715a29204:77:18)
at AkSupa._callee19$ (weapp:///http://127.0.0.1:60394/appservice/components/supadb/aksupa.js?t=wechat&s=1779670571338&v=f87225c7384edc68270412f64fbd4e8a:1959:41)
at s (weapp:///http://127.0.0.1:60394/appservice/@babel/runtime/helpers/regeneratorRuntime.js?forceSync=true:1:1588)
at Generator.<anonymous> (weapp:///http://127.0.0.1:60394/appservice/@babel/runtime/helpers/regeneratorRuntime.js?forceSync=true:1:2925)
at Generator.next (weapp:///http://127.0.0.1:60394/appservice/@babel/runtime/helpers/regeneratorRuntime.js?forceSync=true:1:1951)
at fulfilled (weapp:///http://127.0.0.1:60394/appservice/common/vendor.js?t=wechat&s=1779670571338&v=2c3f4623fbd33b44aa0c0065a9eb296b:10009:24)(env: Windows,mp,1.06.2504030; lib: 3.15.2)
(anonymous) @ mp.esm.js:529
__f__ @ uni.api.esm.js:590
_callee52$ @ supabaseService.uts:4041
>>>>>>> d9103c9bf (完善下单逻辑及其ui展示修复支付倒计时显示错误bug)
s @ regeneratorRuntime.js?forceSync=true:1 s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1 (anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1 (anonymous) @ regeneratorRuntime.js?forceSync=true:1
fulfilled @ uni.mp.esm.js:1134 fulfilled @ uni.mp.esm.js:1134
Promise.then (async) Promise.then (async)
step @ uni.mp.esm.js:1134 step @ uni.mp.esm.js:1134
<<<<<<< HEAD
=======
(anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134
cancelOrder @ supabaseService.uts:4018
_callee5$ @ payment.uvue:1153
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134
confirmCancelOrder @ payment.uvue:1141
callWithErrorHandling @ vue.runtime.esm.js:1356
callWithAsyncErrorHandling @ vue.runtime.esm.js:1363
invoke @ vue.runtime.esm.js:6223
setTimeout (async)
invoker @ vue.runtime.esm.js:6232
[自动热重载] 已开启代码文件保存后自动热重载
mp.esm.js:529 [AkSupaQueryBuilder] execute - 表: ml_orders filter: id=eq.so-1779679148063-14086&user_id=eq.b653fded-7d5e-4950-aa0d-725595543e3c
mp.esm.js:529 [ak-req] PATCH http://119.146.131.237:9126/rest/v1/ml_orders?id=eq.so-1779679148063-14086&user_id=eq.b653fded-7d5e-4950-aa0d-725595543e3c
mp.esm.js:529 [ak-req] apikey: eyJhbG...7890 | Authorization: Bearer eyJhbG...YFxs | auth-mode: pre-set | prefer: return=representation
uni.api.esm.js:1042 PATCH http://119.146.131.237:9126/rest/v1/ml_orders?id=eq.so-1779679148063-14086&user_id=eq.b653fded-7d5e-4950-aa0d-725595543e3c 400 (Bad Request)(env: Windows,mp,1.06.2504030; lib: 3.15.2)
(anonymous) @ uni.api.esm.js:1042
invokeApi @ uni.api.esm.js:330
promiseApi @ uni.api.esm.js:889
(anonymous) @ ak-req.uts:214
doOnce @ ak-req.uts:213
_loop$ @ ak-req.uts:312
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
s @ regeneratorRuntime.js?forceSync=true:1
_ @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
>>>>>>> d9103c9bf (完善下单逻辑及其ui展示修复支付倒计时显示错误bug)
fulfilled @ uni.mp.esm.js:1134 fulfilled @ uni.mp.esm.js:1134
Promise.then (async) Promise.then (async)
step @ uni.mp.esm.js:1134 step @ uni.mp.esm.js:1134
(anonymous) @ uni.mp.esm.js:1134 (anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134 __awaiter @ uni.mp.esm.js:1134
request @ ak-req.uts:148 request @ ak-req.uts:148
<<<<<<< HEAD
_callee9$ @ aksupa.uts:887 _callee9$ @ aksupa.uts:887
=======
_callee19$ @ aksupa.uts:1288
>>>>>>> d9103c9bf (完善下单逻辑及其ui展示修复支付倒计时显示错误bug)
s @ regeneratorRuntime.js?forceSync=true:1 s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1 (anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1 (anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ uni.mp.esm.js:1134 (anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134 __awaiter @ uni.mp.esm.js:1134
<<<<<<< HEAD
signIn @ aksupa.uts:876 signIn @ aksupa.uts:876
_callee3$ @ delivery.uts:1506 _callee3$ @ delivery.uts:1506
=======
requestWithAutoRefresh @ aksupa.uts:1287
_callee14$ @ aksupa.uts:1141
>>>>>>> d9103c9bf (完善下单逻辑及其ui展示修复支付倒计时显示错误bug)
s @ regeneratorRuntime.js?forceSync=true:1 s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1 (anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1 (anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ uni.mp.esm.js:1134 (anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134 __awaiter @ uni.mp.esm.js:1134
<<<<<<< HEAD
loginDelivery @ delivery.uts:1500 loginDelivery @ delivery.uts:1500
_callee$ @ deliveryService.uts:42 _callee$ @ deliveryService.uts:42
=======
update @ aksupa.uts:1117
_callee$ @ aksupa.uts:477
>>>>>>> d9103c9bf (完善下单逻辑及其ui展示修复支付倒计时显示错误bug)
s @ regeneratorRuntime.js?forceSync=true:1 s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1 (anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1 (anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ uni.mp.esm.js:1134 (anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134 __awaiter @ uni.mp.esm.js:1134
<<<<<<< HEAD
loginDelivery @ deliveryService.uts:41 loginDelivery @ deliveryService.uts:41
_callee5$ @ login.uvue:495 _callee5$ @ login.uvue:495
=======
execute @ aksupa.uts:369
_callee52$ @ supabaseService.uts:4038
>>>>>>> d9103c9bf (完善下单逻辑及其ui展示修复支付倒计时显示错误bug)
s @ regeneratorRuntime.js?forceSync=true:1 s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1 (anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1 (anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ uni.mp.esm.js:1134 (anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134 __awaiter @ uni.mp.esm.js:1134
<<<<<<< HEAD
handleLogin @ login.uvue:477 handleLogin @ login.uvue:477
callWithErrorHandling @ vue.runtime.esm.js:1356 callWithErrorHandling @ vue.runtime.esm.js:1356
callWithAsyncErrorHandling @ vue.runtime.esm.js:1363 callWithAsyncErrorHandling @ vue.runtime.esm.js:1363
@@ -256,12 +597,40 @@ mp.esm.js:529 [ak-req] 发送 Authorization: (MISSING!)(env: Windows,mp,1.06.250
(anonymous) @ mp.esm.js:529 (anonymous) @ mp.esm.js:529
__f__ @ uni.api.esm.js:590 __f__ @ uni.api.esm.js:590
_callee2$ @ ak-req.uts:340 _callee2$ @ ak-req.uts:340
=======
cancelOrder @ supabaseService.uts:4018
_callee7$ @ orders.uvue:1422
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134
doCancelOrder @ orders.uvue:1417
success @ orders.uvue:1443
(anonymous) @ uni.api.esm.js:946
Show 25 more frames
mp.esm.js:529 取消订单失败: UniError: 请求失败: 400
at _construct (weapp:///http://127.0.0.1:60394/appservice/@babel/runtime/helpers/construct.js?forceSync=true:1:1227)
at new r (weapp:///http://127.0.0.1:60394/appservice/@babel/runtime/helpers/wrapNativeSuper.js?forceSync=true:1:1357)
at UniError2.<anonymous> (weapp:///http://127.0.0.1:60394/appservice/@babel/runtime/helpers/createSuper.js?forceSync=true:1:1176)
at new UniError2 (weapp:///http://127.0.0.1:60394/appservice/common/vendor.js?t=wechat&s=1779670571338&v=2c3f4623fbd33b44aa0c0065a9eb296b:890:22)
at Object.toUniError (weapp:///http://127.0.0.1:60394/appservice/utils/utils.js?t=wechat&s=1779670571338&v=44243ae0f2468b2c481e39a715a29204:77:18)
at AkSupa._callee19$ (weapp:///http://127.0.0.1:60394/appservice/components/supadb/aksupa.js?t=wechat&s=1779670571338&v=f87225c7384edc68270412f64fbd4e8a:1959:41)
at s (weapp:///http://127.0.0.1:60394/appservice/@babel/runtime/helpers/regeneratorRuntime.js?forceSync=true:1:1588)
at Generator.<anonymous> (weapp:///http://127.0.0.1:60394/appservice/@babel/runtime/helpers/regeneratorRuntime.js?forceSync=true:1:2925)
at Generator.next (weapp:///http://127.0.0.1:60394/appservice/@babel/runtime/helpers/regeneratorRuntime.js?forceSync=true:1:1951)
at fulfilled (weapp:///http://127.0.0.1:60394/appservice/common/vendor.js?t=wechat&s=1779670571338&v=2c3f4623fbd33b44aa0c0065a9eb296b:10009:24)(env: Windows,mp,1.06.2504030; lib: 3.15.2)
(anonymous) @ mp.esm.js:529
__f__ @ uni.api.esm.js:590
_callee52$ @ supabaseService.uts:4041
>>>>>>> d9103c9bf (完善下单逻辑及其ui展示修复支付倒计时显示错误bug)
s @ regeneratorRuntime.js?forceSync=true:1 s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1 (anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1 (anonymous) @ regeneratorRuntime.js?forceSync=true:1
fulfilled @ uni.mp.esm.js:1134 fulfilled @ uni.mp.esm.js:1134
Promise.then (async) Promise.then (async)
step @ uni.mp.esm.js:1134 step @ uni.mp.esm.js:1134
<<<<<<< HEAD
fulfilled @ uni.mp.esm.js:1134 fulfilled @ uni.mp.esm.js:1134
Promise.then (async) Promise.then (async)
step @ uni.mp.esm.js:1134 step @ uni.mp.esm.js:1134
@@ -269,11 +638,18 @@ step @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134 __awaiter @ uni.mp.esm.js:1134
request @ ak-req.uts:148 request @ ak-req.uts:148
_callee9$ @ aksupa.uts:887 _callee9$ @ aksupa.uts:887
=======
(anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134
cancelOrder @ supabaseService.uts:4018
_callee7$ @ orders.uvue:1422
>>>>>>> d9103c9bf (完善下单逻辑及其ui展示修复支付倒计时显示错误bug)
s @ regeneratorRuntime.js?forceSync=true:1 s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1 (anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1 (anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ uni.mp.esm.js:1134 (anonymous) @ uni.mp.esm.js:1134
__awaiter @ uni.mp.esm.js:1134 __awaiter @ uni.mp.esm.js:1134
<<<<<<< HEAD
signIn @ aksupa.uts:876 signIn @ aksupa.uts:876
_callee3$ @ delivery.uts:1506 _callee3$ @ delivery.uts:1506
s @ regeneratorRuntime.js?forceSync=true:1 s @ regeneratorRuntime.js?forceSync=true:1
@@ -459,4 +835,9 @@ Error: timeout
at WAServiceMainContext.js?t=wechat&v=3.15.2:1(env: Windows,mp,1.06.2504030; lib: 3.15.2) at WAServiceMainContext.js?t=wechat&v=3.15.2:1(env: Windows,mp,1.06.2504030; lib: 3.15.2)
15:42:21.953 [plugin:uts] Invalid end tag. 15:42:21.953 [plugin:uts] Invalid end tag.
15:42:22.203 at pages/mall/delivery/home/index.uvue:454:1 15:42:22.203 at pages/mall/delivery/home/index.uvue:454:1
=======
doCancelOrder @ orders.uvue:1417
success @ orders.uvue:1443
(anonymous) @ uni.api.esm.js:946
>>>>>>> d9103c9bf (完善下单逻辑及其ui展示修复支付倒计时显示错误bug)