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

View File

@@ -174,40 +174,13 @@
</view>
</view>
<!-- 推荐商品 -->
<view v-if="currentCartType == 'goods' && recommendProducts.length > 0" class="recommend-section">
<view class="section-header">
<text class="section-title">猜你喜欢</text>
</view>
<view class="recommend-list">
<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>
<GuessYouLike
v-if="currentCartType == 'goods'"
title="猜你喜欢"
:pageSize="8"
:loadMoreKey="guessLoadMoreKey"
@productClick="handleGuessProductClick"
/>
<!-- 底部占位符:确保内容不被原生 TabBar 遮挡 -->
<view class="tabbar-safe-area"></view>
</scroll-view>
@@ -280,6 +253,7 @@ import { ref, computed, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { supabaseService, type CartItem as SupabaseCartItem, type Product } from '@/utils/supabaseService.uts'
import { goToLogin } from '@/utils/utils.uts'
import GuessYouLike from '@/components/mall/GuessYouLike/GuessYouLike.uvue'
type ModalSuccess = { confirm: boolean; cancel: boolean }
@@ -375,6 +349,7 @@ const recommendBottomLocked = ref<boolean>(false)
const recommendViewportHeight = ref<number>(0)
const pageWindowHeight = ref<number>(0)
const isAndroidApp = ref<boolean>(false)
const guessLoadMoreKey = ref<number>(0)
const serviceMockItems = ref<ServiceCartItem[]>([
{
id: 'service-1',
@@ -725,27 +700,6 @@ const mergeRecommendProducts = (oldList: RecommendProduct[], newList: RecommendP
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[]> => {
console.log('[cart推荐] fetchRecommendProducts 请求 page=', page, 'pageSize=', pageSize)
const hotResp = await supabaseService.searchProducts('', page, pageSize, 'sales')
@@ -851,13 +805,7 @@ async function loadRecommendProducts(reset: boolean): Promise<void> {
recommendInitialized.value = true
} catch (error) {
console.error('加载推荐商品失败:', error)
if (reset && recommendProducts.value.length === 0) {
const mockList = mockRecommendProducts(1, recommendPageSize.value)
recommendProducts.value = mockList
recommendPage.value = 2
recommendHasMore.value = true
recommendInitialized.value = true
}
recommendHasMore.value = false
} finally {
recommendLoading.value = false
if (!reset && recommendPendingLoad.value && recommendHasMore.value) {
@@ -869,59 +817,13 @@ async function loadRecommendProducts(reset: boolean): Promise<void> {
}
function onRecommendScrollToLower(): void {
console.log('[cart推荐] scrolltolower 触发 currentCartType=', currentCartType.value, 'initialized=', recommendInitialized.value)
if (currentCartType.value != 'goods') {
console.log('[cart推荐] 跳过:当前不是 goods')
return
if (currentCartType.value == 'goods') {
guessLoadMoreKey.value = guessLoadMoreKey.value + 1
}
recommendBottomLocked.value = true
loadRecommendProducts(false)
}
function onRecommendScroll(event: any): void {
if (currentCartType.value != 'goods' || recommendLoading.value || !recommendHasMore.value) {
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)
}
return
}
// 加载数据
@@ -981,9 +883,6 @@ const loadCartData = async () => {
console.log('Transformed items count:', transformedItems.length);
cartItems.value = transformedItems
if (!recommendInitialized.value) {
await loadRecommendProducts(true)
}
} catch (error) {
console.error('加载购物车数据失败:', error)
cartItems.value = []
@@ -996,6 +895,10 @@ onShow(() => {
loadCartData()
})
const handleGuessProductClick = (productId: string) => {
navigateToProduct({ id: productId, productId: productId, price: 0 })
}
// 商品操作 - 更新选中状态到Supabase
const toggleSelect = async (itemId: string) => {
// 乐观更新
@@ -2593,4 +2496,3 @@ const goToCheckout = () => {
background-color: transparent;
}
</style>

View File

@@ -436,68 +436,6 @@ function buildServiceImageText(categoryId: string): string {
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> {
const result: Array<HomeCareServiceProductType> = []
for (let i = 0; i < catalog.length; i++) {
@@ -525,14 +463,10 @@ async function loadServiceHomeData(): Promise<void> {
serviceLoading.value = true
try {
const catalog = await fetchHomeServiceCatalog()
if (catalog.length > 0) {
allServiceProducts.value = buildServiceProductsFromCatalog(catalog)
} else {
allServiceProducts.value = buildMockServiceProducts()
}
allServiceProducts.value = buildServiceProductsFromCatalog(catalog)
} catch (error) {
console.error('加载服务首页数据失败', error)
allServiceProducts.value = buildMockServiceProducts()
allServiceProducts.value = [] as Array<HomeCareServiceProductType>
} finally {
serviceLoading.value = false
}
@@ -620,6 +554,8 @@ const hotProducts = ref<Product[]>([])
const recommendedProducts = ref<Product[]>([])
const hotKeywords = ref<string[]>([])
const defaultLoadLimit: number = 6
const recommendChannelLoadLimit: number = 16
const categoryChannelLoadLimit: number = 12
// 屏幕尺寸检测
const isMobile = ref(false)
@@ -922,10 +858,376 @@ function buildSimpleChannelCoverImages(startIndex: number): string[] {
return covers
}
function buildSimpleCategoryChannels(categoryId: string): SimpleCategoryChannel[] {
return []
function getRealProductImage(product: Product): string {
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[] {
const source = getRecommendMarketingChannels()
const visible: MarketingChannel[] = []
@@ -937,16 +1239,16 @@ function buildVisibleRecommendChannels(): MarketingChannel[] {
visible.push(channel)
}
return visible
}
}
function applyChannelDisplay(categoryId: string): void {
if (categoryId === 'recommend') {
marketingChannels.value = buildVisibleRecommendChannels()
categorySimpleChannels.value = []
marketingChannels.value = [] as Array<MarketingChannel>
categorySimpleChannels.value = [] as Array<SimpleCategoryChannel>
return
}
marketingChannels.value = []
categorySimpleChannels.value = buildSimpleCategoryChannels(categoryId)
marketingChannels.value = [] as Array<MarketingChannel>
categorySimpleChannels.value = [] as Array<SimpleCategoryChannel>
}
function buildChannelDetailUrl(channelId: string, routeType: string, categoryId: string): string {
@@ -1440,11 +1742,18 @@ async function loadHotProducts(page: number, limit: number): Promise<void> {
}
}
setHotProducts(products)
if (currentFeedCategoryId.value === 'recommend' && page <= 1) {
marketingChannels.value = buildRealRecommendMarketingChannels(products)
}
hasMore.value = result.hasmore
currentPage.value = page
} catch (error) {
console.error('加载热销商品失败:', error)
hotProducts.value = []
if (currentFeedCategoryId.value === 'recommend') {
console.log('[home-channel] fallback to mock channel data')
marketingChannels.value = buildVisibleRecommendChannels()
}
hasMore.value = false
}
}
@@ -1476,14 +1785,18 @@ async function loadCategoryGoods(categoryId: string): Promise<void> {
await syncCategoryLayout(categoryId)
if (categoryId === 'recommend') {
try {
const result = await supabaseService.getMedicalMallSmartRecommendations(1, defaultLoadLimit)
const result = await supabaseService.getMedicalMallSmartRecommendations(1, recommendChannelLoadLimit)
console.log('[home-channel] 推荐商品接口返回数量:', result.data.length)
failedProductImageIds.value = []
setHotProducts(result.data)
marketingChannels.value = buildRealRecommendMarketingChannels(result.data)
hasMore.value = result.hasmore
currentPage.value = 1
} catch (error) {
console.error('加载热销商品失败:', error)
hotProducts.value = []
console.log('[home-channel] fallback to mock channel data')
marketingChannels.value = buildVisibleRecommendChannels()
hasMore.value = false
}
} else {
@@ -1492,10 +1805,12 @@ async function loadCategoryGoods(categoryId: string): Promise<void> {
const result = await supabaseService.getMedicalMallProductsByCategory(categoryId, 1, defaultLoadLimit)
failedProductImageIds.value = []
setHotProducts(result.data)
await loadCategoryChannelCards(categoryId)
hasMore.value = result.hasmore
} catch (e) {
console.error('分类商品加载失败', e)
hotProducts.value = []
categorySimpleChannels.value = [] as Array<SimpleCategoryChannel>
hasMore.value = false
} finally {
loading.value = false
@@ -1520,12 +1835,16 @@ async function refreshHomeCategory(item: CategoryItem): Promise<void> {
secondaryCategoryDisplay.value = buildSecondaryCategoryDisplay(item.id)
applyChannelDisplay(item.id)
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)
marketingChannels.value = buildRealRecommendMarketingChannels(result.data)
hasMore.value = result.hasmore
} catch (error) {
console.error('加载推荐商品失败:', error)
hotProducts.value = []
console.log('[home-channel] fallback to mock channel data')
marketingChannels.value = buildVisibleRecommendChannels()
hasMore.value = false
} finally {
loading.value = false
@@ -1545,10 +1864,12 @@ async function refreshHomeCategory(item: CategoryItem): Promise<void> {
try {
const result = await supabaseService.getMedicalMallProductsByCategory(item.id, 1, defaultLoadLimit)
setHotProducts(result.data)
await loadCategoryChannelCards(item.id)
hasMore.value = result.hasmore
} catch (error) {
console.error('分类商品加载失败', error)
hotProducts.value = []
categorySimpleChannels.value = [] as Array<SimpleCategoryChannel>
hasMore.value = false
} finally {
loading.value = false
@@ -1857,7 +2178,8 @@ const switchSort = (sortId: string) => {
}
hasMore.value = true // 重置加载更多状态
// 重新加载热销商品,排序由 Supabase 服务处理
loadHotProducts(1, defaultLoadLimit)
const nextLimit = currentFeedCategoryId.value === 'recommend' ? recommendChannelLoadLimit : defaultLoadLimit
loadHotProducts(1, nextLimit)
}
// 切换筛选器
@@ -1909,12 +2231,13 @@ const loadMore = async () => {
showLoadMore.value = true
loading.value = true
try {
const pageLimit = currentFeedCategoryId.value === 'recommend' ? recommendChannelLoadLimit : defaultLoadLimit
const nextPage = currentPage.value + 1
const currentCount = hotProducts.value.length
console.log('开始加载更多,当前数量:', currentCount, '页码:', nextPage, '分类:', currentFeedCategoryId.value)
if (currentFeedCategoryId.value === 'recommend') {
const result = await fetchSortedProductsPage(nextPage, defaultLoadLimit)
const result = await fetchSortedProductsPage(nextPage, pageLimit)
const newProducts = result.data
if (newProducts.length == 0) {
@@ -2025,7 +2348,7 @@ const onScan = (): void => {
})
},
fail: (err) => {
console.error('扫码失败:', err)
console.error('扫码失败:', err)
}
})
}

View File

@@ -195,29 +195,12 @@
</scroll-view>
</view>
<view class="recommend-section">
<view class="recommend-header">
<text class="recommend-title">猜你喜欢</text>
<text class="recommend-subtitle">精选推荐,继续逛一逛</text>
</view>
<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>
<GuessYouLike
title="猜你喜欢"
:pageSize="8"
:loadMoreKey="guessLoadMoreKey"
@productClick="handleGuessProductClick"
/>
<view class="profile-bottom-safe"></view>
</view>
@@ -229,6 +212,7 @@
import { UserType } from '@/types/mall-types.uts'
import supabaseService from '@/utils/supabaseService.uts'
import { goToLogin } from '@/utils/utils.uts'
import GuessYouLike from '@/components/mall/GuessYouLike/GuessYouLike.uvue'
type UserStatsType = {
points: number
@@ -290,6 +274,9 @@ type PendingReceiptGoodsType = {
type ModalSuccessResult = { confirm: boolean; cancel: boolean }
export default {
components: {
GuessYouLike
},
data() {
return {
userInfo: {
@@ -345,6 +332,7 @@ export default {
recommendBottomLocked: false,
recommendViewportHeight: 0,
pageWindowHeight: 0,
guessLoadMoreKey: 0,
statusBarHeight: 0,
isAndroidApp: false,
capsuleTop: 0,
@@ -376,7 +364,6 @@ export default {
this.initPage()
this.loadUserProfile()
this.loadOrders()
this.loadRecommendProducts(true)
// 监听订单更新事件
uni.$on('orderUpdated', this.handleOrderUpdated)
@@ -474,24 +461,6 @@ export default {
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>> {
console.log('[profile推荐] fetchRecommendProducts 请求 page=', page, 'pageSize=', pageSize)
const result = await supabaseService.searchProducts('', page, pageSize, 'sales')
@@ -578,14 +547,7 @@ export default {
this.recommendInitialized = true
} catch (e) {
console.error('加载推荐商品失败:', e)
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
}
this.recommendHasMore = false
} finally {
this.recommendLoading = false
if (!reset && this.recommendPendingLoad && this.recommendHasMore) {
@@ -597,55 +559,20 @@ export default {
},
onRecommendScrollToLower() {
console.log('[profile推荐] scrolltolower 触发')
this.recommendBottomLocked = true
this.loadRecommendProducts(false)
this.guessLoadMoreKey = this.guessLoadMoreKey + 1
},
onRecommendScroll(event: any) {
if (this.recommendLoading || !this.recommendHasMore) {
return
},
handleGuessProductClick(productId: string) {
if (productId == null || productId === '') {
return
}
try {
const eventObj = this.toRecommendScrollJson(event)
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)
}
uni.navigateTo({
url: `/pages/mall/consumer/product-detail?id=${productId}&productId=${productId}`
})
},
resetGuestProfileState() {
@@ -1045,9 +972,6 @@ export default {
const userId = supabaseService.getCurrentUserId()
if (userId == null || userId === '') {
this.resetGuestProfileState()
if (!this.recommendInitialized) {
this.loadRecommendProducts(true)
}
return
}
@@ -1055,9 +979,6 @@ export default {
this.loadUserProfile()
this.loadOrders()
this.updateCouponCount() // 更新优惠券数量
if (!this.recommendInitialized) {
this.loadRecommendProducts(true)
}
},
async updateCouponCount() {
@@ -1545,13 +1466,14 @@ export default {
},
payOrder(order: OrderItemType) {
const paymentAmount = order.actual_amount
const userId = supabaseService.getCurrentUserId()
if (userId == null || userId === '') {
goToLogin(`/pages/mall/consumer/payment?orderId=${order.id}`)
goToLogin(`/pages/mall/consumer/payment?orderId=${order.id}&amount=${paymentAmount}`)
return
}
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>

View File

@@ -17,6 +17,15 @@
<input class="input" v-model="regionString" placeholder="省市区县、乡镇等" placeholder-class="placeholder" />
<text class="arrow-icon"></text>
</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">
<text class="label">详细地址</text>
<textarea class="textarea" v-model="formData.detail" placeholder="街道、楼牌号等" placeholder-class="placeholder" maxlength="100"></textarea>
@@ -85,6 +94,9 @@ type Address = {
detail: string
isDefault: boolean
label?: string
latitude?: number
longitude?: number
coordinateType?: string
}
const isEdit = ref(false)
@@ -92,6 +104,9 @@ const addressId = ref('')
const regionString = ref('')
const tags = ['家', '公司', '学校']
const smartInput = ref('')
const locationHint = ref('')
const latitude = ref(0)
const longitude = ref(0)
type AddressForm = {
name: string
@@ -120,6 +135,9 @@ const loadAddress = async (id: string) => {
formData.isDefault = address.is_default
formData.label = address.label ?? ''
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 {
// 如果Supabase没有找到尝试从本地存储加载
const storedAddresses = uni.getStorageSync('addresses')
@@ -133,6 +151,9 @@ const loadAddress = async (id: string) => {
formData.isDefault = localAddress.isDefault
formData.label = localAddress.label ?? ''
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.label = address.label ?? ''
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) {
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) => {
if (options == null) return
const optionsObj = options as UTSJSONObject
@@ -216,7 +276,10 @@ const saveAddress = async () => {
detail_address: formData.detail,
postal_code: '', // 如果需要可以添加邮政编码字段
is_default: formData.isDefault,
label: formData.label
label: formData.label,
latitude: latitude.value,
longitude: longitude.value,
coordinate_type: 'gcj02'
} as AddAddressParams
let success = false
@@ -231,8 +294,11 @@ const saveAddress = async () => {
district: district,
detail_address: formData.detail,
postal_code: '',
is_default: formData.isDefault,
label: formData.label
is_default: formData.isDefault,
label: formData.label,
latitude: latitude.value,
longitude: longitude.value,
coordinate_type: 'gcj02'
} as UpdateAddressParams
success = await supabaseService.updateAddress(addressId.value, updateData)
} else {
@@ -271,7 +337,10 @@ const saveAddress = async () => {
district: district,
detail: formData.detail,
isDefault: formData.isDefault,
label: formData.label
label: formData.label,
latitude: latitude.value,
longitude: longitude.value,
coordinateType: 'gcj02'
}
}
} else {
@@ -284,7 +353,10 @@ const saveAddress = async () => {
district: district,
detail: formData.detail,
isDefault: formData.isDefault,
label: formData.label
label: formData.label,
latitude: latitude.value,
longitude: longitude.value,
coordinateType: 'gcj02'
}
addresses.push(newAddress)
}
@@ -437,6 +509,38 @@ const deleteAddress = () => {
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 {
margin-bottom: 8px;
}

View File

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

View File

@@ -149,6 +149,13 @@ async function loadData() {
bookingDays.value = getBookingDayOptions()
bookingSlots.value = getBookingTimeSlots()
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) {
@@ -192,6 +199,10 @@ function selectSlot(slotId: string, available: boolean) {
}
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 == '') {
uni.showToast({ title: '请补全申请信息', icon: 'none' })
return
@@ -200,6 +211,10 @@ async function submitApplication() {
const parsedAge = parseInt(ageText.value)
form.age = isNaN(parsedAge) ? 0 : parsedAge
const created = await createHomeServiceApplication(form)
if (created == null) {
uni.showToast({ title: '申请提交失败,请检查登录和预约信息', icon: 'none' })
return
}
uni.showToast({ title: '申请已提交', icon: 'success' })
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 { fetchConsumerAcceptanceDetail, submitConsumerAcceptance } from '@/services/homeServiceService.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 detail = ref<HomeServiceAcceptanceType | null>(null)
@@ -62,17 +64,31 @@ const selectedTags = ref<Array<string>>([])
const scores = [1, 2, 3, 4, 5]
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) => {
const id = options['id']
if (id != null) {
caseId.value = id as string
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)
ensureLogin().then((ok) => {
if (!ok) {
return
}
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) {
if (!(await ensureLogin())) {
return
}
if (caseId.value == '' || feedback.value == '') {
uni.showToast({ title: '请填写反馈说明', icon: 'none' })
return

View File

@@ -89,6 +89,8 @@ import ServicePanel from '@/components/homeService/ServicePanel.uvue'
import ServiceStatusTag from '@/components/homeService/ServiceStatusTag.uvue'
import { fetchConsumerHomeServiceCases, fetchHomeServiceCatalog } from '@/services/homeServiceService.uts'
import { HomeServiceCatalogType, HomeServiceCaseType } from '@/types/home-service.uts'
import { getCurrentUser, getCurrentUserId } from '@/utils/store.uts'
import { goToLogin } from '@/utils/utils.uts'
import {
HomeServiceCategoryType,
HomeServiceItemType,
@@ -125,9 +127,23 @@ async function loadData() {
categoryGrid.value = getHomeServiceCategories()
promoCards.value = getHomeServicePromoCards()
services.value = await fetchHomeServiceCatalog()
const user = await getCurrentUser()
if (user == null || getCurrentUserId() == '') {
cases.value = [] as Array<HomeServiceCaseType>
return
}
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) {
selectedCategory.value = categoryId
}
@@ -141,11 +157,21 @@ function goDetail(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) {
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) {

View File

@@ -59,7 +59,17 @@
</view>
</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>
</ServicePanel>
@@ -82,14 +92,29 @@ import ServiceStatusTag from '@/components/homeService/ServiceStatusTag.uvue'
import ServiceTimeline from '@/components/homeService/ServiceTimeline.uvue'
import { fetchConsumerHomeServiceCaseDetail } from '@/services/homeServiceService.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 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() {
if (caseId.value == '') {
return
}
if (!(await ensureLogin())) {
detail.value = null
return
}
detail.value = await fetchConsumerHomeServiceCaseDetail(caseId.value)
}
@@ -97,7 +122,12 @@ function goFeedback() {
if (caseId.value == '') {
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() {

View File

@@ -28,12 +28,20 @@
</view>
<ServicePanel title="Step1 服务地址" subtitle="确认上门地址、楼层与入户条件。">
<view class="form-item">
<text class="label">上门地址</text>
<view class="value-card" @click="selectAddress">
<text class="value-card-text">{{ addressLineText }}</text>
<text class="value-card-action">点击更换</text>
<view class="service-address-card" @click="selectAddress">
<view class="service-address-main">
<text class="service-address-title">服务地址</text>
<view v-if="selectedAddress != null" class="service-address-detail">
<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>
<text class="service-address-arrow">></text>
</view>
<view class="form-grid">
<view class="form-grid-item">
@@ -271,11 +279,13 @@
<script setup lang="uts">
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 ServicePanel from '@/components/homeService/ServicePanel.uvue'
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 {
BookingDayOptionType,
BookingTimeSlotType,
@@ -390,6 +400,7 @@ const servicePackages = ref<Array<ServicePackageOptionType>>([])
const selectedDayId = ref('day-1')
const selectedSlotId = ref('slot-1')
const selectedAddress = ref<HomeServiceSelectedAddressType | null>(null)
const careAddress = ref(defaultCareAddress.address)
const careDetailAddress = ref(defaultCareAddress.detailAddress)
@@ -499,10 +510,56 @@ function buildDefaultPackages(currentServiceId: string, basePrice: number, durat
}
const addressLineText = computed((): string => {
if (selectedAddress.value != null && selectedAddress.value.fullAddress != '') {
return selectedAddress.value.fullAddress
}
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 => {
let selectedDayLabel = ''
for (let i = 0; i < bookingDays.value.length; i++) {
@@ -611,9 +668,20 @@ function buildDemandSummary(): string {
+ ',服务需求:' + selectedNeeds
+ ',详细需求:' + demandDetailText.value
+ ',紧急联系人:' + emergencyName.value + ' ' + emergencyPhone.value
+ ',服务地址备注:' + (selectedAddress.value != null ? selectedAddress.value.remark : '')
+ ',备注:' + 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() {
bookingDays.value = getBookingDayOptions()
bookingSlots.value = getBookingTimeSlots()
@@ -633,15 +701,7 @@ async function loadData() {
}
}
if (matchedService == null) {
const fallbackItems = getHomeServiceItems(catalog)
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)
setUnavailableServiceState()
return
}
serviceTitle.value = matchedService.name
@@ -656,17 +716,22 @@ async function loadData() {
servicePackages.value = buildDefaultPackages(serviceId.value, servicePrice.value, serviceDuration.value)
}
function selectAddress() {
if (careAddress.value == '' || careDetailAddress.value == '') {
careAddress.value = defaultCareAddress.address
careDetailAddress.value = defaultCareAddress.detailAddress
careArea.value = defaultCareAddress.area
careFloor.value = defaultCareAddress.floor
careHasElevator.value = defaultCareAddress.hasElevator
uni.showToast({ title: '已填入示例地址', icon: 'none' })
return
async function ensureLogin(): Promise<boolean> {
const user = await getCurrentUser()
if (user == null || getCurrentUserId() == '') {
goToLogin('/pages/mall/consumer/home-service/service-detail?id=' + serviceId.value + '&mode=booking')
return false
}
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() {
@@ -746,8 +811,15 @@ function selectServicePackage(packageId: string) {
}
async function submitBooking() {
if (careAddress.value == '' || careDetailAddress.value == '') {
uni.showToast({ title: '请选择上门服务地址', icon: 'none' })
if (!(await ensureLogin())) {
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
}
if (recipientName.value == '') {
@@ -781,10 +853,15 @@ async function submitBooking() {
phone: contactPhone.value,
address: addressLineText.value,
preferredTime: selectedTimeText.value,
demandSummary: buildDemandSummary()
demandSummary: buildDemandSummary(),
serviceAddressSnapshot: selectedAddress.value
}
const created = await createHomeServiceApplication(draft)
if (created == null) {
uni.showToast({ title: '预约提交失败,请稍后重试', icon: 'none' })
return
}
uni.showToast({ title: '预约已提交', icon: 'success' })
uni.navigateTo({ url: '/pages/mall/consumer/home-service/order-detail?id=' + created.id })
}
@@ -796,12 +873,14 @@ onLoad((options) => {
}
const mode = options['mode']
if (mode != null && mode == 'booking') {
careAddress.value = defaultCareAddress.address
careDetailAddress.value = defaultCareAddress.detailAddress
careArea.value = defaultCareAddress.area
}
loadData()
})
onShow(() => {
loadCachedSelectedAddress()
})
</script>
<style scoped>
@@ -845,6 +924,81 @@ onLoad((options) => {
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-tag-row,
.summary-price-row,

View File

@@ -9,6 +9,7 @@
<view class="status-title-row">
<text class="status-emoji">{{ getStatusIcon() }}</text>
<text class="status-text">{{ getStatusText() }}</text>
<text v-if="getPendingCountdownText() != ''" class="status-countdown">{{ getPendingCountdownText() }}</text>
</view>
<text class="status-desc">{{ getStatusDesc() }}</text>
</view>
@@ -135,7 +136,7 @@
</view>
</view>
<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 primary" @click="payOrder">立即支付</button>
</view>
@@ -157,9 +158,11 @@
<button class="btn primary" @click="goToReview">评价订单</button>
</view>
<view v-if="order?.order_status === 5" class="btn-group">
<button class="btn primary" @click="rePurchase">重新购买</button>
</view>
<view v-if="shouldShowCancelledActions()" class="btn-group">
<button class="btn" @click="deleteOrder">删除订单</button>
<button class="btn" @click="viewSimilar">看相似</button>
<button class="btn primary" @click="rePurchase">再次购买</button>
</view>
</view>
</view>
</view>
@@ -167,16 +170,21 @@
</template>
<script setup lang="uts">
import { ref, onMounted, computed } from 'vue'
import { onLoad, onBackPress } from '@dcloudio/uni-app'
import { ref } from 'vue'
import { onBackPress, onHide, onLoad, onShow, onUnload } from '@dcloudio/uni-app'
import { supabaseService } from '@/utils/supabaseService.uts'
import { goToLogin } from '@/utils/utils.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 = {
order_no: string,
order_status: number,
payment_status: number,
cancel_reason: string,
pay_expire_at: string,
consumer_deleted_at: string,
total_amount: number,
product_amount: number,
shipping_fee: number,
@@ -221,10 +229,51 @@ const orderItems = ref<OrderItemType[]>([])
const shopName = ref('店铺名称')
const deliveryAddress = ref<AddressType | 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 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 == 2) return '待发货'
if (status == 3) return '待收货'
@@ -237,6 +286,20 @@ const getStatusText = (): string => {
const getStatusDesc = (): string => {
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 == 2) return '商家正在打包商品'
if (status == 3) return '商品正在赶往您的地址'
@@ -249,6 +312,7 @@ const getStatusDesc = (): string => {
const getStatusIcon = (): string => {
const status = order.value?.order_status ?? 0
if (shouldShowCancelledActions()) return '⏰'
if (status === 1) return '💳'
if (status === 2) return '📦'
if (status === 3) return '🚚'
@@ -257,8 +321,14 @@ const getStatusIcon = (): 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
return `status-${status}`
return 'status-' + status
}
const getFullAddress = (addr: any): string => {
@@ -389,6 +459,10 @@ const loadOrderDetail = async () => {
order.value = {
order_no: (dataObj.get('order_no') ?? '') as string,
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,
product_amount: (dataObj.get('product_amount') ?? 0) as number,
shipping_fee: (dataObj.get('shipping_fee') ?? 0) as number,
@@ -424,6 +498,15 @@ const loadOrderDetail = async () => {
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')
console.log('[loadOrderDetail] 收货地址数据:', addressRaw)
@@ -470,6 +553,7 @@ const loadOrderDetail = async () => {
}
console.log('[loadOrderDetail] 订单详情加载成功,商品数量:', orderItems.value.length)
syncTimeoutState()
} else {
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 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 userId = supabaseService.getCurrentUserId()
if (userId == null || userId === '') {
goToLogin(`/pages/mall/consumer/payment?orderId=${orderId.value}&amount=${totalAmount}`)
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({
url: `/pages/mall/consumer/payment?orderId=${orderId.value}&amount=${totalAmount}`
})
@@ -522,23 +671,14 @@ const payOrder = () => {
const doCancelOrder = async () => {
try {
const updatePayload = new UTSJSONObject()
updatePayload.set('order_status', 5)
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) {
const result = await supabaseService.cancelOrder(orderId.value)
if (result) {
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: '订单已取消' })
} else {
console.error('[doCancelOrder] 取消订单失败:', result.error)
uni.showToast({ title: '取消失败', icon: 'none' })
}
} 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 = () => {
uni.showModal({
title: '提示',
@@ -801,6 +969,21 @@ onLoad((options) => {
}
})
onShow(() => {
if (orderId.value != '') {
loadOrderDetail()
}
startDetailTicker()
})
onHide(() => {
stopDetailTicker()
})
onUnload(() => {
stopDetailTicker()
})
</script>
<style scoped>
@@ -868,12 +1051,26 @@ onLoad((options) => {
letter-spacing: 1px;
}
.status-countdown {
font-size: 18px;
font-weight: bold;
margin-left: 8px;
}
.status-desc {
font-size: 14px;
opacity: 0.95;
text-align: center;
}
.status-pending {
background: linear-gradient(135deg, #ff9000, #ff5000);
}
.status-cancelled {
background: linear-gradient(135deg, #9aa5b1, #6b7280);
}
/* 分享免单入口 */
.share-free-entry {
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 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 之前)
const loadOrderInfo = async () => {
try {
@@ -60,12 +81,12 @@ onLoad((options) => {
if (options == null) return
const optionsObj = options as UTSJSONObject
const orderIdValue = optionsObj.getString('orderId') ?? ''
const orderIdValue = getOptionString(optionsObj, 'orderId')
if (orderIdValue != '') {
orderId.value = orderIdValue
orderNo.value = orderIdValue
const amountValue = optionsObj.getString('amount') ?? ''
const amountValue = getOptionString(optionsObj, 'amount')
if (amountValue != '') {
const amountStr = amountValue
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-navbar" :style="navbarStyle">
<view class="back-btn" @click="goBack">
<text class="back-icon"></text>
<text class="back-icon"><</text>
</view>
<view class="jd-search-box">
@@ -12,7 +12,7 @@
:value="searchKeyword"
@input="onInput"
@confirm="onSearch"
:placeholder="(placeholderKeyword != null && placeholderKeyword !== '' && searchKeyword === '') ? placeholderKeyword : '请输入商品名称、店铺'"
:placeholder="(placeholderKeyword != null && placeholderKeyword !== '' && searchKeyword === '') ? placeholderKeyword : '请输入商品名称、店铺'"
placeholder-class="jd-placeholder"
:focus="autoFocus"
confirm-type="search"
@@ -23,7 +23,7 @@
</view>
<view class="camera-wrap" @tap.stop="handleCameraSearch">
<text class="camera-icon">📷</text>
<text class="camera-icon"></text>
</view>
<view class="search-submit-btn" @tap.stop="onSearch">
@@ -32,23 +32,25 @@
</view>
</view>
<!-- 错误状态(模拟服务器超时) -->
<!-- 错误状态 -->
<view v-if="isError" class="error-state" @click="retryLoad">
<view class="error-content">
<text class="error-icon">⚠️</text>
<text class="error-title">加载服务超时</text>
<text class="error-icon">!</text>
<text class="error-title">加载服务超时</text>
<text class="error-desc">请点击屏幕重试</text>
</view>
</view>
<!-- 主内容区域:使用 scroll-view 支持安卓端滚动 -->
<!-- 涓诲唴瀹瑰尯鍩燂細浣跨敤 scroll-view 鏀寔瀹夊崜绔粴鍔?-->
<scroll-view
v-else
class="main-content"
direction="vertical"
:show-scrollbar="false"
>
<!-- 初始状态(无搜索词) -->
v-else
class="main-content"
direction="vertical"
:show-scrollbar="false"
:lower-threshold="120"
@scrolltolower="handleMainScrollToLower"
>
<!-- 初始状态 -->
<view v-if="searchKeyword == '' && showResults == false">
<!-- 搜索历史 -->
<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-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 class="guess-you-like">
<view class="section-header">
<view class="title-with-icon">
<text class="section-icon">✨</text>
<text class="section-title">猜你喜欢</text>
</view>
<text class="refresh-btn" @click="refreshGuessList">换一批</text>
</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>
<GuessYouLike
title="猜你喜欢"
:pageSize="8"
:loadMoreKey="guessLoadMoreKey"
@productClick="goToProductDetail"
/>
</view>
</view>
</view>
<!-- 搜索建议 -->
<view v-if="searchKeyword != '' && showResults == false" class="search-suggestions">
@@ -152,7 +124,7 @@
class="suggestion-item"
@click="selectSuggestion(suggestion)"
>
<view class="suggestion-icon">🔍</view>
<view class="suggestion-icon"></view>
<text class="suggestion-text">{{ suggestion }}</text>
</view>
</view>
@@ -176,7 +148,7 @@
<image class="shop-logo" :src="shop.logo" mode="aspectFill" />
<view class="shop-info">
<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>
@@ -195,13 +167,13 @@
class="filter-tab"
:class="{ active: activeSort === 'sales' }"
@click="switchSort('sales')"
>销量</text>
>销量</text>
<text
class="filter-tab"
:class="{ active: activeSort === 'price' }"
@click="switchSort('price')"
>
价格 {{ activeSort === 'price' ? (priceSortAsc ? '↑' : '↓') : '' }}
价格 {{ activeSort === 'price' ? (priceSortAsc ? '↑' : '↓') : '' }}
</text>
</view>
</view>
@@ -235,14 +207,14 @@
</view>
</view>
<!-- 空结果 - 仅在非加载状态且无结果时显示 -->
<!-- 空结果 -->
<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-sub">换个关键词试试吧</text>
</view>
<!-- 加载更多/加载中 - 在加载状态或有更多数据时显示 -->
<!-- 加载更多 -->
<view v-if="loading" class="loading-more">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
@@ -252,41 +224,14 @@
<text class="no-more-text">--- 到底了 ---</text>
</view>
<view v-if="searchResults.length > 0 && recommendList.length > 0" class="guess-you-like" style="margin-top: 16rpx;">
<view class="section-header">
<view class="title-with-icon">
<text class="section-icon">✨</text>
<text class="section-title">猜你喜欢</text>
</view>
</view>
<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 v-if="searchResults.length > 0" class="guess-you-like" style="margin-top: 16rpx;">
<GuessYouLike
title="猜你喜欢"
:pageSize="8"
:excludeProductIds="searchResultProductIds"
:loadMoreKey="guessLoadMoreKey"
@productClick="goToProductDetail"
/>
</view>
</view>
@@ -302,6 +247,7 @@ import { onPageScroll, onReachBottom } from '@dcloudio/uni-app'
import { supabaseService } from '@/utils/supabaseService.uts'
import type { Product } from '@/utils/supabaseService.uts'
import { goToLogin } from '@/utils/utils.uts'
import GuessYouLike from '@/components/mall/GuessYouLike/GuessYouLike.uvue'
// 状态定义
const statusBarHeight = ref(0)
@@ -316,6 +262,7 @@ const loading = ref(false)
const hasMore = ref(true)
const isError = ref(false)
const autoFocus = ref(true)
const guessLoadMoreKey = ref<number>(0)
const navbarStyle = computed(() => {
const top = navBarTop.value > 0 ? navBarTop.value : statusBarHeight.value
@@ -330,7 +277,6 @@ const handleCameraSearch = () => {
const activeSort = ref('default') // 当前排序方式: default, sales, price
const priceSortAsc = ref(false) // 价格排序是否为升序
type HotSearchItemType = {
keyword: string
hot: boolean
@@ -436,21 +382,21 @@ const guessList = ref<Array<GuessItemType>>([])
const allGuessItems = ref<Array<GuessItemType>>([])
const DEFAULT_HOT_KEYWORDS: Array<string> = [
'大疆neo2',
'澶х枂neo2',
'iPhone 15 Pro',
'Nike Air Max 270',
'厨具',
'老干妈',
'钢化膜',
'手机壳',
'零食坚果',
'新鲜水果',
'液态硅胶壳',
'充电宝',
'蓝牙耳机'
'厨具',
'老干妈',
'钢化膜',
'手机壳',
'零食坚果',
'新鲜水果',
'液态硅胶壳',
'充电宝',
'蓝牙耳机'
]
// 推荐商品区(用于 “猜你喜欢/推荐商品”)
// 推荐商品区
const recommendList = ref<Array<GuessItemType>>([])
const recommendPool = ref<Array<GuessItemType>>([])
const recommendPage = ref(0)
@@ -459,6 +405,16 @@ const recommendAppendSize = ref(20)
const loadingRecommend = ref(false)
const searchResults = ref<Array<SearchResultType>>([])
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'
@@ -504,8 +460,8 @@ const MAX_EXPANDED_COUNT = 24
const clearHistory = () => {
uni.showModal({
title: '提示',
content: '确定清空搜索历史吗?',
title: '提示',
content: '确定清空搜索历史吗?',
success: (res) => {
if (res.confirm) {
searchHistory.value = []
@@ -525,8 +481,6 @@ const deleteHistoryItem = (index: number) => {
const toggleHistoryEdit = () => { isEditMode.value = !isEditMode.value }
const toggleHistoryExpanded = () => { historyExpanded.value = !historyExpanded.value }
import { computed } from 'vue'
const visibleHistory = computed(() => {
if (historyExpanded.value) {
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)
hotProducts = hotResult as Product[]
} catch (hotError) {
console.error('获取热销商品失败,使用空列表:', hotError)
console.error('获取热销商品失败,使用空列表:', hotError)
hotProducts = []
}
@@ -656,7 +610,7 @@ const loadData = async (): Promise<void> => {
}
const retryLoad = () => {
uni.showLoading({ title: '重新加载中' })
uni.showLoading({ title: '重新加载中...' })
setTimeout(() => {
uni.hideLoading()
loadData()
@@ -774,7 +728,7 @@ const performSearch = async (): Promise<void> => {
image: p.main_image_url ?? '/static/default.jpg',
price: p.base_price ?? 0,
marketPrice: formatMarketPriceText(p),
specification: p.specification ?? '标准规格',
specification: p.specification ?? '标准规格',
tag: tag,
sales: p.sale_count ?? 0,
salesText: formatSalesText(p),
@@ -848,7 +802,7 @@ const initPage = () => {
}
}
} catch (e) {
console.error('初始化失败', e)
console.error('初始化失败:', e)
isError.value = true
}
}
@@ -891,16 +845,16 @@ const onSearch = () => {
}
if (effective === '') return
addToHistory(effective)
// 如果搜索词来自 placeholder保输入框为空但执行搜索
// 如果搜索词来自 placeholder输入框为空但执行搜索
if (userInput === '') {
// 保持 searchKeyword 为空 but perform search with effective
// 保持 searchKeyword 为空,但使用 effective 搜索
searchKeyword.value = ''
}
// 将 searchKeyword 临时设置为 effective 以便 performSearch 使用performSearch 使用 searchKeyword
// 临时将 searchKeyword 设置为 effective,供 performSearch 使用
const prev = searchKeyword.value
searchKeyword.value = effective
performSearch()
// 恢复输入框为空状态(如果用户未输入)
// 如果用户没有手动输入,则恢复为空
if (userInput === '') searchKeyword.value = prev
}
@@ -934,12 +888,12 @@ const switchSort = (type: string) => {
priceSortAsc.value = !priceSortAsc.value
} else {
activeSort.value = 'price'
priceSortAsc.value = true // 默认升序
priceSortAsc.value = true // 默认升序
}
} else {
activeSort.value = type
}
// 重新执行搜索以获取正确排序的数据
// 重新搜索以获取正确排序的数据
performSearch()
}
@@ -984,7 +938,7 @@ const loadMore = async (): Promise<void> => {
image: p.main_image_url ?? '/static/default.jpg',
price: p.base_price ?? 0,
marketPrice: formatMarketPriceText(p),
specification: p.specification ?? '标准规格',
specification: p.specification ?? '标准规格',
tag: tag,
sales: p.sale_count ?? 0,
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 {
if (showResults.value === true) {
loadMore()
if (hasMore.value) {
loadMore()
return
}
guessLoadMoreKey.value = guessLoadMoreKey.value + 1
return
}
loadRecommendGoods(recommendAppendSize.value)
guessLoadMoreKey.value = guessLoadMoreKey.value + 1
}
function handleMainScrollToLower(): void {
handleReachBottom()
}
onReachBottom(() => {
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) => {
uni.navigateTo({
url: `/pages/mall/consumer/shop-detail?id=${shop.id}`
@@ -1077,40 +990,40 @@ const addToCart = async (product: SearchResultType | GuessItemType) => {
goToLogin('/pages/mall/consumer/search')
return
}
uni.showLoading({ title: '检查商品...' })
uni.showLoading({ title: '检查商品...' })
try {
// 统一转换为 UTSJSONObject 访问属性
const prodObj = JSON.parse(JSON.stringify(product)) as UTSJSONObject
// 统一转换为 UTSJSONObject 访问属性
const prodObj = JSON.parse(JSON.stringify(product)) as UTSJSONObject
const productId = prodObj.getString('id') ?? ''
const merchantId = prodObj.getString('merchant_id') ?? ''
// 检查商品是否有SKU
// 检查商品是否有 SKU
const skus = await supabaseService.getProductSkus(productId)
uni.hideLoading()
if (skus.length > 0) {
// 有规格,提示并跳转到商品详情选择规格
uni.showToast({ title: '请选择规格', icon: 'none' })
// 有规格时跳商品详情选择规格
uni.showToast({ title: '请选择规格', icon: 'none' })
setTimeout(() => {
uni.navigateTo({
url: '/pages/mall/consumer/product-detail?id=' + productId
})
}, 500)
} else {
// 无规格直接加入购物车
uni.showLoading({ title: '添加中...' })
// 无规格直接加入购物车
uni.showLoading({ title: '添加中...' })
const success = await supabaseService.addToCart(productId, 1, '', merchantId)
uni.hideLoading()
if (success) {
uni.showToast({ title: '已添加到购物车', icon: 'success' })
uni.showToast({ title: '已添加到购物车', icon: 'success' })
} else {
uni.showToast({ title: '添加失败,请先登录', icon: 'none' })
uni.showToast({ title: '添加失败,请先登录', icon: 'none' })
}
}
} catch (e) {
console.error('添加到购物车异常', e)
console.error('添加到购物车异常', e)
uni.hideLoading()
uni.showToast({ title: '操作异常', icon: 'none' })
uni.showToast({ title: '操作异常', icon: 'none' })
}
}
@@ -1119,31 +1032,31 @@ const openCamera = () => {
count: 1,
sourceType: ['camera'],
success: (res) => {
console.log('拍摄图片路径', (res.tempFilePaths as string[])[0])
uni.showToast({ title: '已启用相机', icon: 'none' })
console.log('拍摄图片路径:', (res.tempFilePaths as string[])[0])
uni.showToast({ title: '已启用相机', icon: 'none' })
},
fail: (err) => {
console.error('启用相机失败', err)
console.error('鍚敤鐩告満澶辫触', err)
}
})
}
const goBack = () => {
if (showResults.value) {
// 如果在搜索结果页,先返回搜索初始页
showResults.value = false
searchKeyword.value = ''
} else {
// 如果在搜索初始页,则返回上一页
const pages = getCurrentPages()
if (pages.length > 1) {
uni.navigateBack()
} else {
// 如果只有一页(由于深链接或重定向),返回首页
uni.switchTab({
url: '/pages/main/index'
})
}
if (showResults.value) {
// 如果在搜索结果页,先返回搜索初始页
showResults.value = false
searchKeyword.value = ''
} else {
// 如果在搜索初始页,则返回上一页
const pages = getCurrentPages()
if (pages.length > 1) {
uni.navigateBack()
} else {
// 如果只有一个页面,则回首页
uni.switchTab({
url: '/pages/main/index'
})
}
}
}
</script>
@@ -1158,7 +1071,7 @@ const goBack = () => {
min-height: 100%;
}
/* 店铺搜索结果 */
/* 搴楅摵鎼滅储缁撴灉 */
.shop-results-section {
background-color: #fff;
margin-bottom: 10px;
@@ -1486,7 +1399,7 @@ const goBack = () => {
margin-left: 6rpx;
}
/* 猜你需要 */
/* 鐚滀綘闇€瑕?*/
.guess-you-like {
margin-bottom: 24rpx;
background-color: #ffffff;
@@ -1840,7 +1753,7 @@ const goBack = () => {
font-weight: bold;
}
/* 错误状态 */
/* 閿欒鐘舵€?*/
.error-state {
flex: 1;
display: flex;
@@ -1872,7 +1785,7 @@ const goBack = () => {
color: #999;
}
/* 加载更多 */
/* 鍔犺浇鏇村 */
.loading-more {
padding: 20px 0;
display: flex;
@@ -1932,3 +1845,5 @@ const goBack = () => {
}
</style>