继续补充功能页面,consumer模块完成度70%
This commit is contained in:
42
pages.json
42
pages.json
@@ -39,6 +39,48 @@
|
|||||||
"navigationBarTitleText": "搜索",
|
"navigationBarTitleText": "搜索",
|
||||||
"navigationStyle": "custom"
|
"navigationStyle": "custom"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/mall/consumer/product-detail",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "商品详情"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/mall/consumer/shop-detail",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "店铺详情"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/mall/consumer/coupons",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "我的优惠券"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/mall/consumer/favorites",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "我的收藏"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/mall/consumer/footprint",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "我的足迹"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/mall/consumer/address-list",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "收货地址"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/mall/consumer/address-edit",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "编辑地址"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"tabBar": {
|
"tabBar": {
|
||||||
|
|||||||
@@ -1,793 +1,319 @@
|
|||||||
<!-- 地址编辑页面 -->
|
|
||||||
<template>
|
<template>
|
||||||
<view class="address-edit-page">
|
<view class="address-edit-page">
|
||||||
<!-- 顶部栏 -->
|
<view class="form-group">
|
||||||
<view class="edit-header">
|
|
||||||
<text class="back-btn" @click="goBack">‹</text>
|
|
||||||
<text class="header-title">{{ addressId ? '编辑地址' : '新增地址' }}</text>
|
|
||||||
<text v-if="addressId" class="delete-btn" @click="deleteAddress">删除</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<scroll-view class="edit-content" scroll-y>
|
|
||||||
<!-- 表单 -->
|
|
||||||
<view class="form-section">
|
|
||||||
<view class="form-item">
|
<view class="form-item">
|
||||||
<text class="item-label">收货人</text>
|
<text class="label">收货人</text>
|
||||||
<input class="item-input"
|
<input class="input" v-model="formData.name" placeholder="请填写收货人姓名" />
|
||||||
v-model="formData.recipient_name"
|
|
||||||
placeholder="请输入收货人姓名" />
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="form-item">
|
<view class="form-item">
|
||||||
<text class="item-label">手机号码</text>
|
<text class="label">手机号码</text>
|
||||||
<input class="item-input"
|
<input class="input" v-model="formData.phone" type="number" maxlength="11" placeholder="请填写手机号码" />
|
||||||
v-model="formData.phone"
|
|
||||||
placeholder="请输入手机号码"
|
|
||||||
type="number"
|
|
||||||
maxlength="11" />
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="form-item" @click="showRegionPicker">
|
|
||||||
<text class="item-label">所在地区</text>
|
|
||||||
<text class="item-value">{{ regionText || '请选择省市区' }}</text>
|
|
||||||
<text class="item-arrow">›</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="form-item">
|
<view class="form-item">
|
||||||
<text class="item-label">详细地址</text>
|
<text class="label">所在地区</text>
|
||||||
<textarea class="item-textarea"
|
<input class="input" v-model="regionString" placeholder="省市区县、乡镇等" />
|
||||||
v-model="formData.detail"
|
|
||||||
placeholder="请输入详细地址,如街道、小区、楼栋号、单元室等"
|
|
||||||
maxlength="100" />
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="form-item">
|
<view class="form-item">
|
||||||
<text class="item-label">邮政编码</text>
|
<text class="label">详细地址</text>
|
||||||
<input class="item-input"
|
<input class="input" v-model="formData.detail" placeholder="街道、楼牌号等" />
|
||||||
v-model="formData.postal_code"
|
</view>
|
||||||
placeholder="请输入邮政编码"
|
|
||||||
type="number"
|
|
||||||
maxlength="6" />
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<view class="form-group">
|
||||||
<view class="form-item">
|
<view class="form-item">
|
||||||
<view class="default-switch">
|
<text class="label">标签</text>
|
||||||
<text class="switch-label">设为默认地址</text>
|
<view class="tags-container">
|
||||||
<switch :checked="formData.is_default"
|
<text
|
||||||
@change="toggleDefault" />
|
v-for="tag in tags"
|
||||||
|
:key="tag"
|
||||||
|
class="tag-item"
|
||||||
|
:class="{ active: formData.label === tag }"
|
||||||
|
@click="selectTag(tag)"
|
||||||
|
>{{ tag }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
<view class="form-item switch-item">
|
||||||
|
<text class="label">设为默认收货地址</text>
|
||||||
|
<switch :checked="formData.isDefault" color="#ff5000" @change="onSwitchChange" />
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 地址簿 -->
|
<view class="footer-btn">
|
||||||
<view v-if="addressList.length > 0" class="address-book">
|
<button class="save-btn" @click="saveAddress">保存</button>
|
||||||
<view class="section-title">地址簿</view>
|
<button v-if="isEdit" class="delete-btn" @click="deleteAddress">删除收货地址</button>
|
||||||
<view v-for="address in addressList"
|
|
||||||
:key="address.id"
|
|
||||||
class="book-item"
|
|
||||||
@click="fillFromAddressBook(address)">
|
|
||||||
<view class="book-info">
|
|
||||||
<text class="book-name">{{ address.recipient_name }}</text>
|
|
||||||
<text class="book-phone">{{ address.phone }}</text>
|
|
||||||
</view>
|
|
||||||
<text class="book-address">{{ getFullAddress(address) }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</scroll-view>
|
|
||||||
|
|
||||||
<!-- 保存按钮 -->
|
|
||||||
<view class="save-btn-container">
|
|
||||||
<button class="save-btn" @click="saveAddress">保存地址</button>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 地区选择器 -->
|
|
||||||
<view v-if="showPicker" class="region-picker">
|
|
||||||
<view class="picker-mask" @click="hideRegionPicker"></view>
|
|
||||||
<view class="picker-content">
|
|
||||||
<view class="picker-header">
|
|
||||||
<text class="cancel-btn" @click="hideRegionPicker">取消</text>
|
|
||||||
<text class="picker-title">选择地区</text>
|
|
||||||
<text class="confirm-btn" @click="confirmRegion">确定</text>
|
|
||||||
</view>
|
|
||||||
<picker-view class="picker-view"
|
|
||||||
:value="pickerValue"
|
|
||||||
@change="onPickerChange">
|
|
||||||
<picker-view-column>
|
|
||||||
<view v-for="(province, index) in provinces"
|
|
||||||
:key="index"
|
|
||||||
class="picker-item">
|
|
||||||
{{ province.name }}
|
|
||||||
</view>
|
|
||||||
</picker-view-column>
|
|
||||||
<picker-view-column>
|
|
||||||
<view v-for="(city, index) in cities"
|
|
||||||
:key="index"
|
|
||||||
class="picker-item">
|
|
||||||
{{ city.name }}
|
|
||||||
</view>
|
|
||||||
</picker-view-column>
|
|
||||||
<picker-view-column>
|
|
||||||
<view v-for="(district, index) in districts"
|
|
||||||
:key="index"
|
|
||||||
class="picker-item">
|
|
||||||
{{ district.name }}
|
|
||||||
</view>
|
|
||||||
</picker-view-column>
|
|
||||||
</picker-view>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, reactive, computed } from 'vue'
|
||||||
import supa from '@/components/supadb/aksupainstance.uts'
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
|
|
||||||
type AddressType = {
|
type Address = {
|
||||||
id?: string
|
id: string
|
||||||
user_id: string
|
name: string
|
||||||
recipient_name: string
|
|
||||||
phone: string
|
phone: string
|
||||||
province: string
|
province: string
|
||||||
city: string
|
city: string
|
||||||
district: string
|
district: string
|
||||||
detail: string
|
detail: string
|
||||||
postal_code: string | null
|
isDefault: boolean
|
||||||
is_default: boolean
|
label?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type RegionType = {
|
const isEdit = ref(false)
|
||||||
code: string
|
const addressId = ref('')
|
||||||
name: string
|
const regionString = ref('')
|
||||||
children?: RegionType[]
|
const tags = ['家', '公司', '学校']
|
||||||
}
|
|
||||||
|
|
||||||
const addressId = ref<string>('')
|
const formData = reactive({
|
||||||
const formData = ref<AddressType>({
|
name: '',
|
||||||
user_id: '',
|
|
||||||
recipient_name: '',
|
|
||||||
phone: '',
|
phone: '',
|
||||||
province: '',
|
|
||||||
city: '',
|
|
||||||
district: '',
|
|
||||||
detail: '',
|
detail: '',
|
||||||
postal_code: null,
|
isDefault: false,
|
||||||
is_default: false
|
label: ''
|
||||||
})
|
} as {
|
||||||
const addressList = ref<Array<AddressType>>([])
|
name: string
|
||||||
const showPicker = ref<boolean>(false)
|
phone: string
|
||||||
const provinces = ref<Array<RegionType>>([])
|
detail: string
|
||||||
const cities = ref<Array<RegionType>>([])
|
isDefault: boolean
|
||||||
const districts = ref<Array<RegionType>>([])
|
label: string
|
||||||
const pickerValue = ref<Array<number>>([0, 0, 0])
|
|
||||||
const selectedRegion = ref<{
|
|
||||||
province: RegionType | null
|
|
||||||
city: RegionType | null
|
|
||||||
district: RegionType | null
|
|
||||||
}>({
|
|
||||||
province: null,
|
|
||||||
city: null,
|
|
||||||
district: null
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 获取地区文本
|
onLoad((options) => {
|
||||||
const regionText = computed(() => {
|
if (options['id']) {
|
||||||
const { province, city, district } = selectedRegion.value
|
isEdit.value = true
|
||||||
if (province && city && district) {
|
addressId.value = options['id'] as string
|
||||||
return `${province.name} ${city.name} ${district.name}`
|
loadAddress(addressId.value)
|
||||||
}
|
|
||||||
return ''
|
|
||||||
})
|
|
||||||
|
|
||||||
// 生命周期
|
|
||||||
onMounted(() => {
|
|
||||||
loadAddresses()
|
|
||||||
loadRegions()
|
|
||||||
|
|
||||||
const pages = getCurrentPages()
|
|
||||||
const currentPage = pages[pages.length - 1]
|
|
||||||
const options = currentPage.options as any
|
|
||||||
|
|
||||||
if (options.id) {
|
|
||||||
addressId.value = options.id
|
|
||||||
loadAddressDetail(options.id)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 加载地址列表
|
const loadAddress = (id: string) => {
|
||||||
const loadAddresses = async () => {
|
const storedAddresses = uni.getStorageSync('addresses')
|
||||||
const userId = getCurrentUserId()
|
if (storedAddresses) {
|
||||||
if (!userId) return
|
const addresses = JSON.parse(storedAddresses as string) as Address[]
|
||||||
|
const address = addresses.find(item => item.id === id)
|
||||||
try {
|
if (address) {
|
||||||
const { data, error } = await supa
|
formData.name = address.name
|
||||||
.from('user_addresses')
|
formData.phone = address.phone
|
||||||
.select('*')
|
formData.detail = address.detail
|
||||||
.eq('user_id', userId)
|
formData.isDefault = address.isDefault
|
||||||
.order('is_default', { ascending: false })
|
formData.label = address.label || ''
|
||||||
.limit(5)
|
regionString.value = `${address.province} ${address.city} ${address.district}`.trim()
|
||||||
|
|
||||||
if (error !== null) {
|
|
||||||
console.error('加载地址列表失败:', error)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addressList.value = data ?? []
|
|
||||||
} catch (err) {
|
|
||||||
console.error('加载地址列表异常:', err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载地址详情
|
const selectTag = (tag: string) => {
|
||||||
const loadAddressDetail = async (id: string) => {
|
if (formData.label === tag) {
|
||||||
try {
|
formData.label = ''
|
||||||
const { data, error } = await supa
|
|
||||||
.from('user_addresses')
|
|
||||||
.select('*')
|
|
||||||
.eq('id', id)
|
|
||||||
.single()
|
|
||||||
|
|
||||||
if (error !== null) {
|
|
||||||
console.error('加载地址详情失败:', error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
formData.value = data
|
|
||||||
|
|
||||||
// 设置选中的地区
|
|
||||||
selectedRegion.value = {
|
|
||||||
province: { code: '', name: formData.value.province },
|
|
||||||
city: { code: '', name: formData.value.city },
|
|
||||||
district: { code: '', name: formData.value.district }
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('加载地址详情异常:', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载地区数据
|
|
||||||
const loadRegions = () => {
|
|
||||||
// 这里应该从API加载地区数据,这里使用模拟数据
|
|
||||||
provinces.value = [
|
|
||||||
{ code: '110000', name: '北京市' },
|
|
||||||
{ code: '310000', name: '上海市' },
|
|
||||||
{ code: '440000', name: '广东省' },
|
|
||||||
{ code: '330000', name: '浙江省' },
|
|
||||||
{ code: '320000', name: '江苏省' }
|
|
||||||
]
|
|
||||||
|
|
||||||
// 默认加载第一个省份的城市
|
|
||||||
if (provinces.value.length > 0) {
|
|
||||||
loadCities(provinces.value[0].code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载城市
|
|
||||||
const loadCities = (provinceCode: string) => {
|
|
||||||
// 模拟城市数据
|
|
||||||
const cityMap: Record<string, RegionType[]> = {
|
|
||||||
'110000': [
|
|
||||||
{ code: '110100', name: '北京市' }
|
|
||||||
],
|
|
||||||
'310000': [
|
|
||||||
{ code: '310100', name: '上海市' }
|
|
||||||
],
|
|
||||||
'440000': [
|
|
||||||
{ code: '440100', name: '广州市' },
|
|
||||||
{ code: '440300', name: '深圳市' },
|
|
||||||
{ code: '440600', name: '佛山市' }
|
|
||||||
],
|
|
||||||
'330000': [
|
|
||||||
{ code: '330100', name: '杭州市' },
|
|
||||||
{ code: '330200', name: '宁波市' },
|
|
||||||
{ code: '330300', name: '温州市' }
|
|
||||||
],
|
|
||||||
'320000': [
|
|
||||||
{ code: '320100', name: '南京市' },
|
|
||||||
{ code: '320200', name: '无锡市' },
|
|
||||||
{ code: '320500', name: '苏州市' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
cities.value = cityMap[provinceCode] || []
|
|
||||||
|
|
||||||
// 加载第一个城市的区县
|
|
||||||
if (cities.value.length > 0) {
|
|
||||||
loadDistricts(cities.value[0].code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载区县
|
|
||||||
const loadDistricts = (cityCode: string) => {
|
|
||||||
// 模拟区县数据
|
|
||||||
const districtMap: Record<string, RegionType[]> = {
|
|
||||||
'110100': [
|
|
||||||
{ code: '110101', name: '东城区' },
|
|
||||||
{ code: '110102', name: '西城区' },
|
|
||||||
{ code: '110105', name: '朝阳区' },
|
|
||||||
{ code: '110106', name: '丰台区' }
|
|
||||||
],
|
|
||||||
'440100': [
|
|
||||||
{ code: '440103', name: '荔湾区' },
|
|
||||||
{ code: '440104', name: '越秀区' },
|
|
||||||
{ code: '440105', name: '海珠区' },
|
|
||||||
{ code: '440106', name: '天河区' }
|
|
||||||
],
|
|
||||||
'330100': [
|
|
||||||
{ code: '330102', name: '上城区' },
|
|
||||||
{ code: '330103', name: '下城区' },
|
|
||||||
{ code: '330104', name: '江干区' },
|
|
||||||
{ code: '330105', name: '拱墅区' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
districts.value = districtMap[cityCode] || []
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取当前用户ID
|
|
||||||
const getCurrentUserId = (): string => {
|
|
||||||
const userStore = uni.getStorageSync('userInfo')
|
|
||||||
return userStore?.id || ''
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取完整地址
|
|
||||||
const getFullAddress = (address: AddressType): string => {
|
|
||||||
return `${address.province}${address.city}${address.district}${address.detail}`
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示地区选择器
|
|
||||||
const showRegionPicker = () => {
|
|
||||||
showPicker.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 隐藏地区选择器
|
|
||||||
const hideRegionPicker = () => {
|
|
||||||
showPicker.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 选择器变化
|
|
||||||
const onPickerChange = (event: any) => {
|
|
||||||
const value = event.detail.value
|
|
||||||
pickerValue.value = value
|
|
||||||
|
|
||||||
// 省份变化
|
|
||||||
if (value[0] !== pickerValue.value[0]) {
|
|
||||||
const province = provinces.value[value[0]]
|
|
||||||
selectedRegion.value.province = province
|
|
||||||
loadCities(province.code)
|
|
||||||
pickerValue.value = [value[0], 0, 0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// 城市变化
|
|
||||||
if (value[1] !== pickerValue.value[1]) {
|
|
||||||
const city = cities.value[value[1]]
|
|
||||||
selectedRegion.value.city = city
|
|
||||||
loadDistricts(city.code)
|
|
||||||
pickerValue.value = [value[0], value[1], 0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// 区县变化
|
|
||||||
if (value[2] !== pickerValue.value[2]) {
|
|
||||||
const district = districts.value[value[2]]
|
|
||||||
selectedRegion.value.district = district
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确认地区选择
|
|
||||||
const confirmRegion = () => {
|
|
||||||
const province = provinces.value[pickerValue.value[0]]
|
|
||||||
const city = cities.value[pickerValue.value[1]]
|
|
||||||
const district = districts.value[pickerValue.value[2]]
|
|
||||||
|
|
||||||
if (province && city && district) {
|
|
||||||
selectedRegion.value = { province, city, district }
|
|
||||||
formData.value.province = province.name
|
|
||||||
formData.value.city = city.name
|
|
||||||
formData.value.district = district.name
|
|
||||||
}
|
|
||||||
|
|
||||||
hideRegionPicker()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 切换默认地址
|
|
||||||
const toggleDefault = (event: any) => {
|
|
||||||
formData.value.is_default = event.detail.value
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从地址簿填充
|
|
||||||
const fillFromAddressBook = (address: AddressType) => {
|
|
||||||
formData.value = { ...address }
|
|
||||||
selectedRegion.value = {
|
|
||||||
province: { code: '', name: address.province },
|
|
||||||
city: { code: '', name: address.city },
|
|
||||||
district: { code: '', name: address.district }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存地址
|
|
||||||
const saveAddress = async () => {
|
|
||||||
// 验证表单
|
|
||||||
if (!formData.value.recipient_name.trim()) {
|
|
||||||
uni.showToast({
|
|
||||||
title: '请输入收货人姓名',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formData.value.phone.trim()) {
|
|
||||||
uni.showToast({
|
|
||||||
title: '请输入手机号码',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!/^1[3-9]\d{9}$/.test(formData.value.phone)) {
|
|
||||||
uni.showToast({
|
|
||||||
title: '手机号码格式错误',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formData.value.province) {
|
|
||||||
uni.showToast({
|
|
||||||
title: '请选择所在地区',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formData.value.detail.trim()) {
|
|
||||||
uni.showToast({
|
|
||||||
title: '请输入详细地址',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const userId = getCurrentUserId()
|
|
||||||
if (!userId) {
|
|
||||||
uni.showToast({
|
|
||||||
title: '用户信息错误',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
formData.value.user_id = userId
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (formData.value.is_default) {
|
|
||||||
// 取消其他默认地址
|
|
||||||
await supa
|
|
||||||
.from('user_addresses')
|
|
||||||
.update({ is_default: false })
|
|
||||||
.eq('user_id', userId)
|
|
||||||
.eq('is_default', true)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addressId.value) {
|
|
||||||
// 更新地址
|
|
||||||
const { error } = await supa
|
|
||||||
.from('user_addresses')
|
|
||||||
.update(formData.value)
|
|
||||||
.eq('id', addressId.value)
|
|
||||||
|
|
||||||
if (error !== null) {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
uni.showToast({
|
|
||||||
title: '地址更新成功',
|
|
||||||
icon: 'success'
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
// 新增地址
|
formData.label = tag
|
||||||
const { error } = await supa
|
}
|
||||||
.from('user_addresses')
|
|
||||||
.insert(formData.value)
|
|
||||||
|
|
||||||
if (error !== null) {
|
|
||||||
throw error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
uni.showToast({
|
const onSwitchChange = (e: UniSwitchChangeEvent) => {
|
||||||
title: '地址添加成功',
|
formData.isDefault = e.detail.value
|
||||||
icon: 'success'
|
}
|
||||||
|
|
||||||
|
const saveAddress = () => {
|
||||||
|
if (!formData.name) {
|
||||||
|
uni.showToast({ title: '请填写收货人', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!formData.phone) {
|
||||||
|
uni.showToast({ title: '请填写手机号码', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!regionString.value) {
|
||||||
|
uni.showToast({ title: '请填写所在地区', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!formData.detail) {
|
||||||
|
uni.showToast({ title: '请填写详细地址', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 简单解析地区(这里简化处理,实际应使用选择器)
|
||||||
|
const regions = regionString.value.split(' ')
|
||||||
|
const province = regions[0] || ''
|
||||||
|
const city = regions[1] || ''
|
||||||
|
const district = regions.slice(2).join(' ') || ''
|
||||||
|
|
||||||
|
const storedAddresses = uni.getStorageSync('addresses')
|
||||||
|
let addresses: Address[] = []
|
||||||
|
if (storedAddresses) {
|
||||||
|
try {
|
||||||
|
addresses = JSON.parse(storedAddresses as string) as Address[]
|
||||||
|
} catch (e) {
|
||||||
|
addresses = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果设为默认,取消其他默认
|
||||||
|
if (formData.isDefault) {
|
||||||
|
addresses.forEach(item => {
|
||||||
|
item.isDefault = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 返回上一页
|
if (isEdit.value) {
|
||||||
|
const index = addresses.findIndex(item => item.id === addressId.value)
|
||||||
|
if (index !== -1) {
|
||||||
|
addresses[index] = {
|
||||||
|
...addresses[index],
|
||||||
|
name: formData.name,
|
||||||
|
phone: formData.phone,
|
||||||
|
province: province,
|
||||||
|
city: city,
|
||||||
|
district: district,
|
||||||
|
detail: formData.detail,
|
||||||
|
isDefault: formData.isDefault,
|
||||||
|
label: formData.label
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const newAddress: Address = {
|
||||||
|
id: `addr_${Date.now()}`,
|
||||||
|
name: formData.name,
|
||||||
|
phone: formData.phone,
|
||||||
|
province: province,
|
||||||
|
city: city,
|
||||||
|
district: district,
|
||||||
|
detail: formData.detail,
|
||||||
|
isDefault: formData.isDefault,
|
||||||
|
label: formData.label
|
||||||
|
}
|
||||||
|
// 如果是第一个地址,自动设为默认
|
||||||
|
if (addresses.length === 0) {
|
||||||
|
newAddress.isDefault = true
|
||||||
|
}
|
||||||
|
addresses.push(newAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.setStorageSync('addresses', JSON.stringify(addresses))
|
||||||
|
|
||||||
|
uni.showToast({
|
||||||
|
title: '保存成功',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
uni.navigateBack()
|
uni.navigateBack()
|
||||||
}, 1500)
|
}, 1500)
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
console.error('保存地址失败:', err)
|
|
||||||
uni.showToast({
|
|
||||||
title: '保存失败',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除地址
|
|
||||||
const deleteAddress = () => {
|
const deleteAddress = () => {
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: '删除地址',
|
title: '提示',
|
||||||
content: '确定要删除这个地址吗?',
|
content: '确定要删除该地址吗?',
|
||||||
success: async (res) => {
|
success: (res) => {
|
||||||
if (res.confirm) {
|
if (res.confirm) {
|
||||||
try {
|
const storedAddresses = uni.getStorageSync('addresses')
|
||||||
const { error } = await supa
|
if (storedAddresses) {
|
||||||
.from('user_addresses')
|
let addresses = JSON.parse(storedAddresses as string) as Address[]
|
||||||
.delete()
|
addresses = addresses.filter(item => item.id !== addressId.value)
|
||||||
.eq('id', addressId.value)
|
uni.setStorageSync('addresses', JSON.stringify(addresses))
|
||||||
|
|
||||||
if (error !== null) {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '地址删除成功',
|
title: '删除成功',
|
||||||
icon: 'success'
|
icon: 'success'
|
||||||
})
|
})
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
uni.navigateBack()
|
uni.navigateBack()
|
||||||
}, 1500)
|
}, 1500)
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
console.error('删除地址失败:', err)
|
|
||||||
uni.showToast({
|
|
||||||
title: '删除失败',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 返回
|
|
||||||
const goBack = () => {
|
|
||||||
uni.navigateBack()
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style>
|
||||||
.address-edit-page {
|
.address-edit-page {
|
||||||
display: flex;
|
min-height: 100vh;
|
||||||
flex-direction: column;
|
|
||||||
height: 100vh;
|
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
|
padding-top: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-header {
|
.form-group {
|
||||||
background-color: #ffffff;
|
background-color: white;
|
||||||
padding: 15px;
|
margin-bottom: 15px;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
border-bottom: 1px solid #e5e5e5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.back-btn {
|
|
||||||
font-size: 24px;
|
|
||||||
color: #333333;
|
|
||||||
padding: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-title {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #333333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.delete-btn {
|
|
||||||
color: #ff4757;
|
|
||||||
font-size: 14px;
|
|
||||||
padding: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.edit-content {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-section {
|
|
||||||
background-color: #ffffff;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
padding: 0 15px;
|
padding: 0 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-item {
|
.form-item {
|
||||||
padding: 15px 0;
|
|
||||||
border-bottom: 1px solid #f5f5f5;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
border-bottom: 1px solid #f5f5f5;
|
||||||
|
padding: 15px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-item:last-child {
|
.form-item:last-child {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-label {
|
.label {
|
||||||
width: 80px;
|
width: 80px;
|
||||||
font-size: 14px;
|
font-size: 15px;
|
||||||
color: #333333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-input {
|
.input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 30px;
|
font-size: 15px;
|
||||||
font-size: 14px;
|
color: #333;
|
||||||
color: #333333;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-value {
|
.switch-item {
|
||||||
flex: 1;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #666666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-arrow {
|
|
||||||
color: #999999;
|
|
||||||
font-size: 16px;
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-textarea {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 60px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #333333;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.default-switch {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.switch-label {
|
.tags-container {
|
||||||
font-size: 14px;
|
flex: 1;
|
||||||
color: #333333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.address-book {
|
|
||||||
background-color: #ffffff;
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #333333;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.book-item {
|
|
||||||
padding: 15px 0;
|
|
||||||
border-bottom: 1px solid #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.book-item:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.book-info {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-wrap: wrap;
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.book-name {
|
.tag-item {
|
||||||
font-size: 14px;
|
font-size: 12px;
|
||||||
color: #333333;
|
color: #666;
|
||||||
font-weight: bold;
|
border: 1px solid #ddd;
|
||||||
margin-right: 15px;
|
padding: 4px 12px;
|
||||||
|
border-radius: 15px;
|
||||||
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.book-phone {
|
.tag-item.active {
|
||||||
font-size: 14px;
|
background-color: #ff5000;
|
||||||
color: #666666;
|
color: white;
|
||||||
|
border-color: #ff5000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.book-address {
|
.footer-btn {
|
||||||
font-size: 13px;
|
margin-top: 30px;
|
||||||
color: #666666;
|
padding: 0 15px;
|
||||||
line-height: 1.4;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.save-btn-container {
|
|
||||||
background-color: #ffffff;
|
|
||||||
padding: 15px;
|
|
||||||
border-top: 1px solid #e5e5e5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.save-btn {
|
.save-btn {
|
||||||
background-color: #007aff;
|
background-color: #ff5000;
|
||||||
color: #ffffff;
|
color: white;
|
||||||
height: 50px;
|
|
||||||
border-radius: 25px;
|
border-radius: 25px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: bold;
|
height: 44px;
|
||||||
|
line-height: 44px;
|
||||||
border: none;
|
border: none;
|
||||||
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.region-picker {
|
.delete-btn {
|
||||||
position: fixed;
|
background-color: white;
|
||||||
top: 0;
|
color: #333;
|
||||||
left: 0;
|
border-radius: 25px;
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
z-index: 999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.picker-mask {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.picker-content {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
background-color: #ffffff;
|
|
||||||
border-top-left-radius: 10px;
|
|
||||||
border-top-right-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.picker-header {
|
|
||||||
padding: 15px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
border-bottom: 1px solid #e5e5e5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cancel-btn {
|
|
||||||
color: #999999;
|
|
||||||
font-size: 14px;
|
|
||||||
padding: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.picker-title {
|
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: bold;
|
height: 44px;
|
||||||
color: #333333;
|
line-height: 44px;
|
||||||
}
|
border: 1px solid #ddd;
|
||||||
|
|
||||||
.confirm-btn {
|
|
||||||
color: #007aff;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: bold;
|
|
||||||
padding: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.picker-view {
|
|
||||||
height: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.picker-item {
|
|
||||||
height: 50px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #333333;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
221
pages/mall/consumer/address-list.uvue
Normal file
221
pages/mall/consumer/address-list.uvue
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
<template>
|
||||||
|
<view class="address-list-page">
|
||||||
|
<view class="address-list">
|
||||||
|
<view v-if="addresses.length === 0" class="empty-state">
|
||||||
|
<text class="empty-icon">📍</text>
|
||||||
|
<text class="empty-text">暂无收货地址</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-else v-for="(item, index) in addresses" :key="item.id" class="address-item" @click="selectAddress(item)">
|
||||||
|
<view class="item-content">
|
||||||
|
<view class="item-header">
|
||||||
|
<text class="user-name">{{ item.name }}</text>
|
||||||
|
<text class="user-phone">{{ item.phone }}</text>
|
||||||
|
<text v-if="item.isDefault" class="default-tag">默认</text>
|
||||||
|
<text v-if="item.label" class="label-tag">{{ item.label }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="address-text">{{ getFullAddress(item) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="item-edit" @click.stop="editAddress(item.id)">
|
||||||
|
<text class="edit-icon">📝</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="footer-btn">
|
||||||
|
<button class="add-btn" @click="addAddress">新建收货地址</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="uts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { onShow } from '@dcloudio/uni-app'
|
||||||
|
|
||||||
|
type Address = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
phone: string
|
||||||
|
province: string
|
||||||
|
city: string
|
||||||
|
district: string
|
||||||
|
detail: string
|
||||||
|
isDefault: boolean
|
||||||
|
label?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const addresses = ref<Address[]>([])
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
loadAddresses()
|
||||||
|
})
|
||||||
|
|
||||||
|
const loadAddresses = () => {
|
||||||
|
const storedAddresses = uni.getStorageSync('addresses')
|
||||||
|
if (storedAddresses) {
|
||||||
|
try {
|
||||||
|
addresses.value = JSON.parse(storedAddresses as string) as Address[]
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse addresses', e)
|
||||||
|
addresses.value = []
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 初始Mock数据
|
||||||
|
addresses.value = [
|
||||||
|
{
|
||||||
|
id: 'addr_001',
|
||||||
|
name: '张三',
|
||||||
|
phone: '13800138000',
|
||||||
|
province: '北京市',
|
||||||
|
city: '北京市',
|
||||||
|
district: '朝阳区',
|
||||||
|
detail: '三里屯SOHO A座',
|
||||||
|
isDefault: true,
|
||||||
|
label: '公司'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
uni.setStorageSync('addresses', JSON.stringify(addresses.value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFullAddress = (item: Address): string => {
|
||||||
|
return `${item.province}${item.city}${item.district} ${item.detail}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const addAddress = () => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/mall/consumer/address-edit'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const editAddress = (id: string) => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/mall/consumer/address-edit?id=${id}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectAddress = (item: Address) => {
|
||||||
|
// 如果是选择地址模式(例如从订单确认页过来),则返回并传递地址
|
||||||
|
// 目前暂未实现选择模式,仅作为普通点击
|
||||||
|
editAddress(item.id)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.address-list-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding-bottom: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-list {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding-top: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 60px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
color: #999;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-item {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-content {
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-phone {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.default-tag {
|
||||||
|
background-color: #ff5000;
|
||||||
|
color: white;
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-tag {
|
||||||
|
background-color: #e0f2f1;
|
||||||
|
color: #00796b;
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-edit {
|
||||||
|
padding: 10px;
|
||||||
|
border-left: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-btn {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background-color: white;
|
||||||
|
padding: 10px 15px;
|
||||||
|
padding-bottom: calc(10px + env(safe-area-inset-bottom));
|
||||||
|
box-shadow: 0 -2px 10px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-btn {
|
||||||
|
background-color: #ff5000;
|
||||||
|
color: white;
|
||||||
|
border-radius: 25px;
|
||||||
|
font-size: 16px;
|
||||||
|
height: 44px;
|
||||||
|
line-height: 44px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,18 +1,38 @@
|
|||||||
<!-- pages/mall/consumer/category.uvue -->
|
|
||||||
<template>
|
<template>
|
||||||
<view class="category-page">
|
<view class="category-page">
|
||||||
<!-- 顶部搜索栏 -->
|
<!-- 顶部搜索栏 -->
|
||||||
<view class="search-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
|
<view class="search-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||||
<view class="search-container">
|
<view class="search-container">
|
||||||
<view class="search-box" @click="navigateToSearch">
|
<view class="search-box" @click="navigateToSearch" :style="{ height: '30px' }">
|
||||||
<text class="search-icon">🔍</text>
|
<!-- 模拟输入框 -->
|
||||||
<text class="search-placeholder">症状/药品/品牌智能搜索</text>
|
<text class="search-placeholder">请输入药品名称、症状或品牌</text>
|
||||||
|
|
||||||
|
<!-- 扫码图标 -->
|
||||||
|
<view class="nav-icon-btn" @click.stop="onScan">
|
||||||
|
<text class="nav-icon">🔳</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 相机图标 -->
|
||||||
|
<view class="nav-camera-btn" @click.stop="onCamera">
|
||||||
|
<text class="nav-camera-icon">📷</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 搜索按钮 -->
|
||||||
|
<view class="nav-inner-search-btn" :style="{ height: '22px' }">
|
||||||
|
<text class="nav-inner-search-text">搜索</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 分类内容区 -->
|
<!-- 分类内容区 -->
|
||||||
<view class="category-content">
|
<view
|
||||||
|
class="category-content"
|
||||||
|
:style="{
|
||||||
|
marginTop: (statusBarHeight + headerHeight + 10) + 'px',
|
||||||
|
height: `calc(100vh - ${statusBarHeight + headerHeight + 10}px)`
|
||||||
|
}"
|
||||||
|
>
|
||||||
<!-- 左侧一级分类 -->
|
<!-- 左侧一级分类 -->
|
||||||
<scroll-view scroll-y class="primary-category">
|
<scroll-view scroll-y class="primary-category">
|
||||||
<view
|
<view
|
||||||
@@ -94,6 +114,7 @@ import { ref, onMounted } from 'vue'
|
|||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
const statusBarHeight = ref(0)
|
const statusBarHeight = ref(0)
|
||||||
|
const headerHeight = ref(44) // 默认头部高度
|
||||||
const primaryCategories = ref<any[]>([])
|
const primaryCategories = ref<any[]>([])
|
||||||
const productList = ref<any[]>([])
|
const productList = ref<any[]>([])
|
||||||
const activePrimary = ref<string>('cold')
|
const activePrimary = ref<string>('cold')
|
||||||
@@ -126,6 +147,8 @@ const mockProducts = {
|
|||||||
cold: [
|
cold: [
|
||||||
{
|
{
|
||||||
id: 'cold1',
|
id: 'cold1',
|
||||||
|
shopId: 'shop_001',
|
||||||
|
shopName: '修正药业官方旗舰店',
|
||||||
name: '布洛芬缓释胶囊',
|
name: '布洛芬缓释胶囊',
|
||||||
specification: '0.3g*24粒',
|
specification: '0.3g*24粒',
|
||||||
price: 18.5,
|
price: 18.5,
|
||||||
@@ -137,6 +160,8 @@ const mockProducts = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'cold2',
|
id: 'cold2',
|
||||||
|
shopId: 'shop_002',
|
||||||
|
shopName: '白云山大药房',
|
||||||
name: '板蓝根颗粒',
|
name: '板蓝根颗粒',
|
||||||
specification: '10g*20袋',
|
specification: '10g*20袋',
|
||||||
price: 22.8,
|
price: 22.8,
|
||||||
@@ -148,6 +173,8 @@ const mockProducts = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'cold3',
|
id: 'cold3',
|
||||||
|
shopId: 'shop_003',
|
||||||
|
shopName: '以岭药业自营店',
|
||||||
name: '连花清瘟胶囊',
|
name: '连花清瘟胶囊',
|
||||||
specification: '0.35g*36粒',
|
specification: '0.35g*36粒',
|
||||||
price: 42.8,
|
price: 42.8,
|
||||||
@@ -159,6 +186,8 @@ const mockProducts = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'cold4',
|
id: 'cold4',
|
||||||
|
shopId: 'shop_004',
|
||||||
|
shopName: '强生制药旗舰店',
|
||||||
name: '对乙酰氨基酚片',
|
name: '对乙酰氨基酚片',
|
||||||
specification: '0.5g*12片',
|
specification: '0.5g*12片',
|
||||||
price: 8.9,
|
price: 8.9,
|
||||||
@@ -170,6 +199,8 @@ const mockProducts = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'cold5',
|
id: 'cold5',
|
||||||
|
shopId: 'shop_005',
|
||||||
|
shopName: '同仁堂大药房',
|
||||||
name: '感冒清热颗粒',
|
name: '感冒清热颗粒',
|
||||||
specification: '3g*10袋',
|
specification: '3g*10袋',
|
||||||
price: 16.5,
|
price: 16.5,
|
||||||
@@ -181,6 +212,8 @@ const mockProducts = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'cold6',
|
id: 'cold6',
|
||||||
|
shopId: 'shop_006',
|
||||||
|
shopName: '三九医药旗舰店',
|
||||||
name: '复方氨酚烷胺片',
|
name: '复方氨酚烷胺片',
|
||||||
specification: '10片/盒',
|
specification: '10片/盒',
|
||||||
price: 12.8,
|
price: 12.8,
|
||||||
@@ -195,6 +228,8 @@ const mockProducts = {
|
|||||||
stomach: [
|
stomach: [
|
||||||
{
|
{
|
||||||
id: 'stomach1',
|
id: 'stomach1',
|
||||||
|
shopId: 'shop_006',
|
||||||
|
shopName: '三九医药旗舰店',
|
||||||
name: '胃康灵胶囊',
|
name: '胃康灵胶囊',
|
||||||
specification: '0.4g*24粒',
|
specification: '0.4g*24粒',
|
||||||
price: 32.8,
|
price: 32.8,
|
||||||
@@ -206,6 +241,8 @@ const mockProducts = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'stomach2',
|
id: 'stomach2',
|
||||||
|
shopId: 'shop_007',
|
||||||
|
shopName: '阿斯利康医药',
|
||||||
name: '奥美拉唑肠溶胶囊',
|
name: '奥美拉唑肠溶胶囊',
|
||||||
specification: '20mg*14粒',
|
specification: '20mg*14粒',
|
||||||
price: 28.5,
|
price: 28.5,
|
||||||
@@ -217,6 +254,8 @@ const mockProducts = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'stomach3',
|
id: 'stomach3',
|
||||||
|
shopId: 'shop_008',
|
||||||
|
shopName: '江中制药旗舰店',
|
||||||
name: '健胃消食片',
|
name: '健胃消食片',
|
||||||
specification: '0.8g*32片',
|
specification: '0.8g*32片',
|
||||||
price: 15.9,
|
price: 15.9,
|
||||||
@@ -228,6 +267,8 @@ const mockProducts = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'stomach4',
|
id: 'stomach4',
|
||||||
|
shopId: 'shop_009',
|
||||||
|
shopName: '益普生大药房',
|
||||||
name: '蒙脱石散',
|
name: '蒙脱石散',
|
||||||
specification: '3g*10袋',
|
specification: '3g*10袋',
|
||||||
price: 18.6,
|
price: 18.6,
|
||||||
@@ -239,6 +280,8 @@ const mockProducts = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'stomach5',
|
id: 'stomach5',
|
||||||
|
shopId: 'shop_010',
|
||||||
|
shopName: '西安杨森旗舰店',
|
||||||
name: '多潘立酮片',
|
name: '多潘立酮片',
|
||||||
specification: '10mg*30片',
|
specification: '10mg*30片',
|
||||||
price: 22.8,
|
price: 22.8,
|
||||||
@@ -250,6 +293,8 @@ const mockProducts = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'stomach6',
|
id: 'stomach6',
|
||||||
|
shopId: 'shop_011',
|
||||||
|
shopName: '拜耳医药自营店',
|
||||||
name: '铝碳酸镁咀嚼片',
|
name: '铝碳酸镁咀嚼片',
|
||||||
specification: '0.5g*20片',
|
specification: '0.5g*20片',
|
||||||
price: 25.9,
|
price: 25.9,
|
||||||
@@ -264,6 +309,8 @@ const mockProducts = {
|
|||||||
pain: [
|
pain: [
|
||||||
{
|
{
|
||||||
id: 'pain1',
|
id: 'pain1',
|
||||||
|
shopId: 'shop_012',
|
||||||
|
shopName: '华北制药旗舰店',
|
||||||
name: '阿莫西林胶囊',
|
name: '阿莫西林胶囊',
|
||||||
specification: '0.25g*24粒',
|
specification: '0.25g*24粒',
|
||||||
price: 28.5,
|
price: 28.5,
|
||||||
@@ -275,6 +322,8 @@ const mockProducts = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'pain2',
|
id: 'pain2',
|
||||||
|
shopId: 'shop_013',
|
||||||
|
shopName: '诺华制药旗舰店',
|
||||||
name: '双氯芬酸钠缓释片',
|
name: '双氯芬酸钠缓释片',
|
||||||
specification: '75mg*10片',
|
specification: '75mg*10片',
|
||||||
price: 19.8,
|
price: 19.8,
|
||||||
@@ -286,6 +335,8 @@ const mockProducts = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'pain3',
|
id: 'pain3',
|
||||||
|
shopId: 'shop_014',
|
||||||
|
shopName: '云南白药旗舰店',
|
||||||
name: '云南白药胶囊',
|
name: '云南白药胶囊',
|
||||||
specification: '0.25g*32粒',
|
specification: '0.25g*32粒',
|
||||||
price: 35.9,
|
price: 35.9,
|
||||||
@@ -297,6 +348,8 @@ const mockProducts = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'pain4',
|
id: 'pain4',
|
||||||
|
shopId: 'shop_015',
|
||||||
|
shopName: '辉瑞医药旗舰店',
|
||||||
name: '塞来昔布胶囊',
|
name: '塞来昔布胶囊',
|
||||||
specification: '0.2g*10粒',
|
specification: '0.2g*10粒',
|
||||||
price: 48.6,
|
price: 48.6,
|
||||||
@@ -308,6 +361,8 @@ const mockProducts = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'pain5',
|
id: 'pain5',
|
||||||
|
shopId: 'shop_016',
|
||||||
|
shopName: '中美史克大药房',
|
||||||
name: '布洛芬片',
|
name: '布洛芬片',
|
||||||
specification: '0.1g*24片',
|
specification: '0.1g*24片',
|
||||||
price: 12.5,
|
price: 12.5,
|
||||||
@@ -319,6 +374,8 @@ const mockProducts = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'pain6',
|
id: 'pain6',
|
||||||
|
shopId: 'shop_002',
|
||||||
|
shopName: '白云山大药房',
|
||||||
name: '头孢克肟胶囊',
|
name: '头孢克肟胶囊',
|
||||||
specification: '0.1g*6粒',
|
specification: '0.1g*6粒',
|
||||||
price: 32.8,
|
price: 32.8,
|
||||||
@@ -344,6 +401,8 @@ const generateDefaultProducts = (categoryId: string) => {
|
|||||||
|
|
||||||
return baseProducts.map((product, index) => ({
|
return baseProducts.map((product, index) => ({
|
||||||
id: `${categoryId}${index + 1}`,
|
id: `${categoryId}${index + 1}`,
|
||||||
|
shopId: `shop_default_${categoryId}_${index}`, // 确保不同分类店铺ID不同
|
||||||
|
shopName: '平台自营大药房',
|
||||||
...product,
|
...product,
|
||||||
specification: '规格待定',
|
specification: '规格待定',
|
||||||
originalPrice: product.price * 1.2,
|
originalPrice: product.price * 1.2,
|
||||||
@@ -419,8 +478,15 @@ onLoad((options: any) => {
|
|||||||
}, 100)
|
}, 100)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('⚠️ onLoad中未找到分类参数')
|
console.log('⚠️ onLoad中未找到分类参数,使用默认分类')
|
||||||
console.log('保持当前分类显示:', activePrimary.value)
|
// 默认选中第一个分类
|
||||||
|
const defaultCategory = 'cold'
|
||||||
|
console.log('默认分类:', defaultCategory)
|
||||||
|
|
||||||
|
// 无论如何都重新加载一次默认分类的数据
|
||||||
|
setTimeout(() => {
|
||||||
|
selectPrimaryCategory(defaultCategory)
|
||||||
|
}, 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('=== category页面onLoad执行完成 ===')
|
console.log('=== category页面onLoad执行完成 ===')
|
||||||
@@ -505,6 +571,10 @@ const initPage = () => {
|
|||||||
const systemInfo = uni.getSystemInfoSync()
|
const systemInfo = uni.getSystemInfoSync()
|
||||||
statusBarHeight.value = systemInfo.statusBarHeight || 0
|
statusBarHeight.value = systemInfo.statusBarHeight || 0
|
||||||
|
|
||||||
|
// 保持与主页一致的固定高度计算,不进行动态调整
|
||||||
|
// 这样在移动端会与主页的视觉体验保持一致(主页占位符固定为44px)
|
||||||
|
headerHeight.value = 10
|
||||||
|
|
||||||
// 获取页面参数
|
// 获取页面参数
|
||||||
const pages = getCurrentPages()
|
const pages = getCurrentPages()
|
||||||
if (pages.length > 0) {
|
if (pages.length > 0) {
|
||||||
@@ -585,6 +655,41 @@ const selectPrimaryCategory = (categoryId: string) => {
|
|||||||
|
|
||||||
// 添加到购物车
|
// 添加到购物车
|
||||||
const addToCart = (product: any) => {
|
const addToCart = (product: any) => {
|
||||||
|
// 获取现有购物车数据
|
||||||
|
const cartData = uni.getStorageSync('cart')
|
||||||
|
let cartItems: any[] = []
|
||||||
|
|
||||||
|
if (cartData) {
|
||||||
|
try {
|
||||||
|
cartItems = JSON.parse(cartData as string) as any[]
|
||||||
|
} catch (e) {
|
||||||
|
console.error('解析购物车数据失败', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查商品是否已存在
|
||||||
|
const existingItem = cartItems.find((item: any) => item.id === product.id)
|
||||||
|
|
||||||
|
if (existingItem) {
|
||||||
|
existingItem.quantity++
|
||||||
|
} else {
|
||||||
|
// 添加新商品
|
||||||
|
cartItems.push({
|
||||||
|
id: product.id,
|
||||||
|
shopId: product.shopId || 'shop_default',
|
||||||
|
shopName: product.shopName || product.manufacturer || '自营店铺',
|
||||||
|
name: product.name,
|
||||||
|
price: product.price,
|
||||||
|
image: product.image,
|
||||||
|
spec: product.specification || '默认规格',
|
||||||
|
quantity: 1,
|
||||||
|
selected: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存回存储
|
||||||
|
uni.setStorageSync('cart', JSON.stringify(cartItems))
|
||||||
|
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '已添加到购物车',
|
title: '已添加到购物车',
|
||||||
icon: 'success'
|
icon: 'success'
|
||||||
@@ -597,7 +702,48 @@ const navigateToSearch = () => uni.navigateTo({ url: '/pages/mall/consumer/searc
|
|||||||
const navigateToCart = () => uni.navigateTo({ url: '/pages/medicine/cart' })
|
const navigateToCart = () => uni.navigateTo({ url: '/pages/medicine/cart' })
|
||||||
const navigateToProduct = (product: any) => {
|
const navigateToProduct = (product: any) => {
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: `/pages/medicine/detail?id=${product.id}`
|
url: `/pages/mall/consumer/product-detail?productId=${product.id}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 相机功能
|
||||||
|
const onCamera = () => {
|
||||||
|
uni.chooseImage({
|
||||||
|
count: 1,
|
||||||
|
sourceType: ['camera'],
|
||||||
|
success: (res) => {
|
||||||
|
console.log('相机拍摄成功:', res.tempFilePaths[0])
|
||||||
|
uni.showToast({
|
||||||
|
title: '已拍摄,正在识别...',
|
||||||
|
icon: 'loading'
|
||||||
|
})
|
||||||
|
// 这里可以添加后续的识别逻辑
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.showToast({
|
||||||
|
title: '识别成功',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
}, 1000)
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
console.error('相机调用失败:', err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 扫码功能
|
||||||
|
const onScan = () => {
|
||||||
|
uni.scanCode({
|
||||||
|
success: (res) => {
|
||||||
|
console.log('扫码成功:', res)
|
||||||
|
uni.showToast({
|
||||||
|
title: '扫码成功: ' + res.result,
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
console.error('扫码失败:', err)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -611,7 +757,6 @@ const navigateToProduct = (product: any) => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 搜索栏 */
|
/* 搜索栏 */
|
||||||
.search-bar {
|
.search-bar {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -623,8 +768,10 @@ const navigateToProduct = (product: any) => {
|
|||||||
box-shadow: 0 2px 12px rgba(76, 175, 80, 0.15);
|
box-shadow: 0 2px 12px rgba(76, 175, 80, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 搜索栏 */
|
||||||
|
/* 导航栏搜索框容器内边距调整 */
|
||||||
.search-container {
|
.search-container {
|
||||||
height: 60px;
|
height: 44px; /* 调整为与消息页一致的高度 */
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -634,44 +781,90 @@ const navigateToProduct = (product: any) => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 搜索框 hover 效果 */
|
||||||
|
.search-box:hover {
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 导航栏搜索框容器内边距调整 */
|
||||||
.search-box {
|
.search-box {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
background: rgba(255, 255, 255, 0.95);
|
background: #f0f0f0;
|
||||||
border-radius: 25px;
|
border-radius: 20px;
|
||||||
padding: 10px 20px;
|
padding: 0 4px 0 12px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: row; /* UVUE 显式设置 row */
|
||||||
align-items: center;
|
align-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
border: 2px solid transparent;
|
width: 100%;
|
||||||
}
|
height: 32px; /* 减小高度,与顶部高度44px适配,略小于顶部高度 */
|
||||||
|
|
||||||
.search-box:hover {
|
|
||||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
|
||||||
border-color: rgba(255, 255, 255, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-icon {
|
|
||||||
font-size: 18px;
|
|
||||||
color: #4CAF50;
|
|
||||||
margin-right: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-placeholder {
|
.search-placeholder {
|
||||||
font-size: 15px;
|
font-size: 14px;
|
||||||
color: #666;
|
color: #999;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-inner-search-text {
|
||||||
|
font-size: 12px; /* 字体稍微变小 */
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-icon-btn {
|
||||||
|
padding: 4px 8px 4px 4px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-right: 1px solid #ddd;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-camera-btn {
|
||||||
|
padding: 4px 8px 4px 4px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-right-width: 1px;
|
||||||
|
border-right-style: solid;
|
||||||
|
border-right-color: #ddd;
|
||||||
|
border-right: 1px solid #ddd; /* 修复UVUE样式 */
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-camera-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 搜索按钮高度微调 */
|
||||||
|
.nav-inner-search-btn {
|
||||||
|
padding: 0 12px; /* 减小内边距 */
|
||||||
|
background-color: #87CEEB; /* 天空蓝 */
|
||||||
|
border-radius: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 24px; /* 随搜索框高度减小而减小 */
|
||||||
|
}
|
||||||
|
|
||||||
.cart-badge {
|
.cart-badge {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -5px;
|
top: -5px;
|
||||||
@@ -693,14 +886,14 @@ const navigateToProduct = (product: any) => {
|
|||||||
.category-content {
|
.category-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row; /* 强制水平排列 */
|
flex-direction: row; /* 强制水平排列 */
|
||||||
margin-top: 60px;
|
/* margin-top: 44px; 已通过 style 动态绑定 */
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
max-width: 1400px;
|
max-width: 1400px;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
height: calc(100vh - 60px); /* 设置固定高度,减去头部高度 */
|
/* height: calc(100vh - 44px); 已通过 style 动态绑定 */
|
||||||
overflow: hidden; /* 防止整体滚动 */
|
overflow: hidden; /* 防止整体滚动 */
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -996,11 +1189,59 @@ const navigateToProduct = (product: any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.product-grid {
|
.product-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: repeat(2, 1fr); /* 改为双列显示 */
|
||||||
gap: 12px;
|
gap: 8px;
|
||||||
padding: 0 4px 20px 4px; /* 增加底部内边距 */
|
padding: 0 4px 20px 4px; /* 增加底部内边距 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 手机端商品卡片极简模式 - 仿照主页样式 */
|
||||||
|
.product-spec,
|
||||||
|
.manufacturer,
|
||||||
|
.original-price,
|
||||||
|
.sales-info,
|
||||||
|
.product-badge { /* 分类页也隐藏角标,保持整洁 */
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info {
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-image {
|
||||||
|
height: 100px; /* 由于分类页右侧空间更窄,图片高度设得更小一点 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-name {
|
||||||
|
font-size: 12px;
|
||||||
|
height: 32px;
|
||||||
|
line-height: 1.3;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-section {
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-top: 4px;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-symbol {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-value {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-meta {
|
||||||
|
display: none; /* 隐藏整个元数据行 */
|
||||||
|
}
|
||||||
|
|
||||||
.search-container {
|
.search-container {
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
height: 55px;
|
height: 55px;
|
||||||
|
|||||||
@@ -1,527 +1,119 @@
|
|||||||
<!-- 优惠券页面 -->
|
|
||||||
<template>
|
<template>
|
||||||
<view class="coupons-page">
|
<view class="coupons-page">
|
||||||
<!-- 顶部栏 -->
|
<view class="coupon-list">
|
||||||
<view class="coupons-header">
|
<view v-if="coupons.length === 0" class="empty-state">
|
||||||
<view class="header-tabs">
|
|
||||||
<view :class="['header-tab', { active: activeTab === 'available' }]" @click="changeTab('available')">
|
|
||||||
<text class="tab-text">可用券</text>
|
|
||||||
</view>
|
|
||||||
<view :class="['header-tab', { active: activeTab === 'unavailable' }]" @click="changeTab('unavailable')">
|
|
||||||
<text class="tab-text">已失效</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 优惠券列表 -->
|
|
||||||
<scroll-view class="coupons-list" scroll-y>
|
|
||||||
<!-- 为空提示 -->
|
|
||||||
<view v-if="coupons.length === 0" class="empty-coupons">
|
|
||||||
<text class="empty-icon">🎫</text>
|
<text class="empty-icon">🎫</text>
|
||||||
<text class="empty-text">{{ getEmptyText() }}</text>
|
<text class="empty-text">暂无优惠券</text>
|
||||||
<text class="empty-subtext">{{ getEmptySubtext() }}</text>
|
|
||||||
<button v-if="activeTab === 'available'" class="get-coupons-btn" @click="goToCouponCenter">
|
|
||||||
去领券中心
|
|
||||||
</button>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 优惠券项 -->
|
<view v-else v-for="(coupon, index) in coupons" :key="index" class="coupon-item">
|
||||||
<view v-for="coupon in coupons" :key="coupon.id" class="coupon-item">
|
|
||||||
<view class="coupon-left">
|
<view class="coupon-left">
|
||||||
<text class="coupon-value">{{ formatCouponValue(coupon) }}</text>
|
<text class="coupon-amount">{{ coupon.amount }}</text>
|
||||||
<text class="coupon-condition">{{ formatCondition(coupon) }}</text>
|
<text class="coupon-type">优惠券</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="coupon-right">
|
<view class="coupon-right">
|
||||||
<view class="coupon-info">
|
<text class="coupon-title">{{ coupon.title }}</text>
|
||||||
<text class="coupon-name">{{ coupon.template?.name || coupon.name }}</text>
|
<text class="coupon-expiry">有效期至: {{ coupon.expiry }}</text>
|
||||||
<text class="coupon-desc">{{ coupon.template?.description || '' }}</text>
|
<button class="use-btn" @click="useCoupon(coupon)">去使用</button>
|
||||||
<text class="coupon-time">{{ formatTimeRange(coupon) }}</text>
|
|
||||||
</view>
|
|
||||||
<view class="coupon-actions">
|
|
||||||
<button v-if="coupon.status === 1 && coupon.is_valid"
|
|
||||||
class="use-btn"
|
|
||||||
@click="useCoupon(coupon)">
|
|
||||||
立即使用
|
|
||||||
</button>
|
|
||||||
<button v-else-if="coupon.template_id && activeTab === 'available'"
|
|
||||||
class="receive-btn"
|
|
||||||
@click="receiveCoupon(coupon)">
|
|
||||||
领取
|
|
||||||
</button>
|
|
||||||
<text v-else class="status-text">{{ getStatusText(coupon) }}</text>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import { ref, onMounted, watch } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import supa from '@/components/supadb/aksupainstance.uts'
|
|
||||||
|
|
||||||
type CouponTemplateType = {
|
type Coupon = {
|
||||||
|
title: string
|
||||||
|
amount: string
|
||||||
|
expiry: string
|
||||||
id: string
|
id: string
|
||||||
name: string
|
|
||||||
description: string | null
|
|
||||||
coupon_type: number
|
|
||||||
discount_value: number
|
|
||||||
min_order_amount: number
|
|
||||||
start_time: string
|
|
||||||
end_time: string
|
|
||||||
per_user_limit: number
|
|
||||||
total_quantity: number
|
|
||||||
used_quantity: number
|
|
||||||
status: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserCouponType = {
|
const coupons = ref<Coupon[]>([])
|
||||||
id: string
|
|
||||||
user_id: string
|
|
||||||
template_id: string
|
|
||||||
coupon_code: string
|
|
||||||
status: number
|
|
||||||
is_valid: boolean
|
|
||||||
used_at: string | null
|
|
||||||
expire_at: string
|
|
||||||
created_at: string
|
|
||||||
template: CouponTemplateType | null
|
|
||||||
}
|
|
||||||
|
|
||||||
const activeTab = ref<string>('available')
|
|
||||||
const coupons = ref<Array<any>>([])
|
|
||||||
const isLoading = ref<boolean>(false)
|
|
||||||
|
|
||||||
// 监听标签页变化
|
|
||||||
watch(activeTab, () => {
|
|
||||||
loadCoupons()
|
|
||||||
})
|
|
||||||
|
|
||||||
// 生命周期
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadCoupons()
|
loadCoupons()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 加载优惠券
|
const loadCoupons = () => {
|
||||||
const loadCoupons = async () => {
|
// 从本地存储获取已领取的优惠券详情
|
||||||
const userId = getCurrentUserId()
|
// 假设存储格式为 JSON 字符串数组
|
||||||
if (!userId) {
|
const storedCoupons = uni.getStorageSync('myCoupons')
|
||||||
uni.showToast({
|
if (storedCoupons) {
|
||||||
title: '请先登录',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
uni.navigateTo({
|
|
||||||
url: '/pages/user/login'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoading.value = true
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (activeTab.value === 'available') {
|
coupons.value = JSON.parse(storedCoupons as string) as Coupon[]
|
||||||
await loadAvailableCoupons(userId)
|
} catch (e) {
|
||||||
} else {
|
console.error('Failed to parse coupons', e)
|
||||||
await loadUnavailableCoupons(userId)
|
coupons.value = []
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('加载优惠券异常:', err)
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载可用优惠券
|
|
||||||
const loadAvailableCoupons = async (userId: string) => {
|
|
||||||
try {
|
|
||||||
const now = new Date().toISOString()
|
|
||||||
|
|
||||||
// 加载用户已领取的优惠券
|
|
||||||
const { data: userCoupons, error: userError } = await supa
|
|
||||||
.from('user_coupons')
|
|
||||||
.select(`
|
|
||||||
*,
|
|
||||||
template:coupon_templates(*)
|
|
||||||
`)
|
|
||||||
.eq('user_id', userId)
|
|
||||||
.eq('status', 1)
|
|
||||||
.gte('expire_at', now)
|
|
||||||
.order('expire_at', { ascending: true })
|
|
||||||
|
|
||||||
if (userError !== null) {
|
|
||||||
console.error('加载用户优惠券失败:', userError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载可领取的优惠券
|
|
||||||
const { data: availableCoupons, error: availableError } = await supa
|
|
||||||
.from('coupon_templates')
|
|
||||||
.select('*')
|
|
||||||
.eq('status', 1)
|
|
||||||
.gte('end_time', now)
|
|
||||||
.lte('start_time', now)
|
|
||||||
.order('created_at', { ascending: false })
|
|
||||||
|
|
||||||
if (availableError !== null) {
|
|
||||||
console.error('加载可领取优惠券失败:', availableError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 合并结果
|
|
||||||
const allCoupons = [...(userCoupons || []), ...(availableCoupons || [])]
|
|
||||||
coupons.value = allCoupons
|
|
||||||
} catch (err) {
|
|
||||||
console.error('加载可用优惠券异常:', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载不可用优惠券
|
|
||||||
const loadUnavailableCoupons = async (userId: string) => {
|
|
||||||
try {
|
|
||||||
const now = new Date().toISOString()
|
|
||||||
|
|
||||||
const { data, error } = await supa
|
|
||||||
.from('user_coupons')
|
|
||||||
.select(`
|
|
||||||
*,
|
|
||||||
template:coupon_templates(*)
|
|
||||||
`)
|
|
||||||
.eq('user_id', userId)
|
|
||||||
.or('status.eq.2,expire_at.lt.' + now)
|
|
||||||
.order('used_at', { ascending: false })
|
|
||||||
.order('expire_at', { ascending: false })
|
|
||||||
|
|
||||||
if (error !== null) {
|
|
||||||
console.error('加载失效优惠券失败:', error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
coupons.value = data ?? []
|
|
||||||
} catch (err) {
|
|
||||||
console.error('加载失效优惠券异常:', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取当前用户ID
|
|
||||||
const getCurrentUserId = (): string | null => {
|
|
||||||
const userStore = uni.getStorageSync('userInfo')
|
|
||||||
return userStore?.id || null
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取空状态文本
|
|
||||||
const getEmptyText = (): string => {
|
|
||||||
return activeTab.value === 'available' ? '暂无可用优惠券' : '暂无失效优惠券'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取空状态副文本
|
|
||||||
const getEmptySubtext = (): string => {
|
|
||||||
return activeTab.value === 'available' ? '去领券中心看看' : '努力使用优惠券吧'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化优惠券价值
|
|
||||||
const formatCouponValue = (coupon: any): string => {
|
|
||||||
if (coupon.template_id) {
|
|
||||||
// 用户优惠券
|
|
||||||
const template = coupon.template
|
|
||||||
if (!template) return '未知'
|
|
||||||
|
|
||||||
if (template.coupon_type === 1) {
|
|
||||||
return `¥${template.discount_value}`
|
|
||||||
} else if (template.coupon_type === 2) {
|
|
||||||
return `${template.discount_value}折`
|
|
||||||
} else {
|
|
||||||
return '未知'
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 优惠券模板
|
// 默认空或者是mock一些基础数据如果需要
|
||||||
if (coupon.coupon_type === 1) {
|
coupons.value = []
|
||||||
return `¥${coupon.discount_value}`
|
|
||||||
} else if (coupon.coupon_type === 2) {
|
|
||||||
return `${coupon.discount_value}折`
|
|
||||||
} else {
|
|
||||||
return '未知'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 格式化使用条件
|
const useCoupon = (coupon: Coupon) => {
|
||||||
const formatCondition = (coupon: any): string => {
|
uni.switchTab({
|
||||||
const minAmount = coupon.template?.min_order_amount || coupon.min_order_amount
|
|
||||||
if (minAmount > 0) {
|
|
||||||
return `满${minAmount}元可用`
|
|
||||||
}
|
|
||||||
return '无门槛'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化时间范围
|
|
||||||
const formatTimeRange = (coupon: any): string => {
|
|
||||||
const startTime = coupon.template?.start_time || coupon.start_time
|
|
||||||
const endTime = coupon.template?.end_time || coupon.expire_at
|
|
||||||
|
|
||||||
if (startTime && endTime) {
|
|
||||||
const start = new Date(startTime)
|
|
||||||
const end = new Date(endTime)
|
|
||||||
|
|
||||||
const startStr = `${start.getMonth() + 1}月${start.getDate()}日`
|
|
||||||
const endStr = `${end.getMonth() + 1}月${end.getDate()}日`
|
|
||||||
|
|
||||||
return `${startStr}-${endStr}`
|
|
||||||
}
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取状态文本
|
|
||||||
const getStatusText = (coupon: any): string => {
|
|
||||||
if (coupon.status === 2) {
|
|
||||||
return '已使用'
|
|
||||||
} else if (!coupon.is_valid) {
|
|
||||||
return '已失效'
|
|
||||||
} else if (new Date(coupon.expire_at) < new Date()) {
|
|
||||||
return '已过期'
|
|
||||||
}
|
|
||||||
return '未知'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 切换标签页
|
|
||||||
const changeTab = (tab: string) => {
|
|
||||||
activeTab.value = tab
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用优惠券
|
|
||||||
const useCoupon = (coupon: any) => {
|
|
||||||
// 如果是从订单页面跳转过来的,返回选择的优惠券
|
|
||||||
const pages = getCurrentPages()
|
|
||||||
const prevPage = pages[pages.length - 2]
|
|
||||||
|
|
||||||
if (prevPage && prevPage.route === 'pages/mall/consumer/checkout') {
|
|
||||||
uni.$emit('couponSelected', coupon)
|
|
||||||
uni.navigateBack()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 否则跳转到商品列表页
|
|
||||||
uni.navigateTo({
|
|
||||||
url: '/pages/mall/consumer/index'
|
url: '/pages/mall/consumer/index'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 领取优惠券
|
|
||||||
const receiveCoupon = async (coupon: any) => {
|
|
||||||
const userId = getCurrentUserId()
|
|
||||||
if (!userId) return
|
|
||||||
|
|
||||||
// 检查是否已领取
|
|
||||||
const { data: existingCoupons, error: checkError } = await supa
|
|
||||||
.from('user_coupons')
|
|
||||||
.select('id')
|
|
||||||
.eq('user_id', userId)
|
|
||||||
.eq('template_id', coupon.id)
|
|
||||||
|
|
||||||
if (checkError !== null) {
|
|
||||||
console.error('检查优惠券失败:', checkError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingCoupons && existingCoupons.length >= coupon.per_user_limit) {
|
|
||||||
uni.showToast({
|
|
||||||
title: '已达领取上限',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查库存
|
|
||||||
if (coupon.used_quantity >= coupon.total_quantity) {
|
|
||||||
uni.showToast({
|
|
||||||
title: '优惠券已领完',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成优惠券码
|
|
||||||
const couponCode = generateCouponCode()
|
|
||||||
const expireAt = coupon.end_time
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { error: insertError } = await supa
|
|
||||||
.from('user_coupons')
|
|
||||||
.insert({
|
|
||||||
user_id: userId,
|
|
||||||
template_id: coupon.id,
|
|
||||||
coupon_code: couponCode,
|
|
||||||
expire_at: expireAt,
|
|
||||||
status: 1,
|
|
||||||
is_valid: true
|
|
||||||
})
|
|
||||||
|
|
||||||
if (insertError !== null) {
|
|
||||||
console.error('领取优惠券失败:', insertError)
|
|
||||||
uni.showToast({
|
|
||||||
title: '领取失败',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新优惠券模板的已领取数量
|
|
||||||
const { error: updateError } = await supa
|
|
||||||
.from('coupon_templates')
|
|
||||||
.update({ used_quantity: coupon.used_quantity + 1 })
|
|
||||||
.eq('id', coupon.id)
|
|
||||||
|
|
||||||
if (updateError !== null) {
|
|
||||||
console.error('更新优惠券数量失败:', updateError)
|
|
||||||
}
|
|
||||||
|
|
||||||
uni.showToast({
|
|
||||||
title: '领取成功',
|
|
||||||
icon: 'success'
|
|
||||||
})
|
|
||||||
|
|
||||||
// 重新加载数据
|
|
||||||
loadCoupons()
|
|
||||||
} catch (err) {
|
|
||||||
console.error('领取优惠券异常:', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成优惠券码
|
|
||||||
const generateCouponCode = (): string => {
|
|
||||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
|
||||||
let result = ''
|
|
||||||
for (let i = 0; i < 12; i++) {
|
|
||||||
result += chars.charAt(Math.floor(Math.random() * chars.length))
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// 跳转到领券中心
|
|
||||||
const goToCouponCenter = () => {
|
|
||||||
uni.navigateTo({
|
|
||||||
url: '/pages/mall/consumer/coupon-center'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style>
|
||||||
.coupons-page {
|
.coupons-page {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100vh;
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.coupons-header {
|
|
||||||
background-color: #ffffff;
|
|
||||||
border-bottom: 1px solid #e5e5e5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-tabs {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-tab {
|
|
||||||
flex: 1;
|
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
text-align: center;
|
background-color: #f5f5f5;
|
||||||
position: relative;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-tab.active {
|
.empty-state {
|
||||||
color: #007aff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-tab.active::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 2px;
|
|
||||||
background-color: #007aff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-text {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #666666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-tab.active .tab-text {
|
|
||||||
color: #007aff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.coupons-list {
|
|
||||||
flex: 1;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-coupons {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 80px 20px;
|
padding-top: 100px;
|
||||||
background-color: #ffffff;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-icon {
|
.empty-icon {
|
||||||
font-size: 80px;
|
font-size: 60px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-text {
|
.empty-text {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: #666666;
|
color: #999;
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-subtext {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #999999;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.get-coupons-btn {
|
|
||||||
background-color: #007aff;
|
|
||||||
color: #ffffff;
|
|
||||||
padding: 10px 40px;
|
|
||||||
border-radius: 25px;
|
|
||||||
font-size: 14px;
|
|
||||||
border: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.coupon-item {
|
.coupon-item {
|
||||||
background-color: #ffffff;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
background-color: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.coupon-left {
|
.coupon-left {
|
||||||
width: 100px;
|
width: 100px;
|
||||||
background: linear-gradient(135deg, #ff6b6b, #ffa726);
|
background: linear-gradient(135deg, #FF9800, #FF5722);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 20px 10px;
|
color: white;
|
||||||
color: #ffffff;
|
padding: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.coupon-value {
|
.coupon-amount {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.coupon-condition {
|
.coupon-type {
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
opacity: 0.9;
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.coupon-right {
|
.coupon-right {
|
||||||
@@ -532,59 +124,26 @@ const goToCouponCenter = () => {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.coupon-info {
|
.coupon-title {
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.coupon-name {
|
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #333333;
|
color: #333;
|
||||||
margin-bottom: 5px;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.coupon-desc {
|
|
||||||
font-size: 13px;
|
|
||||||
color: #666666;
|
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
display: block;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.coupon-time {
|
.coupon-expiry {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #999999;
|
color: #999;
|
||||||
display: block;
|
margin-bottom: 10px;
|
||||||
}
|
|
||||||
|
|
||||||
.coupon-actions {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.use-btn,
|
|
||||||
.receive-btn {
|
|
||||||
padding: 8px 20px;
|
|
||||||
border-radius: 15px;
|
|
||||||
font-size: 12px;
|
|
||||||
border: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.use-btn {
|
.use-btn {
|
||||||
background-color: #007aff;
|
align-self: flex-end;
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.receive-btn {
|
|
||||||
background-color: #ff4757;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-text {
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #999999;
|
background-color: #FF5722;
|
||||||
font-style: italic;
|
color: white;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 15px;
|
||||||
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,694 +1,269 @@
|
|||||||
<!-- 收藏页面 -->
|
|
||||||
<template>
|
<template>
|
||||||
<view class="favorites-page">
|
<view class="favorites-page">
|
||||||
<!-- 顶部栏 -->
|
<view class="product-grid">
|
||||||
<view class="favorites-header">
|
<view v-if="favorites.length === 0" class="empty-state">
|
||||||
<view class="header-title">
|
|
||||||
<text class="title-text">我的收藏</text>
|
|
||||||
</view>
|
|
||||||
<view v-if="favorites.length > 0" class="edit-btn" @click="toggleEditMode">
|
|
||||||
<text class="edit-text">{{ isEditMode ? '完成' : '编辑' }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 标签页 -->
|
|
||||||
<view class="favorites-tabs">
|
|
||||||
<view :class="['favorites-tab', { active: activeTab === 'product' }]" @click="changeTab('product')">
|
|
||||||
<text class="tab-text">商品</text>
|
|
||||||
</view>
|
|
||||||
<view :class="['favorites-tab', { active: activeTab === 'shop' }]" @click="changeTab('shop')">
|
|
||||||
<text class="tab-text">店铺</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 收藏内容 -->
|
|
||||||
<scroll-view class="favorites-content" scroll-y @scrolltolower="loadMore">
|
|
||||||
<!-- 空状态 -->
|
|
||||||
<view v-if="favorites.length === 0 && !isLoading" class="empty-favorites">
|
|
||||||
<text class="empty-icon">❤️</text>
|
<text class="empty-icon">❤️</text>
|
||||||
<text class="empty-text">{{ getEmptyText() }}</text>
|
<text class="empty-text">暂无收藏商品</text>
|
||||||
<text class="empty-subtext">{{ getEmptySubtext() }}</text>
|
|
||||||
<button class="go-shopping-btn" @click="goShopping">去逛逛</button>
|
<button class="go-shopping-btn" @click="goShopping">去逛逛</button>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 商品收藏 -->
|
<view v-else v-for="(product, index) in favorites" :key="index" class="product-item" @click="goToDetail(product.id)">
|
||||||
<view v-if="activeTab === 'product'" class="product-favorites">
|
<image :src="product.image" class="product-image" mode="aspectFill" />
|
||||||
<view v-for="item in favorites" :key="item.id" class="product-item">
|
|
||||||
<view v-if="isEditMode" class="item-selector" @click="toggleSelect(item)">
|
|
||||||
<view :class="['select-icon', { selected: item.selected }]">
|
|
||||||
<text v-if="item.selected" class="icon-text">✓</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<view class="item-content" @click="viewProduct(item)">
|
|
||||||
<image class="product-image" :src="getProductImage(item)" />
|
|
||||||
<view class="product-info">
|
<view class="product-info">
|
||||||
<text class="product-name">{{ item.product?.name || '商品已下架' }}</text>
|
<text class="product-name">{{ product.name }}</text>
|
||||||
<text class="product-price">¥{{ item.product?.price || 0 }}</text>
|
<text class="product-price">¥{{ product.price }}</text>
|
||||||
<view class="product-meta">
|
<view class="product-footer">
|
||||||
<text class="meta-text">收藏时间: {{ formatTime(item.created_at) }}</text>
|
<text class="product-sales">已售 {{ product.sales }}</text>
|
||||||
|
<view class="action-btns">
|
||||||
|
<view class="cart-btn" @click.stop="addToCart(product)">
|
||||||
|
<text class="cart-icon">🛒</text>
|
||||||
|
</view>
|
||||||
|
<view class="remove-btn" @click.stop="removeFavorite(product.id)">
|
||||||
|
<text class="remove-icon">🗑️</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 店铺收藏 -->
|
|
||||||
<view v-if="activeTab === 'shop'" class="shop-favorites">
|
|
||||||
<view v-for="item in favorites" :key="item.id" class="shop-item">
|
|
||||||
<view v-if="isEditMode" class="item-selector" @click="toggleSelect(item)">
|
|
||||||
<view :class="['select-icon', { selected: item.selected }]">
|
|
||||||
<text v-if="item.selected" class="icon-text">✓</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<view class="item-content" @click="viewShop(item)">
|
|
||||||
<image class="shop-logo" :src="item.shop?.shop_logo || '/static/default-shop.png'" />
|
|
||||||
<view class="shop-info">
|
|
||||||
<text class="shop-name">{{ item.shop?.shop_name || '店铺已关闭' }}</text>
|
|
||||||
<view class="shop-rating">
|
|
||||||
<text class="rating-text">评分: {{ item.shop?.rating?.toFixed(1) || '0.0' }}</text>
|
|
||||||
<text class="sales-text">销量: {{ item.shop?.total_sales || 0 }}</text>
|
|
||||||
</view>
|
|
||||||
<view class="shop-meta">
|
|
||||||
<text class="meta-text">收藏时间: {{ formatTime(item.created_at) }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 加载更多 -->
|
|
||||||
<view v-if="isLoading" class="loading-more">
|
|
||||||
<text class="loading-text">加载中...</text>
|
|
||||||
</view>
|
|
||||||
<view v-if="!hasMore && favorites.length > 0" class="no-more">
|
|
||||||
<text class="no-more-text">没有更多了</text>
|
|
||||||
</view>
|
|
||||||
</scroll-view>
|
|
||||||
|
|
||||||
<!-- 编辑操作栏 -->
|
|
||||||
<view v-if="isEditMode && favorites.length > 0" class="edit-bar">
|
|
||||||
<view class="select-all" @click="toggleSelectAll">
|
|
||||||
<view :class="['all-select-icon', { selected: isAllSelected }]">
|
|
||||||
<text v-if="isAllSelected" class="icon-text">✓</text>
|
|
||||||
</view>
|
|
||||||
<text class="select-all-text">全选</text>
|
|
||||||
</view>
|
|
||||||
<view class="delete-btn" @click="deleteSelected">
|
|
||||||
<text class="delete-text">删除({{ selectedCount }})</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import { ref, onMounted, computed, watch } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import supa from '@/components/supadb/aksupainstance.uts'
|
|
||||||
|
|
||||||
type FavoriteType = {
|
type Product = {
|
||||||
id: string
|
|
||||||
user_id: string
|
|
||||||
product_id: string | null
|
|
||||||
merchant_id: string | null
|
|
||||||
type: string // 'product' | 'shop'
|
|
||||||
created_at: string
|
|
||||||
selected?: boolean
|
|
||||||
product?: {
|
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
price: number
|
price: number
|
||||||
images: string[]
|
image: string
|
||||||
status: number
|
sales: number
|
||||||
}
|
shopId?: string
|
||||||
shop?: {
|
shopName?: string
|
||||||
id: string
|
|
||||||
shop_name: string
|
|
||||||
shop_logo: string
|
|
||||||
rating: number
|
|
||||||
total_sales: number
|
|
||||||
shop_status: number
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeTab = ref<string>('product')
|
const favorites = ref<Product[]>([])
|
||||||
const favorites = ref<Array<FavoriteType>>([])
|
|
||||||
const isEditMode = ref<boolean>(false)
|
|
||||||
const isLoading = ref<boolean>(false)
|
|
||||||
const currentPage = ref<number>(1)
|
|
||||||
const pageSize = ref<number>(20)
|
|
||||||
const hasMore = ref<boolean>(true)
|
|
||||||
|
|
||||||
// 计算属性
|
|
||||||
const selectedCount = computed(() => {
|
|
||||||
return favorites.value.filter(item => item.selected).length
|
|
||||||
})
|
|
||||||
|
|
||||||
const isAllSelected = computed(() => {
|
|
||||||
return favorites.value.length > 0 && favorites.value.every(item => item.selected)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 监听标签页变化
|
|
||||||
watch(activeTab, () => {
|
|
||||||
resetData()
|
|
||||||
loadFavorites()
|
|
||||||
})
|
|
||||||
|
|
||||||
// 生命周期
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadFavorites()
|
loadFavorites()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 重置数据
|
const addToCart = (product: Product) => {
|
||||||
const resetData = () => {
|
// 获取现有购物车数据
|
||||||
favorites.value = []
|
const cartData = uni.getStorageSync('cart')
|
||||||
currentPage.value = 1
|
let cartItems: any[] = []
|
||||||
hasMore.value = true
|
|
||||||
isEditMode.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载收藏数据
|
|
||||||
const loadFavorites = async (loadMore: boolean = false) => {
|
|
||||||
if (isLoading.value || (!hasMore.value && loadMore)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoading.value = true
|
|
||||||
|
|
||||||
|
if (cartData) {
|
||||||
try {
|
try {
|
||||||
const userId = getCurrentUserId()
|
cartItems = JSON.parse(cartData as string) as any[]
|
||||||
if (!userId) {
|
} catch (e) {
|
||||||
uni.navigateTo({
|
console.error('解析购物车数据失败', e)
|
||||||
url: '/pages/user/login'
|
}
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const page = loadMore ? currentPage.value + 1 : 1
|
// 检查商品是否已存在
|
||||||
|
const existingItem = cartItems.find((item: any) => item.id === product.id)
|
||||||
|
|
||||||
let query = supa
|
if (existingItem) {
|
||||||
.from('user_favorites')
|
existingItem.quantity++
|
||||||
.select(`
|
|
||||||
*,
|
|
||||||
product:product_id(*),
|
|
||||||
shop:merchant_id(*)
|
|
||||||
`)
|
|
||||||
.eq('user_id', userId)
|
|
||||||
.eq('type', activeTab.value)
|
|
||||||
.order('created_at', { ascending: false })
|
|
||||||
|
|
||||||
// 分页
|
|
||||||
query = query.range((page - 1) * pageSize.value, page * pageSize.value - 1)
|
|
||||||
|
|
||||||
const { data, error } = await query
|
|
||||||
|
|
||||||
if (error !== null) {
|
|
||||||
console.error('加载收藏失败:', error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const newFavorites = (data || []).map((item: any) => ({
|
|
||||||
...item,
|
|
||||||
selected: false
|
|
||||||
}))
|
|
||||||
|
|
||||||
if (loadMore) {
|
|
||||||
favorites.value.push(...newFavorites)
|
|
||||||
currentPage.value = page
|
|
||||||
} else {
|
} else {
|
||||||
favorites.value = newFavorites
|
// 添加新商品
|
||||||
currentPage.value = 1
|
cartItems.push({
|
||||||
}
|
id: product.id,
|
||||||
|
shopId: product.shopId || 'shop_favorite_default',
|
||||||
hasMore.value = newFavorites.length === pageSize.value
|
shopName: product.shopName || '收藏店铺',
|
||||||
} catch (err) {
|
name: product.name,
|
||||||
console.error('加载收藏异常:', err)
|
price: product.price,
|
||||||
} finally {
|
image: product.image,
|
||||||
isLoading.value = false
|
spec: '默认规格',
|
||||||
}
|
quantity: 1,
|
||||||
}
|
selected: true
|
||||||
|
|
||||||
// 获取当前用户ID
|
|
||||||
const getCurrentUserId = (): string => {
|
|
||||||
const userStore = uni.getStorageSync('userInfo')
|
|
||||||
return userStore?.id || ''
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取商品图片
|
|
||||||
const getProductImage = (item: FavoriteType): string => {
|
|
||||||
if (!item.product?.images?.[0]) {
|
|
||||||
return '/static/default-product.png'
|
|
||||||
}
|
|
||||||
return item.product.images[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化时间
|
|
||||||
const formatTime = (timeStr: string): string => {
|
|
||||||
const date = new Date(timeStr)
|
|
||||||
const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
|
||||||
const day = date.getDate().toString().padStart(2, '0')
|
|
||||||
return `${month}-${day}`
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取空状态文本
|
|
||||||
const getEmptyText = (): string => {
|
|
||||||
return activeTab.value === 'product' ? '暂无商品收藏' : '暂无店铺收藏'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取空状态副文本
|
|
||||||
const getEmptySubtext = (): string => {
|
|
||||||
return activeTab.value === 'product' ? '快去收藏喜欢的商品吧' : '快去收藏喜欢的店铺吧'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 切换编辑模式
|
|
||||||
const toggleEditMode = () => {
|
|
||||||
isEditMode.value = !isEditMode.value
|
|
||||||
// 重置选择状态
|
|
||||||
favorites.value.forEach(item => {
|
|
||||||
item.selected = false
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换标签页
|
// 保存回存储
|
||||||
const changeTab = (tab: string) => {
|
uni.setStorageSync('cart', JSON.stringify(cartItems))
|
||||||
activeTab.value = tab
|
|
||||||
}
|
|
||||||
|
|
||||||
// 切换选择状态
|
|
||||||
const toggleSelect = (item: FavoriteType) => {
|
|
||||||
item.selected = !item.selected
|
|
||||||
favorites.value = [...favorites.value]
|
|
||||||
}
|
|
||||||
|
|
||||||
// 全选/取消全选
|
|
||||||
const toggleSelectAll = () => {
|
|
||||||
const newSelectedState = !isAllSelected.value
|
|
||||||
favorites.value.forEach(item => {
|
|
||||||
item.selected = newSelectedState
|
|
||||||
})
|
|
||||||
favorites.value = [...favorites.value]
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除选中项
|
|
||||||
const deleteSelected = async () => {
|
|
||||||
const selectedItems = favorites.value.filter(item => item.selected)
|
|
||||||
if (selectedItems.length === 0) {
|
|
||||||
uni.showToast({
|
|
||||||
title: '请选择要删除的收藏',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
uni.showModal({
|
|
||||||
title: '确认删除',
|
|
||||||
content: `确定要删除选中的${selectedItems.length}个收藏吗?`,
|
|
||||||
success: async (res) => {
|
|
||||||
if (res.confirm) {
|
|
||||||
try {
|
|
||||||
const ids = selectedItems.map(item => item.id)
|
|
||||||
|
|
||||||
const { error } = await supa
|
|
||||||
.from('user_favorites')
|
|
||||||
.delete()
|
|
||||||
.in('id', ids)
|
|
||||||
|
|
||||||
if (error !== null) {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从列表中移除
|
|
||||||
favorites.value = favorites.value.filter(item => !item.selected)
|
|
||||||
|
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '删除成功',
|
title: '已添加到购物车',
|
||||||
icon: 'success'
|
icon: 'success'
|
||||||
})
|
})
|
||||||
|
|
||||||
// 如果删完了,退出编辑模式
|
|
||||||
if (favorites.value.length === 0) {
|
|
||||||
isEditMode.value = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (err) {
|
const loadFavorites = () => {
|
||||||
console.error('删除收藏失败:', err)
|
// 从本地存储获取收藏列表
|
||||||
uni.showToast({
|
const storedFavorites = uni.getStorageSync('favorites')
|
||||||
title: '删除失败',
|
if (storedFavorites) {
|
||||||
icon: 'none'
|
try {
|
||||||
})
|
favorites.value = JSON.parse(storedFavorites as string) as Product[]
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse favorites', e)
|
||||||
|
favorites.value = []
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
}
|
favorites.value = []
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查看商品
|
|
||||||
const viewProduct = (item: FavoriteType) => {
|
|
||||||
if (isEditMode.value) return
|
|
||||||
|
|
||||||
if (!item.product_id || !item.product?.status) {
|
|
||||||
uni.showToast({
|
|
||||||
title: '商品已下架',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
uni.navigateTo({
|
|
||||||
url: `/pages/mall/consumer/product-detail?id=${item.product_id}`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查看店铺
|
|
||||||
const viewShop = (item: FavoriteType) => {
|
|
||||||
if (isEditMode.value) return
|
|
||||||
|
|
||||||
if (!item.merchant_id || !item.shop?.shop_status) {
|
|
||||||
uni.showToast({
|
|
||||||
title: '店铺已关闭',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
uni.navigateTo({
|
|
||||||
url: `/pages/mall/consumer/shop-detail?id=${item.merchant_id}`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载更多
|
|
||||||
const loadMore = () => {
|
|
||||||
if (hasMore.value && !isLoading.value) {
|
|
||||||
loadFavorites(true)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 去逛逛
|
|
||||||
const goShopping = () => {
|
const goShopping = () => {
|
||||||
uni.switchTab({
|
uni.switchTab({
|
||||||
url: '/pages/mall/consumer/index'
|
url: '/pages/mall/consumer/index'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const goToDetail = (id: string) => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/mall/consumer/product-detail?productId=${id}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeFavorite = (id: string) => {
|
||||||
|
uni.showModal({
|
||||||
|
title: '取消收藏',
|
||||||
|
content: '确定要取消收藏该商品吗?',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
const index = favorites.value.findIndex(item => item.id === id)
|
||||||
|
if (index !== -1) {
|
||||||
|
favorites.value.splice(index, 1)
|
||||||
|
uni.setStorageSync('favorites', JSON.stringify(favorites.value))
|
||||||
|
uni.showToast({
|
||||||
|
title: '已取消收藏',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style>
|
||||||
.favorites-page {
|
.favorites-page {
|
||||||
display: flex;
|
padding: 15px;
|
||||||
flex-direction: column;
|
|
||||||
height: 100vh;
|
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.favorites-header {
|
.product-grid {
|
||||||
background-color: #ffffff;
|
|
||||||
padding: 15px;
|
|
||||||
border-bottom: 1px solid #e5e5e5;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-wrap: wrap;
|
||||||
justify-content: space-between;
|
gap: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-title {
|
.empty-state {
|
||||||
flex: 1;
|
width: 100%;
|
||||||
}
|
|
||||||
|
|
||||||
.title-text {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #333333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.edit-btn {
|
|
||||||
padding: 5px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.edit-text {
|
|
||||||
color: #007aff;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.favorites-tabs {
|
|
||||||
background-color: #ffffff;
|
|
||||||
display: flex;
|
|
||||||
border-bottom: 1px solid #e5e5e5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.favorites-tab {
|
|
||||||
flex: 1;
|
|
||||||
padding: 15px;
|
|
||||||
text-align: center;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.favorites-tab.active {
|
|
||||||
color: #007aff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.favorites-tab.active::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 2px;
|
|
||||||
background-color: #007aff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-text {
|
|
||||||
font-size: 16px;
|
|
||||||
color: #666666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.favorites-tab.active .tab-text {
|
|
||||||
color: #007aff;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.favorites-content {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-favorites {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 80px 20px;
|
padding-top: 100px;
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-icon {
|
.empty-icon {
|
||||||
font-size: 80px;
|
font-size: 60px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
color: #ddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-text {
|
.empty-text {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: #666666;
|
color: #999;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 20px;
|
||||||
}
|
|
||||||
|
|
||||||
.empty-subtext {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #999999;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.go-shopping-btn {
|
.go-shopping-btn {
|
||||||
background-color: #007aff;
|
background-color: #ff5000;
|
||||||
color: #ffffff;
|
color: white;
|
||||||
padding: 10px 40px;
|
padding: 8px 24px;
|
||||||
border-radius: 25px;
|
border-radius: 20px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-favorites,
|
.product-item {
|
||||||
.shop-favorites {
|
width: calc(50% - 8px);
|
||||||
padding: 10px;
|
background-color: white;
|
||||||
}
|
|
||||||
|
|
||||||
.product-item,
|
|
||||||
.shop-item {
|
|
||||||
background-color: #ffffff;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
flex-direction: column;
|
||||||
}
|
|
||||||
|
|
||||||
.item-selector {
|
|
||||||
width: 50px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select-icon {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
border: 1px solid #cccccc;
|
|
||||||
border-radius: 10px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select-icon.selected {
|
|
||||||
background-color: #007aff;
|
|
||||||
border-color: #007aff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-text {
|
|
||||||
color: #ffffff;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-content {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
padding: 15px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-image {
|
.product-image {
|
||||||
width: 80px;
|
width: 100%;
|
||||||
height: 80px;
|
height: 170px;
|
||||||
border-radius: 5px;
|
background-color: #f5f5f5;
|
||||||
margin-right: 15px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-info {
|
.product-info {
|
||||||
flex: 1;
|
padding: 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-name {
|
.product-name {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #333333;
|
color: #333;
|
||||||
line-height: 1.4;
|
margin-bottom: 6px;
|
||||||
margin-bottom: 10px;
|
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
height: 40px;
|
||||||
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-price {
|
.product-price {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: #ff4757;
|
color: #ff5000;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-meta {
|
.product-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
margin-top: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.meta-text {
|
.product-sales {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #999999;
|
color: #999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shop-logo {
|
.action-btns {
|
||||||
width: 80px;
|
|
||||||
height: 80px;
|
|
||||||
border-radius: 8px;
|
|
||||||
margin-right: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shop-info {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shop-name {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #333333;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shop-rating {
|
|
||||||
display: flex;
|
|
||||||
gap: 15px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rating-text,
|
|
||||||
.sales-text {
|
|
||||||
font-size: 13px;
|
|
||||||
color: #666666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shop-meta {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-more,
|
.cart-btn, .remove-btn {
|
||||||
.no-more {
|
width: 28px;
|
||||||
padding: 20px;
|
height: 28px;
|
||||||
text-align: center;
|
border-radius: 50%;
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-text,
|
|
||||||
.no-more-text {
|
|
||||||
color: #999999;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.edit-bar {
|
|
||||||
background-color: #ffffff;
|
|
||||||
border-top: 1px solid #e5e5e5;
|
|
||||||
padding: 15px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select-all {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.all-select-icon {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
border: 1px solid #cccccc;
|
|
||||||
border-radius: 10px;
|
|
||||||
margin-right: 10px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.all-select-icon.selected {
|
.cart-btn {
|
||||||
background-color: #007aff;
|
background-color: #ff5000;
|
||||||
border-color: #007aff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-all-text {
|
.cart-icon {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #333333;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.delete-btn {
|
.remove-btn {
|
||||||
background-color: #ff4757;
|
background-color: #f0f0f0;
|
||||||
padding: 10px 20px;
|
|
||||||
border-radius: 15px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.delete-text {
|
.remove-icon {
|
||||||
color: #ffffff;
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -39,17 +39,17 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="item-content" @click="viewProduct(item)">
|
<view class="item-content" @click="viewProduct(item)">
|
||||||
<image class="product-image" :src="getProductImage(item)" />
|
<image class="product-image" :src="item.image" />
|
||||||
<view class="product-info">
|
<view class="product-info">
|
||||||
<text class="product-name">{{ item.product?.name || '商品已下架' }}</text>
|
<text class="product-name">{{ item.name }}</text>
|
||||||
<view class="product-price-row">
|
<view class="product-price-row">
|
||||||
<text class="current-price">¥{{ item.product?.price || 0 }}</text>
|
<text class="current-price">¥{{ item.price }}</text>
|
||||||
<text v-if="item.product?.original_price && item.product.original_price > item.product.price"
|
<text v-if="item.original_price && item.original_price > item.price"
|
||||||
class="original-price">¥{{ item.product.original_price }}</text>
|
class="original-price">¥{{ item.original_price }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="product-meta">
|
<view class="product-meta">
|
||||||
<text class="sales-text">已售{{ item.product?.sales || 0 }}</text>
|
<text class="sales-text">已售{{ item.sales }}</text>
|
||||||
<text class="time-text">{{ formatTime(item.created_at) }}</text>
|
<text class="time-text">{{ formatTime(item.viewTime) }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -83,31 +83,24 @@
|
|||||||
|
|
||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import { ref, onMounted, computed } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import supa from '@/components/supadb/aksupainstance.uts'
|
|
||||||
|
|
||||||
type FootprintType = {
|
type FootprintType = {
|
||||||
id: string
|
|
||||||
user_id: string
|
|
||||||
product_id: string
|
|
||||||
created_at: string
|
|
||||||
selected?: boolean
|
|
||||||
product?: {
|
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
price: number
|
price: number
|
||||||
original_price: number | null
|
original_price?: number
|
||||||
images: string[]
|
image: string
|
||||||
sales: number
|
sales: number
|
||||||
status: number
|
shopId: string
|
||||||
}
|
shopName: string
|
||||||
|
viewTime: number
|
||||||
|
selected?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const footprints = ref<Array<FootprintType>>([])
|
const footprints = ref<Array<FootprintType>>([])
|
||||||
const isEditMode = ref<boolean>(false)
|
const isEditMode = ref<boolean>(false)
|
||||||
const isLoading = ref<boolean>(false)
|
const isLoading = ref<boolean>(false)
|
||||||
const currentPage = ref<number>(1)
|
const hasMore = ref<boolean>(false)
|
||||||
const pageSize = ref<number>(30)
|
|
||||||
const hasMore = ref<boolean>(true)
|
|
||||||
|
|
||||||
// 计算属性
|
// 计算属性
|
||||||
const selectedCount = computed(() => {
|
const selectedCount = computed(() => {
|
||||||
@@ -122,7 +115,7 @@ const groupedFootprints = computed(() => {
|
|||||||
const groups: Record<string, FootprintType[]> = {}
|
const groups: Record<string, FootprintType[]> = {}
|
||||||
|
|
||||||
footprints.value.forEach(item => {
|
footprints.value.forEach(item => {
|
||||||
const date = item.created_at.split('T')[0]
|
const date = new Date(item.viewTime).toDateString()
|
||||||
if (!groups[date]) {
|
if (!groups[date]) {
|
||||||
groups[date] = []
|
groups[date] = []
|
||||||
}
|
}
|
||||||
@@ -138,72 +131,28 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 加载足迹数据
|
// 加载足迹数据
|
||||||
const loadFootprints = async (loadMore: boolean = false) => {
|
const loadFootprints = (loadMore: boolean = false) => {
|
||||||
if (isLoading.value || (!hasMore.value && loadMore)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
|
|
||||||
|
// 从本地存储获取足迹数据
|
||||||
|
const storedFootprints = uni.getStorageSync('footprints')
|
||||||
|
if (storedFootprints) {
|
||||||
try {
|
try {
|
||||||
const userId = getCurrentUserId()
|
const data = JSON.parse(storedFootprints as string) as any[]
|
||||||
if (!userId) {
|
footprints.value = data.map(item => ({
|
||||||
uni.navigateTo({
|
|
||||||
url: '/pages/user/login'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const page = loadMore ? currentPage.value + 1 : 1
|
|
||||||
|
|
||||||
const { data, error } = await supa
|
|
||||||
.from('user_footprints')
|
|
||||||
.select(`
|
|
||||||
*,
|
|
||||||
product:product_id(*)
|
|
||||||
`)
|
|
||||||
.eq('user_id', userId)
|
|
||||||
.order('created_at', { ascending: false })
|
|
||||||
.range((page - 1) * pageSize.value, page * pageSize.value - 1)
|
|
||||||
|
|
||||||
if (error !== null) {
|
|
||||||
console.error('加载足迹失败:', error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const newFootprints = (data || []).map((item: any) => ({
|
|
||||||
...item,
|
...item,
|
||||||
selected: false
|
selected: false
|
||||||
}))
|
}))
|
||||||
|
} catch (e) {
|
||||||
if (loadMore) {
|
console.error('Failed to parse footprints', e)
|
||||||
footprints.value.push(...newFootprints)
|
footprints.value = []
|
||||||
currentPage.value = page
|
}
|
||||||
} else {
|
} else {
|
||||||
footprints.value = newFootprints
|
footprints.value = []
|
||||||
currentPage.value = 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hasMore.value = newFootprints.length === pageSize.value
|
|
||||||
} catch (err) {
|
|
||||||
console.error('加载足迹异常:', err)
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
}
|
hasMore.value = false // 本地存储一次性加载完
|
||||||
}
|
|
||||||
|
|
||||||
// 获取当前用户ID
|
|
||||||
const getCurrentUserId = (): string => {
|
|
||||||
const userStore = uni.getStorageSync('userInfo')
|
|
||||||
return userStore?.id || ''
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取商品图片
|
|
||||||
const getProductImage = (item: FootprintType): string => {
|
|
||||||
if (!item.product?.images?.[0]) {
|
|
||||||
return '/static/default-product.png'
|
|
||||||
}
|
|
||||||
return item.product.images[0]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 格式化日期分组
|
// 格式化日期分组
|
||||||
@@ -225,8 +174,8 @@ const formatGroupDate = (dateStr: string): string => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 格式化时间
|
// 格式化时间
|
||||||
const formatTime = (timeStr: string): string => {
|
const formatTime = (timestamp: number): string => {
|
||||||
const date = new Date(timeStr)
|
const date = new Date(timestamp)
|
||||||
const hours = date.getHours().toString().padStart(2, '0')
|
const hours = date.getHours().toString().padStart(2, '0')
|
||||||
const minutes = date.getMinutes().toString().padStart(2, '0')
|
const minutes = date.getMinutes().toString().padStart(2, '0')
|
||||||
return `${hours}:${minutes}`
|
return `${hours}:${minutes}`
|
||||||
@@ -248,35 +197,15 @@ const clearAll = () => {
|
|||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: '清空足迹',
|
title: '清空足迹',
|
||||||
content: '确定要清空所有浏览记录吗?',
|
content: '确定要清空所有浏览记录吗?',
|
||||||
success: async (res) => {
|
success: (res) => {
|
||||||
if (res.confirm) {
|
if (res.confirm) {
|
||||||
try {
|
|
||||||
const userId = getCurrentUserId()
|
|
||||||
if (!userId) return
|
|
||||||
|
|
||||||
const { error } = await supa
|
|
||||||
.from('user_footprints')
|
|
||||||
.delete()
|
|
||||||
.eq('user_id', userId)
|
|
||||||
|
|
||||||
if (error !== null) {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
footprints.value = []
|
footprints.value = []
|
||||||
|
uni.removeStorageSync('footprints')
|
||||||
|
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '已清空',
|
title: '已清空',
|
||||||
icon: 'success'
|
icon: 'success'
|
||||||
})
|
})
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
console.error('清空足迹失败:', err)
|
|
||||||
uni.showToast({
|
|
||||||
title: '清空失败',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -289,8 +218,8 @@ const toggleSelect = (item: FootprintType) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 切换分组全选
|
// 切换分组全选
|
||||||
const toggleGroupSelect = (date: string) => {
|
const toggleGroupSelect = (dateStr: string) => {
|
||||||
const group = groupedFootprints.value[date]
|
const group = groupedFootprints.value[dateStr]
|
||||||
if (!group) return
|
if (!group) return
|
||||||
|
|
||||||
const isAllSelected = group.every(item => item.selected)
|
const isAllSelected = group.every(item => item.selected)
|
||||||
@@ -304,8 +233,8 @@ const toggleGroupSelect = (date: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 检查组是否全选
|
// 检查组是否全选
|
||||||
const isGroupSelected = (date: string): boolean => {
|
const isGroupSelected = (dateStr: string): boolean => {
|
||||||
const group = groupedFootprints.value[date]
|
const group = groupedFootprints.value[dateStr]
|
||||||
if (!group || group.length === 0) return false
|
if (!group || group.length === 0) return false
|
||||||
return group.every(item => item.selected)
|
return group.every(item => item.selected)
|
||||||
}
|
}
|
||||||
@@ -320,7 +249,7 @@ const toggleSelectAll = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 删除选中项
|
// 删除选中项
|
||||||
const deleteSelected = async () => {
|
const deleteSelected = () => {
|
||||||
const selectedItems = footprints.value.filter(item => item.selected)
|
const selectedItems = footprints.value.filter(item => item.selected)
|
||||||
if (selectedItems.length === 0) {
|
if (selectedItems.length === 0) {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
@@ -333,23 +262,18 @@ const deleteSelected = async () => {
|
|||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: '确认删除',
|
title: '确认删除',
|
||||||
content: `确定要删除选中的${selectedItems.length}条记录吗?`,
|
content: `确定要删除选中的${selectedItems.length}条记录吗?`,
|
||||||
success: async (res) => {
|
success: (res) => {
|
||||||
if (res.confirm) {
|
if (res.confirm) {
|
||||||
try {
|
|
||||||
const ids = selectedItems.map(item => item.id)
|
|
||||||
|
|
||||||
const { error } = await supa
|
|
||||||
.from('user_footprints')
|
|
||||||
.delete()
|
|
||||||
.in('id', ids)
|
|
||||||
|
|
||||||
if (error !== null) {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从列表中移除
|
// 从列表中移除
|
||||||
footprints.value = footprints.value.filter(item => !item.selected)
|
footprints.value = footprints.value.filter(item => !item.selected)
|
||||||
|
|
||||||
|
// 保存回本地存储
|
||||||
|
const dataToSave = footprints.value.map(item => {
|
||||||
|
const { selected, ...rest } = item
|
||||||
|
return rest
|
||||||
|
})
|
||||||
|
uni.setStorageSync('footprints', JSON.stringify(dataToSave))
|
||||||
|
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '删除成功',
|
title: '删除成功',
|
||||||
icon: 'success'
|
icon: 'success'
|
||||||
@@ -359,14 +283,6 @@ const deleteSelected = async () => {
|
|||||||
if (footprints.value.length === 0) {
|
if (footprints.value.length === 0) {
|
||||||
isEditMode.value = false
|
isEditMode.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
console.error('删除足迹失败:', err)
|
|
||||||
uni.showToast({
|
|
||||||
title: '删除失败',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -376,24 +292,14 @@ const deleteSelected = async () => {
|
|||||||
const viewProduct = (item: FootprintType) => {
|
const viewProduct = (item: FootprintType) => {
|
||||||
if (isEditMode.value) return
|
if (isEditMode.value) return
|
||||||
|
|
||||||
if (!item.product_id || !item.product?.status) {
|
|
||||||
uni.showToast({
|
|
||||||
title: '商品已下架',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: `/pages/mall/consumer/product-detail?id=${item.product_id}`
|
url: `/pages/mall/consumer/product-detail?productId=${item.id}`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载更多
|
// 加载更多
|
||||||
const loadMore = () => {
|
const loadMore = () => {
|
||||||
if (hasMore.value && !isLoading.value) {
|
// 本地存储模式下暂不需要加载更多逻辑
|
||||||
loadFootprints(true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 去逛逛
|
// 去逛逛
|
||||||
|
|||||||
@@ -6,40 +6,34 @@
|
|||||||
class="smart-navbar"
|
class="smart-navbar"
|
||||||
:style="{
|
:style="{
|
||||||
paddingTop: statusBarHeight + 'px',
|
paddingTop: statusBarHeight + 'px',
|
||||||
transform: showNavbar ? 'translateY(0)' : 'translateY(-100%)',
|
transform: showNavbar ? 'translateY(0)' : 'translateY(-100%)'
|
||||||
opacity: showNavbar ? 1 : 0
|
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<view class="nav-container">
|
<view class="search-container">
|
||||||
<!-- 品牌标识区域 - 响应式显示 -->
|
<view class="search-box" @click="navigateToSearch" :style="{ height: '30px' }">
|
||||||
<view class="brand-section">
|
<!-- 模拟输入框 -->
|
||||||
<!-- 品牌标识 - 在电脑端和平板端显示,小屏幕隐藏 -->
|
<text class="search-placeholder">请输入药品名称、症状或品牌</text>
|
||||||
<view class="brand-logo-area" :class="{ 'hidden-on-mobile': isMobile }">
|
|
||||||
<view class="brand-info">
|
<!-- 扫码图标 -->
|
||||||
<text class="brand-name">康乐医药商城</text>
|
<view class="nav-icon-btn" @click.stop="onScan">
|
||||||
<text class="brand-tag">官方认证·正品保障</text>
|
<text class="nav-icon">🔳</text>
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 导航栏内搜索框 - 自适应平铺 -->
|
<!-- 相机图标 -->
|
||||||
<view class="nav-search-container">
|
<view class="nav-camera-btn" @click.stop="onCamera">
|
||||||
<view class="nav-search" @click="navigateToSearch">
|
<text class="nav-camera-icon">📷</text>
|
||||||
<view class="nav-search-box">
|
|
||||||
<text class="nav-search-icon">🔍</text>
|
|
||||||
<!-- 小屏幕只显示搜索图标,隐藏搜索工具 -->
|
|
||||||
<view class="nav-search-tools" :class="{ 'hidden-on-mobile': isMobile }">
|
|
||||||
<text class="nav-tool-item">语音</text>
|
|
||||||
<text class="nav-tool-item">拍照</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 搜索按钮 -->
|
||||||
|
<view class="nav-inner-search-btn" :style="{ height: '22px' }">
|
||||||
|
<text class="nav-inner-search-text">搜索</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 导航栏占位符 - 确保内容正确布局 -->
|
<!-- 导航栏占位符 - 移除,改为使用 margin-top -->
|
||||||
<view class="navbar-placeholder" :style="{ height: (statusBarHeight + 100) + 'px' }"></view>
|
<!-- <view class="navbar-placeholder" :style="{ height: (statusBarHeight + 44) + 'px' }"></view> -->
|
||||||
|
|
||||||
<!-- 主内容区 -->
|
<!-- 主内容区 -->
|
||||||
<scroll-view
|
<scroll-view
|
||||||
@@ -53,7 +47,7 @@
|
|||||||
@scroll="handleScroll"
|
@scroll="handleScroll"
|
||||||
>
|
>
|
||||||
<!-- 智能健康卡片 -->
|
<!-- 智能健康卡片 -->
|
||||||
<view class="smart-health-card">
|
<view class="smart-health-card" :style="{ marginTop: (statusBarHeight + 44 + 10) + 'px' }">
|
||||||
<view class="health-content">
|
<view class="health-content">
|
||||||
<view class="health-header">
|
<view class="health-header">
|
||||||
<text class="health-title">智能健康助手</text>
|
<text class="health-title">智能健康助手</text>
|
||||||
@@ -239,7 +233,7 @@
|
|||||||
v-for="item in familyItems"
|
v-for="item in familyItems"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
class="family-item"
|
class="family-item"
|
||||||
@click="navigateToCategory(item.categoryId)"
|
@click="navigateToCategory(item)"
|
||||||
>
|
>
|
||||||
<view class="family-icon" :style="{ backgroundColor: item.color }">
|
<view class="family-icon" :style="{ backgroundColor: item.color }">
|
||||||
<text>{{ item.icon }}</text>
|
<text>{{ item.icon }}</text>
|
||||||
@@ -363,6 +357,11 @@ const activeSort = ref('sales')
|
|||||||
const activeFilter = ref('recommend')
|
const activeFilter = ref('recommend')
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
|
|
||||||
|
// 数据源
|
||||||
|
const allProducts = ref<any[]>([])
|
||||||
|
const hotProducts = ref<any[]>([])
|
||||||
|
const recommendedProducts = ref<any[]>([])
|
||||||
|
|
||||||
// 屏幕尺寸检测
|
// 屏幕尺寸检测
|
||||||
const isMobile = ref(false)
|
const isMobile = ref(false)
|
||||||
|
|
||||||
@@ -424,53 +423,100 @@ const healthNews = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
// 热销药品
|
// 初始化数据
|
||||||
const hotProducts = [
|
const initData = () => {
|
||||||
{
|
const manufacturers = ['修正药业', '白云山', '养生堂', '三九医药', '同仁堂', '云南白药', '拜耳', '辉瑞']
|
||||||
id: 'hot1',
|
const names = ['布洛芬', '板蓝根', '维生素C', '胃康灵', '阿莫西林', '连花清瘟', '氨溴索', '氯雷他定', '感冒灵', '健胃消食片', '阿司匹林', '蒙脱石散']
|
||||||
name: '布洛芬缓释胶囊',
|
const tags = ['处方药', '中成药', '止咳化痰', '抗过敏', '感冒发烧', '肠胃用药', '消炎镇痛']
|
||||||
specification: '0.3g*24粒',
|
const featureds = ['医生推荐', '热销爆款', '家庭必备', '季节必备', '店长推荐']
|
||||||
price: 18.5,
|
|
||||||
originalPrice: 25.8,
|
const products = [] as any[]
|
||||||
image: 'https://picsum.photos/300/300?random=medicine1',
|
for (let i = 0; i < 50; i++) {
|
||||||
manufacturer: '修正药业',
|
const nameIdx = Math.floor(Math.random() * names.length)
|
||||||
sales: 2560,
|
const name = names[nameIdx]
|
||||||
badge: '热销'
|
const price = parseFloat((10 + Math.random() * 100).toFixed(1))
|
||||||
},
|
const originalPrice = parseFloat((price * (1.1 + Math.random() * 0.5)).toFixed(1))
|
||||||
{
|
const sales = Math.floor(Math.random() * 5000)
|
||||||
id: 'hot2',
|
|
||||||
name: '板蓝根颗粒',
|
// 随机店铺ID,避免全部是同一家
|
||||||
specification: '10g*20袋',
|
const randomShopSuffix = Math.floor(Math.random() * 20) + 1
|
||||||
price: 22.8,
|
|
||||||
originalPrice: 29.9,
|
products.push({
|
||||||
image: 'https://picsum.photos/300/300?random=medicine2',
|
id: `prod_${i}`,
|
||||||
manufacturer: '白云山',
|
shopId: `shop_${randomShopSuffix}`,
|
||||||
sales: 1890,
|
shopName: manufacturers[Math.floor(Math.random() * manufacturers.length)] + '官方旗舰店',
|
||||||
badge: '推荐'
|
name: name + (Math.random() > 0.5 ? '胶囊' : '颗粒'),
|
||||||
},
|
specification: Math.random() > 0.5 ? '0.3g*24粒' : '10g*10袋',
|
||||||
{
|
price: price,
|
||||||
id: 'hot3',
|
originalPrice: originalPrice,
|
||||||
name: '维生素C片',
|
image: '/static/images/default-product.png',
|
||||||
specification: '100mg*100片',
|
manufacturer: manufacturers[Math.floor(Math.random() * manufacturers.length)],
|
||||||
price: 15.9,
|
sales: sales,
|
||||||
originalPrice: 19.9,
|
rating: (3.5 + Math.random() * 1.5).toFixed(1),
|
||||||
image: 'https://picsum.photos/300/300?random=medicine3',
|
reviews: Math.floor(Math.random() * 500),
|
||||||
manufacturer: '养生堂',
|
tag: tags[Math.floor(Math.random() * tags.length)],
|
||||||
sales: 1420,
|
featured: Math.random() > 0.7 ? featureds[Math.floor(Math.random() * featureds.length)] : '',
|
||||||
badge: '特价'
|
badge: sales > 3000 ? '热销' : (price < 20 ? '特价' : (Math.random() > 0.8 ? '新品' : '')),
|
||||||
},
|
// Attributes for filtering
|
||||||
{
|
isNew: Math.random() > 0.8,
|
||||||
id: 'hot4',
|
isRecommend: Math.random() > 0.6,
|
||||||
name: '胃康灵胶囊',
|
isHot: sales > 2000,
|
||||||
specification: '0.4g*24粒',
|
isDiscount: (originalPrice - price) > 15,
|
||||||
price: 32.8,
|
isQuality: price > 60
|
||||||
originalPrice: 38.5,
|
})
|
||||||
image: 'https://picsum.photos/300/300?random=medicine4',
|
}
|
||||||
manufacturer: '三九医药',
|
allProducts.value = products
|
||||||
sales: 890,
|
|
||||||
badge: '新品'
|
filterHotProducts()
|
||||||
|
filterRecommendedProducts()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 筛选热销商品
|
||||||
|
const filterHotProducts = () => {
|
||||||
|
let list = [...allProducts.value]
|
||||||
|
|
||||||
|
if (activeSort.value === 'sales') {
|
||||||
|
list.sort((a, b) => b.sales - a.sales)
|
||||||
|
} else if (activeSort.value === 'price') {
|
||||||
|
list.sort((a, b) => a.price - b.price)
|
||||||
|
} else if (activeSort.value === 'new') {
|
||||||
|
list = list.filter(p => p.isNew)
|
||||||
|
} else if (activeSort.value === 'recommend') {
|
||||||
|
list = list.filter(p => p.isRecommend)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果筛选后数量不足4个,补足
|
||||||
|
if (list.length < 4) {
|
||||||
|
const remaining = allProducts.value.filter(p => !list.includes(p))
|
||||||
|
list = [...list, ...remaining]
|
||||||
|
}
|
||||||
|
|
||||||
|
hotProducts.value = list.slice(0, 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 筛选推荐商品
|
||||||
|
const filterRecommendedProducts = () => {
|
||||||
|
let list = [...allProducts.value]
|
||||||
|
|
||||||
|
if (activeFilter.value === 'hot') {
|
||||||
|
list = list.filter(p => p.isHot)
|
||||||
|
} else if (activeFilter.value === 'discount') {
|
||||||
|
list = list.filter(p => p.isDiscount)
|
||||||
|
} else if (activeFilter.value === 'quality') {
|
||||||
|
list = list.filter(p => p.isQuality)
|
||||||
|
} else {
|
||||||
|
// 默认随机排序
|
||||||
|
list.sort(() => Math.random() - 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果筛选后数量不足4个,补足
|
||||||
|
if (list.length < 4) {
|
||||||
|
const remaining = allProducts.value.filter(p => !list.includes(p))
|
||||||
|
list = [...list, ...remaining]
|
||||||
|
}
|
||||||
|
|
||||||
|
recommendedProducts.value = list.slice(0, 4)
|
||||||
}
|
}
|
||||||
]
|
|
||||||
|
|
||||||
// 家庭常备药
|
// 家庭常备药
|
||||||
const familyItems = [
|
const familyItems = [
|
||||||
@@ -524,61 +570,10 @@ const familyItems = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
// 推荐商品
|
|
||||||
const recommendedProducts = [
|
|
||||||
{
|
|
||||||
id: 'rec1',
|
|
||||||
name: '阿莫西林胶囊',
|
|
||||||
specification: '0.25g*24粒',
|
|
||||||
price: 28.5,
|
|
||||||
originalPrice: 35.0,
|
|
||||||
image: 'https://picsum.photos/350/350?random=rec1',
|
|
||||||
rating: 4.8,
|
|
||||||
reviews: 156,
|
|
||||||
tag: '处方药',
|
|
||||||
featured: '医生推荐'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'rec2',
|
|
||||||
name: '连花清瘟胶囊',
|
|
||||||
specification: '0.35g*36粒',
|
|
||||||
price: 42.8,
|
|
||||||
originalPrice: 48.0,
|
|
||||||
image: 'https://picsum.photos/350/350?random=rec2',
|
|
||||||
rating: 4.9,
|
|
||||||
reviews: 289,
|
|
||||||
tag: '中成药',
|
|
||||||
featured: '热销爆款'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'rec3',
|
|
||||||
name: '盐酸氨溴索口服液',
|
|
||||||
specification: '100ml',
|
|
||||||
price: 35.9,
|
|
||||||
originalPrice: 42.0,
|
|
||||||
image: 'https://picsum.photos/350/350?random=rec3',
|
|
||||||
rating: 4.7,
|
|
||||||
reviews: 132,
|
|
||||||
tag: '止咳化痰',
|
|
||||||
featured: '家庭必备'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'rec4',
|
|
||||||
name: '氯雷他定片',
|
|
||||||
specification: '10mg*6片',
|
|
||||||
price: 18.6,
|
|
||||||
originalPrice: 22.0,
|
|
||||||
image: 'https://picsum.photos/350/350?random=rec4',
|
|
||||||
rating: 4.6,
|
|
||||||
reviews: 98,
|
|
||||||
tag: '抗过敏',
|
|
||||||
featured: '季节必备'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
// 生命周期
|
// 生命周期
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
initPage()
|
initPage()
|
||||||
|
initData()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 页面显示时重置状态
|
// 页面显示时重置状态
|
||||||
@@ -693,13 +688,13 @@ const switchCategory = (category: any) => {
|
|||||||
// 切换排序
|
// 切换排序
|
||||||
const switchSort = (sortId: string) => {
|
const switchSort = (sortId: string) => {
|
||||||
activeSort.value = sortId
|
activeSort.value = sortId
|
||||||
// 这里可以添加排序逻辑
|
filterHotProducts()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换筛选器
|
// 切换筛选器
|
||||||
const switchFilter = (filterId: string) => {
|
const switchFilter = (filterId: string) => {
|
||||||
activeFilter.value = filterId
|
activeFilter.value = filterId
|
||||||
// 这里可以添加筛选逻辑
|
filterRecommendedProducts()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查看新闻详情
|
// 查看新闻详情
|
||||||
@@ -747,6 +742,41 @@ const loadMore = () => {
|
|||||||
|
|
||||||
// 添加到购物车
|
// 添加到购物车
|
||||||
const addToCart = (product: any) => {
|
const addToCart = (product: any) => {
|
||||||
|
// 获取现有购物车数据
|
||||||
|
const cartData = uni.getStorageSync('cart')
|
||||||
|
let cartItems: any[] = []
|
||||||
|
|
||||||
|
if (cartData) {
|
||||||
|
try {
|
||||||
|
cartItems = JSON.parse(cartData as string) as any[]
|
||||||
|
} catch (e) {
|
||||||
|
console.error('解析购物车数据失败', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查商品是否已存在
|
||||||
|
const existingItem = cartItems.find((item: any) => item.id === product.id)
|
||||||
|
|
||||||
|
if (existingItem) {
|
||||||
|
existingItem.quantity++
|
||||||
|
} else {
|
||||||
|
// 添加新商品
|
||||||
|
cartItems.push({
|
||||||
|
id: product.id,
|
||||||
|
shopId: product.shopId || 'shop_default',
|
||||||
|
shopName: product.shopName || product.manufacturer || '自营店铺',
|
||||||
|
name: product.name,
|
||||||
|
price: product.price,
|
||||||
|
image: product.image,
|
||||||
|
spec: product.specification || '默认规格',
|
||||||
|
quantity: 1,
|
||||||
|
selected: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存回存储
|
||||||
|
uni.setStorageSync('cart', JSON.stringify(cartItems))
|
||||||
|
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '已添加到购物车',
|
title: '已添加到购物车',
|
||||||
icon: 'success'
|
icon: 'success'
|
||||||
@@ -758,12 +788,12 @@ const navigateToSearch = () => uni.navigateTo({ url: '/pages/mall/consumer/searc
|
|||||||
const navigateToNews = () => uni.navigateTo({ url: '/pages/news/list' })
|
const navigateToNews = () => uni.navigateTo({ url: '/pages/news/list' })
|
||||||
const navigateToProduct = (product: any) => {
|
const navigateToProduct = (product: any) => {
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: `/pages/medicine/detail?id=${product.id}`
|
url: `/pages/mall/consumer/product-detail?productId=${product.id}`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const navigateToCategory = (categoryId: string) => {
|
const navigateToCategory = (item: any) => {
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: `/pages/medicine/category?id=${categoryId}`
|
url: `/pages/mall/consumer/search?keyword=${encodeURIComponent(item.name)}&type=family`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const navigateToConsultation = () => uni.navigateTo({ url: '/pages/medicine/consultation' })
|
const navigateToConsultation = () => uni.navigateTo({ url: '/pages/medicine/consultation' })
|
||||||
@@ -799,7 +829,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
|
|||||||
right: 0;
|
right: 0;
|
||||||
background: linear-gradient(135deg, #4CAF50 0%, #2E7D32 100%);
|
background: linear-gradient(135deg, #4CAF50 0%, #2E7D32 100%);
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
box-shadow: 0 2px 20px rgba(76, 175, 80, 0.15);
|
box-shadow: 0 2px 12px rgba(76, 175, 80, 0.15); /* 调整为与分类页一致 */
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
will-change: transform, opacity;
|
will-change: transform, opacity;
|
||||||
pointer-events: auto; /* 确保导航栏可以交互 */
|
pointer-events: auto; /* 确保导航栏可以交互 */
|
||||||
@@ -807,10 +837,12 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
|
|||||||
-webkit-backface-visibility: hidden;
|
-webkit-backface-visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-container {
|
/* 导航栏搜索框容器内边距调整 */
|
||||||
height: 100px; /* 增加导航栏高度 */
|
.search-container {
|
||||||
|
height: 44px; /* 调整为与消息页一致的高度 */
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: row; /* 显式设置 flex-direction: row */
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
max-width: 1400px;
|
max-width: 1400px;
|
||||||
@@ -818,103 +850,66 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 品牌区域 - 重新设计为自适应平铺 */
|
/* 搜索框 hover 效果 */
|
||||||
.brand-section {
|
.search-box:hover {
|
||||||
display: flex;
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
align-items: center;
|
}
|
||||||
width: 100%;
|
|
||||||
gap: 16px;
|
/* 导航栏搜索框容器内边距调整 */
|
||||||
flex-wrap: nowrap; /* 强制不换行 */
|
.search-box {
|
||||||
justify-content: space-between;
|
flex: 1;
|
||||||
}
|
max-width: 600px;
|
||||||
|
background: #f0f0f0;
|
||||||
/* 品牌标识区域 - 自适应平铺 */
|
border-radius: 20px;
|
||||||
.brand-logo-area {
|
padding: 0 4px 0 12px;
|
||||||
flex-shrink: 0; /* 防止被压缩 */
|
|
||||||
min-width: auto;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 小屏幕隐藏品牌标识 */
|
|
||||||
.brand-logo-area.hidden-on-mobile {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 小屏幕隐藏搜索工具 */
|
|
||||||
.nav-search-tools.hidden-on-mobile {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-name {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: white;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
white-space: nowrap; /* 防止文字换行 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-tag {
|
|
||||||
font-size: 11px;
|
|
||||||
color: rgba(255, 255, 255, 0.9);
|
|
||||||
margin-top: 2px;
|
|
||||||
white-space: nowrap; /* 防止文字换行 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 导航栏搜索框容器 - 自适应平铺 */
|
|
||||||
.nav-search-container {
|
|
||||||
flex: 1; /* 占据剩余空间 */
|
|
||||||
min-width: 0; /* 允许缩小 */
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-search {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-search-box {
|
|
||||||
background: rgba(255, 255, 255, 0.95);
|
|
||||||
border-radius: 25px;
|
|
||||||
padding: 12px 20px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: row; /* UVUE 显式设置 row */
|
||||||
align-items: center;
|
align-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
border: 2px solid transparent;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 32px; /* 减小高度,与顶部高度44px适配,略小于顶部高度 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-search-box:hover {
|
.search-placeholder {
|
||||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
font-size: 14px;
|
||||||
border-color: rgba(255, 255, 255, 0.3);
|
color: #999;
|
||||||
}
|
flex: 1;
|
||||||
|
|
||||||
.nav-search-icon {
|
|
||||||
font-size: 18px;
|
|
||||||
color: #4CAF50;
|
|
||||||
margin-right: 12px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-search-tools {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
margin-left: 12px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-tool-item {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #4CAF50;
|
|
||||||
padding: 4px 10px;
|
|
||||||
background: rgba(76, 175, 80, 0.1);
|
|
||||||
border-radius: 12px;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-icon-btn {
|
||||||
|
padding: 4px 8px 4px 4px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-right: 1px solid #ddd;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 搜索按钮高度微调 */
|
||||||
|
.nav-inner-search-btn {
|
||||||
|
padding: 0 12px; /* 减小内边距 */
|
||||||
|
background-color: #87CEEB; /* 天空蓝 */
|
||||||
|
border-radius: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 24px; /* 随搜索框高度减小而减小 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-inner-search-text {
|
||||||
|
font-size: 12px; /* 字体稍微变小 */
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 导航栏占位符 */
|
/* 导航栏占位符 */
|
||||||
@@ -923,10 +918,27 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-camera-btn {
|
||||||
|
padding: 4px 8px 4px 4px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-right-width: 1px;
|
||||||
|
border-right-style: solid;
|
||||||
|
border-right-color: #ddd;
|
||||||
|
border-right: 1px solid #ddd; /* 修复UVUE样式 */
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-camera-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
/* 主内容区域 */
|
/* 主内容区域 */
|
||||||
.main-scroll {
|
.main-scroll {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 16px;
|
padding: 0 16px 16px;
|
||||||
max-width: 1400px;
|
max-width: 1400px;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
@@ -941,6 +953,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
|
|||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
/* margin-top 由 style 动态控制 */
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1226,25 +1239,27 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
|
|||||||
|
|
||||||
.sort-tabs {
|
.sort-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: row; /* UVUE 显式设置 row */
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: nowrap; /* 强制不换行 */
|
flex-wrap: wrap; /* 允许换行,实现自适应 */
|
||||||
justify-content: space-between; /* 两端对齐 */
|
justify-content: flex-start;
|
||||||
width: 100%; /* 占满宽度 */
|
width: 100%;
|
||||||
margin-top: 12px; /* 增加顶部间距,与标题分开 */
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sort-tab {
|
.sort-tab {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #666;
|
color: #666;
|
||||||
padding: 8px 0; /* 上下padding,左右由flex均分 */
|
padding: 8px 12px; /* 增加左右内边距 */
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
border: 1px solid #e0e0e0;
|
border: 1px solid #e0e0e0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
flex: 1; /* 均分宽度 */
|
flex: 1; /* 均分宽度 */
|
||||||
text-align: center; /* 文字居中 */
|
min-width: 70px; /* 设置最小宽度防止过窄 */
|
||||||
|
text-align: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1480,25 +1495,27 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
|
|||||||
|
|
||||||
.recommend-filters {
|
.recommend-filters {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: row; /* UVUE 显式设置 row */
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: nowrap; /* 强制不换行 */
|
flex-wrap: wrap; /* 允许换行,实现自适应 */
|
||||||
justify-content: space-between; /* 两端对齐 */
|
justify-content: flex-start;
|
||||||
width: 100%; /* 占满宽度 */
|
width: 100%;
|
||||||
margin-top: 12px; /* 增加顶部间距 */
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-item {
|
.filter-item {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #666;
|
color: #666;
|
||||||
padding: 8px 0; /* 上下padding,左右由flex均分 */
|
padding: 8px 12px; /* 增加左右内边距 */
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
border: 1px solid #e0e0e0;
|
border: 1px solid #e0e0e0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
flex: 1; /* 均分宽度 */
|
flex: 1; /* 均分宽度 */
|
||||||
text-align: center; /* 文字居中 */
|
min-width: 80px; /* 设置最小宽度防止过窄 */
|
||||||
|
text-align: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1755,77 +1772,196 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
|
|||||||
|
|
||||||
/* 小屏手机 (小于414px) */
|
/* 小屏手机 (小于414px) */
|
||||||
@media screen and (max-width: 414px) {
|
@media screen and (max-width: 414px) {
|
||||||
.nav-container {
|
.search-container {
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
height: 90px;
|
height: 44px; /* 恢复为44px,与PC和分类页逻辑保持一致 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-section {
|
.search-box {
|
||||||
flex-direction: column;
|
padding: 8px 16px; /* 与分类页保持一致 */
|
||||||
align-items: stretch;
|
|
||||||
gap: 12px;
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-logo-area {
|
|
||||||
min-width: auto;
|
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
flex-shrink: 0;
|
margin: 0;
|
||||||
width: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-info {
|
|
||||||
align-items: center;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-name {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-tag {
|
|
||||||
font-size: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-search-container {
|
|
||||||
flex: 1;
|
|
||||||
width: 100%;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-search-box {
|
|
||||||
padding: 10px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-search-tools {
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-scroll {
|
.main-scroll {
|
||||||
padding: 12px;
|
padding: 0 12px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-grid {
|
.category-grid {
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(5, 1fr); /* 调整为一行5个,正好两行 */
|
||||||
gap: 12px;
|
gap: 8px;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-card {
|
||||||
|
padding: 8px 0;
|
||||||
|
background: transparent; /* 移除卡片背景 */
|
||||||
|
box-shadow: none; /* 移除阴影 */
|
||||||
|
border: none; /* 移除边框 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-card:hover {
|
||||||
|
transform: none; /* 移动端移除悬停效果 */
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-icon {
|
||||||
|
width: 44px; /* 减小图标尺寸 */
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 22px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-icon text {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-name {
|
||||||
|
font-size: 11px; /* 减小文字大小 */
|
||||||
|
font-weight: normal;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-desc {
|
||||||
|
display: none; /* 手机端隐藏描述文字,保持界面整洁 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.services-grid {
|
.services-grid {
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(4, 1fr); /* 服务入口也调整为一行4个 */
|
||||||
gap: 16px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.products-grid {
|
.service-card {
|
||||||
grid-template-columns: 1fr;
|
padding: 10px 4px;
|
||||||
|
background: #f8f9fa; /* 保持淡色背景 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.service-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 20px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-icon text {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-name {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-desc {
|
||||||
|
display: none; /* 隐藏描述 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.products-grid,
|
||||||
.recommend-grid {
|
.recommend-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: repeat(2, 1fr); /* 手机端调整为双列显示 */
|
||||||
|
gap: 8px; /* 减小间距 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info,
|
||||||
|
.product-details {
|
||||||
|
padding: 8px; /* 减小内边距 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-name,
|
||||||
|
.product-title {
|
||||||
|
font-size: 13px; /* 调整字体大小 */
|
||||||
|
height: 36px; /* 限制高度,防止参差不齐 */
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-image,
|
||||||
|
.product-image-container {
|
||||||
|
height: 140px; /* 稍微减小图片高度适配双列 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 手机端商品卡片极简模式(热销 & 推荐) */
|
||||||
|
.hot-products .product-spec,
|
||||||
|
.hot-products .manufacturer,
|
||||||
|
.hot-products .original-price,
|
||||||
|
.hot-products .cart-text,
|
||||||
|
.hot-products .sales-info,
|
||||||
|
.hot-products .product-action, /* 隐藏热销区加购按钮 */
|
||||||
|
.smart-recommend .product-specification,
|
||||||
|
.smart-recommend .product-rating,
|
||||||
|
.smart-recommend .original-price,
|
||||||
|
.smart-recommend .product-actions /* 隐藏推荐区加购按钮 */
|
||||||
|
{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hot-products .product-info,
|
||||||
|
.smart-recommend .product-details {
|
||||||
|
padding: 6px; /* 极小内边距 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.hot-products .product-image,
|
||||||
|
.hot-products .product-image-container,
|
||||||
|
.smart-recommend .product-image,
|
||||||
|
.smart-recommend .product-image-container {
|
||||||
|
height: 110px; /* 进一步减小图片高度 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.hot-products .product-name,
|
||||||
|
.smart-recommend .product-title {
|
||||||
|
margin-bottom: 2px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.3;
|
||||||
|
height: 32px; /* 限制2行高度 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.hot-products .price-section,
|
||||||
|
.smart-recommend .price-section {
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hot-products .price-symbol,
|
||||||
|
.smart-recommend .price-symbol {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #FF5722;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hot-products .price-value,
|
||||||
|
.smart-recommend .price-value {
|
||||||
|
font-size: 14px; /* 字体变小 */
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.family-grid {
|
.family-grid {
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(4, 1fr); /* 家庭常备药也调整为一行4个 */
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.family-item {
|
||||||
|
padding: 8px 4px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.family-icon {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 18px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.family-icon text {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.family-name {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.family-desc {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.news-swiper {
|
.news-swiper {
|
||||||
@@ -1835,16 +1971,19 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
|
|||||||
.sort-tabs,
|
.sort-tabs,
|
||||||
.recommend-filters {
|
.recommend-filters {
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
justify-content: center;
|
justify-content: flex-start; /* 保持左对齐 */
|
||||||
|
overflow-x: auto; /* 允许横向滚动 */
|
||||||
|
flex-wrap: nowrap; /* 禁止换行 */
|
||||||
|
padding-bottom: 4px; /* 滚动条空间 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.sort-tab,
|
.sort-tab,
|
||||||
.filter-item {
|
.filter-item {
|
||||||
padding: 5px 12px;
|
padding: 5px 12px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
flex-grow: 1;
|
flex-shrink: 0; /* 防止被压缩 */
|
||||||
text-align: center;
|
min-width: auto;
|
||||||
min-width: 60px;
|
flex: 0 0 auto; /* 取消均分 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-header {
|
.section-header {
|
||||||
@@ -1865,38 +2004,9 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
|
|||||||
|
|
||||||
/* 中屏手机/小平板 (415px-768px) */
|
/* 中屏手机/小平板 (415px-768px) */
|
||||||
@media screen and (min-width: 415px) and (max-width: 768px) {
|
@media screen and (min-width: 415px) and (max-width: 768px) {
|
||||||
.nav-container {
|
.search-container {
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
height: 95px;
|
height: 44px;
|
||||||
}
|
|
||||||
|
|
||||||
.brand-section {
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: 15px;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-logo-area {
|
|
||||||
min-width: 140px;
|
|
||||||
max-width: 160px;
|
|
||||||
flex-shrink: 1;
|
|
||||||
flex-grow: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-name {
|
|
||||||
font-size: 17px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-search-container {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-search-tools {
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-scroll {
|
.main-scroll {
|
||||||
@@ -1956,10 +2066,10 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
|
|||||||
|
|
||||||
/* 平板设备 (769px-1024px) */
|
/* 平板设备 (769px-1024px) */
|
||||||
@media screen and (min-width: 769px) and (max-width: 1024px) {
|
@media screen and (min-width: 769px) and (max-width: 1024px) {
|
||||||
.nav-container {
|
.search-container {
|
||||||
padding: 0 24px;
|
padding: 0 24px;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: 100px;
|
height: 44px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-search-tools .nav-tool-item {
|
.nav-search-tools .nav-tool-item {
|
||||||
@@ -1968,7 +2078,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
|
|||||||
|
|
||||||
.main-scroll {
|
.main-scroll {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
padding: 20px 24px;
|
padding: 0 24px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-grid {
|
.category-grid {
|
||||||
@@ -1998,17 +2108,18 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
|
|||||||
|
|
||||||
/* 桌面端 (1025px以上) */
|
/* 桌面端 (1025px以上) */
|
||||||
@media screen and (min-width: 1025px) {
|
@media screen and (min-width: 1025px) {
|
||||||
.nav-container {
|
.search-container {
|
||||||
padding: 0 32px;
|
padding: 0 32px;
|
||||||
height: 100px;
|
height: 44px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-scroll {
|
.main-scroll {
|
||||||
padding: 24px 32px;
|
max-width: 100%;
|
||||||
|
padding: 0 32px 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-grid {
|
.category-grid {
|
||||||
grid-template-columns: repeat(5, 1fr);
|
grid-template-columns: repeat(6, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
.services-grid {
|
.services-grid {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,23 @@
|
|||||||
<!-- pages/mall/consumer/messages.uvue -->
|
|
||||||
<template>
|
<template>
|
||||||
<view class="messages-page">
|
<view class="messages-page">
|
||||||
<!-- 顶部标题栏 -->
|
<!-- 智能顶部导航栏 - 与主页保持一致 -->
|
||||||
<view class="messages-header">
|
<view class="smart-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||||
<text class="header-title">消息中心</text>
|
<view class="nav-container">
|
||||||
<view class="header-actions">
|
<text class="nav-title">消息中心</text>
|
||||||
<text class="action-icon" @click="clearAllUnread">📝</text>
|
<view class="nav-actions">
|
||||||
|
<view class="action-btn" @click="clearAllUnread">
|
||||||
|
<text class="action-icon">🧹</text>
|
||||||
|
<text class="action-text">一键已读</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 消息分类标签 - 客服消息放在第一位 -->
|
<!-- 导航栏占位符 -->
|
||||||
|
<view class="navbar-placeholder" :style="{ height: (statusBarHeight + 10) + 'px' }"></view>
|
||||||
|
|
||||||
|
<!-- 消息分类标签 - 固定在顶部,方便随时切换 -->
|
||||||
|
<view class="tabs-container">
|
||||||
<view class="message-tabs">
|
<view class="message-tabs">
|
||||||
<view
|
<view
|
||||||
v-for="tab in messageTabs"
|
v-for="tab in messageTabs"
|
||||||
@@ -18,21 +26,24 @@
|
|||||||
@click="switchTab(tab.id)"
|
@click="switchTab(tab.id)"
|
||||||
>
|
>
|
||||||
<text class="tab-name">{{ tab.name }}</text>
|
<text class="tab-name">{{ tab.name }}</text>
|
||||||
<text v-if="tab.unread > 0" class="tab-badge">{{ tab.unread }}</text>
|
<text v-if="tab.unread > 0" class="tab-badge">{{ tab.unread > 99 ? '99+' : tab.unread }}</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 消息列表 -->
|
<!-- 消息列表内容区 -->
|
||||||
<scroll-view
|
<scroll-view
|
||||||
scroll-y
|
scroll-y
|
||||||
class="messages-content"
|
class="messages-content"
|
||||||
refresher-enabled
|
refresher-enabled
|
||||||
:refresher-triggered="refreshing"
|
:refresher-triggered="refreshing"
|
||||||
@refresherrefresh="onRefresh"
|
@refresherrefresh="onRefresh"
|
||||||
|
:scroll-top="scrollTop"
|
||||||
>
|
>
|
||||||
<!-- 客服消息 - 放在第一位 -->
|
|
||||||
|
<!-- 客服消息 -->
|
||||||
<view v-if="activeTab === 'service'" class="message-section">
|
<view v-if="activeTab === 'service'" class="message-section">
|
||||||
<!-- 在线客服 -->
|
<!-- 在线客服卡片 -->
|
||||||
<view class="customer-service-info">
|
<view class="customer-service-info">
|
||||||
<view class="service-header">
|
<view class="service-header">
|
||||||
<text class="service-title">康乐医药在线客服</text>
|
<text class="service-title">康乐医药在线客服</text>
|
||||||
@@ -171,9 +182,10 @@
|
|||||||
<text class="message-time">{{ message.time }}</text>
|
<text class="message-time">{{ message.time }}</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="message-preview">{{ message.content }}</text>
|
<text class="message-preview">{{ message.content }}</text>
|
||||||
<view v-if="message.coupon" class="coupon-info">
|
<view v-if="message.coupon" class="coupon-info" @click.stop="claimCoupon(message)">
|
||||||
<text class="coupon-text">{{ message.coupon }}优惠券</text>
|
<text class="coupon-text">{{ message.coupon }}优惠券</text>
|
||||||
<text class="coupon-expiry">有效期至 {{ message.expiry }}</text>
|
<text class="coupon-expiry">有效期至 {{ message.expiry }}</text>
|
||||||
|
<text class="coupon-action">{{ message.claimed ? '已领取' : '点击领取' }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -185,6 +197,9 @@
|
|||||||
<text class="empty-title">暂无消息</text>
|
<text class="empty-title">暂无消息</text>
|
||||||
<text class="empty-desc">暂时没有新消息</text>
|
<text class="empty-desc">暂时没有新消息</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 底部安全区域 -->
|
||||||
|
<view class="safe-area"></view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
|
|
||||||
<!-- 底部固定按钮 -->
|
<!-- 底部固定按钮 -->
|
||||||
@@ -200,13 +215,25 @@
|
|||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import { ref, reactive, computed, onMounted } from 'vue'
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||||||
|
|
||||||
// 响应式数据 - 默认显示客服消息
|
// 响应式数据
|
||||||
const activeTab = ref<string>('service')
|
const activeTab = ref<string>('service')
|
||||||
const refreshing = ref<boolean>(false)
|
const refreshing = ref<boolean>(false)
|
||||||
const loading = ref<boolean>(false)
|
const loading = ref<boolean>(false)
|
||||||
const unreadCount = ref<number>(12)
|
const unreadCount = ref<number>(12)
|
||||||
|
const statusBarHeight = ref(0)
|
||||||
|
const scrollTop = ref(0)
|
||||||
|
|
||||||
// 消息分类标签 - 客服消息放在第一位
|
// 初始化页面布局数据
|
||||||
|
const initPage = () => {
|
||||||
|
const systemInfo = uni.getSystemInfoSync()
|
||||||
|
statusBarHeight.value = systemInfo.statusBarHeight || 0
|
||||||
|
|
||||||
|
// 计算滚动区域高度:屏幕高度 - 状态栏 - 导航栏(44) - 标签栏(42)
|
||||||
|
const windowHeight = systemInfo.windowHeight
|
||||||
|
scrollHeight.value = windowHeight - statusBarHeight.value - 44 - 42
|
||||||
|
}
|
||||||
|
|
||||||
|
// 消息分类标签
|
||||||
const messageTabs = reactive([
|
const messageTabs = reactive([
|
||||||
{ id: 'service', name: '客服消息', unread: 5 },
|
{ id: 'service', name: '客服消息', unread: 5 },
|
||||||
{ id: 'system', name: '系统通知', unread: 3 },
|
{ id: 'system', name: '系统通知', unread: 3 },
|
||||||
@@ -214,7 +241,7 @@ const messageTabs = reactive([
|
|||||||
{ id: 'promo', name: '优惠活动', unread: 2 }
|
{ id: 'promo', name: '优惠活动', unread: 2 }
|
||||||
])
|
])
|
||||||
|
|
||||||
// Mock 客服消息数据 - 增强版
|
// Mock 客服消息数据
|
||||||
const serviceMessages = reactive([
|
const serviceMessages = reactive([
|
||||||
{
|
{
|
||||||
id: 'service001',
|
id: 'service001',
|
||||||
@@ -376,7 +403,8 @@ const promoMessages = reactive([
|
|||||||
read: false,
|
read: false,
|
||||||
type: 'promo',
|
type: 'promo',
|
||||||
coupon: '50元',
|
coupon: '50元',
|
||||||
expiry: '2023-11-26'
|
expiry: '2023-11-26',
|
||||||
|
claimed: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'promo002',
|
id: 'promo002',
|
||||||
@@ -386,7 +414,8 @@ const promoMessages = reactive([
|
|||||||
read: true,
|
read: true,
|
||||||
type: 'promo',
|
type: 'promo',
|
||||||
coupon: '满300减50',
|
coupon: '满300减50',
|
||||||
expiry: '2023-11-30'
|
expiry: '2023-11-30',
|
||||||
|
claimed: false
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -403,6 +432,7 @@ const currentMessages = computed(() => {
|
|||||||
|
|
||||||
// 生命周期
|
// 生命周期
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
initPage()
|
||||||
loadMessages()
|
loadMessages()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -420,22 +450,18 @@ const loadMessages = () => {
|
|||||||
const updateUnreadCount = () => {
|
const updateUnreadCount = () => {
|
||||||
let totalUnread = 0
|
let totalUnread = 0
|
||||||
|
|
||||||
// 计算客服消息未读
|
|
||||||
const serviceUnread = serviceMessages.filter(msg => !msg.read).length
|
const serviceUnread = serviceMessages.filter(msg => !msg.read).length
|
||||||
messageTabs[0].unread = serviceUnread
|
messageTabs[0].unread = serviceUnread
|
||||||
totalUnread += serviceUnread
|
totalUnread += serviceUnread
|
||||||
|
|
||||||
// 计算系统消息未读
|
|
||||||
const systemUnread = systemMessages.filter(msg => !msg.read).length
|
const systemUnread = systemMessages.filter(msg => !msg.read).length
|
||||||
messageTabs[1].unread = systemUnread
|
messageTabs[1].unread = systemUnread
|
||||||
totalUnread += systemUnread
|
totalUnread += systemUnread
|
||||||
|
|
||||||
// 计算订单消息未读
|
|
||||||
const orderUnread = orderMessages.filter(msg => !msg.read).length
|
const orderUnread = orderMessages.filter(msg => !msg.read).length
|
||||||
messageTabs[2].unread = orderUnread
|
messageTabs[2].unread = orderUnread
|
||||||
totalUnread += orderUnread
|
totalUnread += orderUnread
|
||||||
|
|
||||||
// 计算优惠活动未读
|
|
||||||
const promoUnread = promoMessages.filter(msg => !msg.read).length
|
const promoUnread = promoMessages.filter(msg => !msg.read).length
|
||||||
messageTabs[3].unread = promoUnread
|
messageTabs[3].unread = promoUnread
|
||||||
totalUnread += promoUnread
|
totalUnread += promoUnread
|
||||||
@@ -446,18 +472,16 @@ const updateUnreadCount = () => {
|
|||||||
// 切换标签
|
// 切换标签
|
||||||
const switchTab = (tabId: string) => {
|
const switchTab = (tabId: string) => {
|
||||||
activeTab.value = tabId
|
activeTab.value = tabId
|
||||||
|
// 切换标签时回到顶部,使用微小变化触发滚动更新
|
||||||
|
scrollTop.value = scrollTop.value === 0 ? 0.01 : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// 开始与客服聊天
|
// 开始与客服聊天
|
||||||
const startChatWithService = (message: any) => {
|
const startChatWithService = (message: any) => {
|
||||||
// 标记为已读
|
|
||||||
message.read = true
|
message.read = true
|
||||||
message.unreadCount = 0
|
message.unreadCount = 0
|
||||||
|
|
||||||
// 更新未读计数
|
|
||||||
updateUnreadCount()
|
updateUnreadCount()
|
||||||
|
|
||||||
// 跳转到聊天页面,传递客服信息
|
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: `/pages/mall/consumer/chat?id=${message.id}&name=${encodeURIComponent(message.title)}&role=${encodeURIComponent(message.role)}`
|
url: `/pages/mall/consumer/chat?id=${message.id}&name=${encodeURIComponent(message.title)}&role=${encodeURIComponent(message.role)}`
|
||||||
})
|
})
|
||||||
@@ -509,6 +533,46 @@ const viewPromoMessage = (message: any) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 领取优惠券
|
||||||
|
const claimCoupon = (message: any) => {
|
||||||
|
if (message.claimed) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '您已领取该优惠券',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
message.claimed = true
|
||||||
|
// 保存领取状态到本地存储,供个人页读取
|
||||||
|
const claimedCouponsCount = uni.getStorageSync('claimedCoupons') || 0
|
||||||
|
uni.setStorageSync('claimedCoupons', (claimedCouponsCount as number) + 1)
|
||||||
|
|
||||||
|
// 保存详细的优惠券信息到 myCoupons 列表
|
||||||
|
const myCoupons = uni.getStorageSync('myCoupons')
|
||||||
|
let couponsList: any[] = []
|
||||||
|
if (myCoupons) {
|
||||||
|
try {
|
||||||
|
couponsList = JSON.parse(myCoupons as string) as any[]
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse myCoupons', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
couponsList.push({
|
||||||
|
title: message.title,
|
||||||
|
amount: message.coupon,
|
||||||
|
expiry: message.expiry,
|
||||||
|
id: message.id
|
||||||
|
})
|
||||||
|
uni.setStorageSync('myCoupons', JSON.stringify(couponsList))
|
||||||
|
|
||||||
|
uni.showToast({
|
||||||
|
title: '领取成功',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 清除所有未读
|
// 清除所有未读
|
||||||
const clearAllUnread = () => {
|
const clearAllUnread = () => {
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
@@ -516,7 +580,6 @@ const clearAllUnread = () => {
|
|||||||
content: '确定要标记所有消息为已读吗?',
|
content: '确定要标记所有消息为已读吗?',
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
if (res.confirm) {
|
if (res.confirm) {
|
||||||
// 标记所有消息为已读
|
|
||||||
serviceMessages.forEach(msg => {
|
serviceMessages.forEach(msg => {
|
||||||
msg.read = true
|
msg.read = true
|
||||||
msg.unreadCount = 0
|
msg.unreadCount = 0
|
||||||
@@ -525,7 +588,6 @@ const clearAllUnread = () => {
|
|||||||
orderMessages.forEach(msg => msg.read = true)
|
orderMessages.forEach(msg => msg.read = true)
|
||||||
promoMessages.forEach(msg => msg.read = true)
|
promoMessages.forEach(msg => msg.read = true)
|
||||||
|
|
||||||
// 更新标签未读数
|
|
||||||
messageTabs.forEach(tab => tab.unread = 0)
|
messageTabs.forEach(tab => tab.unread = 0)
|
||||||
unreadCount.value = 0
|
unreadCount.value = 0
|
||||||
|
|
||||||
@@ -553,52 +615,129 @@ const onRefresh = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
/* 页面结构优化 - 避免双滚动条 */
|
||||||
.messages-page {
|
.messages-page {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
background-color: #f8fafc;
|
background-color: #f8fafc;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
overflow: hidden; /* 关键:防止body滚动 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 头部 */
|
/* 智能导航栏 */
|
||||||
.messages-header {
|
.smart-navbar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
background: linear-gradient(135deg, #4CAF50 0%, #2E7D32 100%);
|
background: linear-gradient(135deg, #4CAF50 0%, #2E7D32 100%);
|
||||||
padding: 15px;
|
z-index: 1000;
|
||||||
|
box-shadow: 0 2px 12px rgba(76, 175, 80, 0.15);
|
||||||
|
/* height: 50px; 移除固定高度,由内容决定 */
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: row; /* 显式设置行方向 */
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0; /* 防止被压缩 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-container {
|
||||||
|
padding: 0 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row; /* 关键修复:UVUE默认是column,必须显式设置为row才能横向排列 */
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
color: white;
|
width: 100%;
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
height: 44px; /* 调整为标准高度 44px */
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-title {
|
.nav-title {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: white;
|
color: white;
|
||||||
|
flex: 1; /* 自适应宽度 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-actions .action-icon {
|
.nav-actions {
|
||||||
font-size: 20px;
|
|
||||||
color: white;
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 消息分类标签 */
|
|
||||||
.message-tabs {
|
|
||||||
background-color: white;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 0 15px;
|
flex-direction: row; /* 显式设置行方向 */
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row; /* 显式设置行方向 */
|
||||||
|
align-items: center;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-icon {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: white;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 导航栏占位符 */
|
||||||
|
.navbar-placeholder {
|
||||||
|
width: 100%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 消息分类标签容器 */
|
||||||
|
.tabs-container {
|
||||||
|
background: white;
|
||||||
|
padding: 0 10px;
|
||||||
border-bottom: 1px solid #e0e0e0;
|
border-bottom: 1px solid #e0e0e0;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||||
|
z-index: 900;
|
||||||
|
height: 42px; /* 减小高度,更紧凑 */
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-tabs {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row; /* 横向排列 */
|
||||||
|
height: 100%;
|
||||||
|
/* overflow-x: auto; 移除自动滚动,改为自适应宽度 */
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
gap: 4px; /* 减小间距 */
|
||||||
|
justify-content: space-between; /* 均匀分布 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-tabs::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-item {
|
.tab-item {
|
||||||
flex: 1;
|
padding: 0 4px;
|
||||||
padding: 15px 5px;
|
display: flex; /* 改为 flex 布局 */
|
||||||
text-align: center;
|
flex-direction: row; /* 关键:横向排列 文字和数字 */
|
||||||
|
align-items: center; /* 垂直居中 */
|
||||||
|
justify-content: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
border-bottom: 3px solid transparent;
|
border-bottom: 3px solid transparent;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
|
white-space: nowrap; /* 防止换行 */
|
||||||
|
flex: 1; /* 关键:均分宽度,消除滚动条 */
|
||||||
|
min-width: 0; /* 允许压缩 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-item.active {
|
.tab-item.active {
|
||||||
@@ -608,27 +747,36 @@ const onRefresh = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tab-name {
|
.tab-name {
|
||||||
font-size: 14px;
|
font-size: 14px; /* 微调字体大小适配小屏 */
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 徽标样式优化 - 放在文字右边 */
|
||||||
.tab-badge {
|
.tab-badge {
|
||||||
position: absolute;
|
|
||||||
top: 8px;
|
|
||||||
right: 8px;
|
|
||||||
background-color: #FF5722;
|
background-color: #FF5722;
|
||||||
color: white;
|
color: white;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
padding: 2px 6px;
|
padding: 1px 5px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
min-width: 16px;
|
min-width: 16px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
margin-left: 4px; /* 距离文字的间距 */
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 消息内容区 */
|
/* 消息内容区 */
|
||||||
.messages-content {
|
.messages-content {
|
||||||
flex: 1;
|
flex: 1; /* 关键:自适应剩余高度 */
|
||||||
padding-bottom: 80px;
|
height: 0; /* 关键:强制flex item计算高度 */
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 客服信息区域 */
|
/* 客服信息区域 */
|
||||||
@@ -942,6 +1090,16 @@ const onRefresh = () => {
|
|||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.coupon-action {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #fff;
|
||||||
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-top: 4px;
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
/* 客服系统提示 */
|
/* 客服系统提示 */
|
||||||
.service-tips {
|
.service-tips {
|
||||||
background: #FFF3E0;
|
background: #FFF3E0;
|
||||||
@@ -1023,17 +1181,13 @@ const onRefresh = () => {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.safe-area {
|
||||||
|
height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
/* 响应式适配 */
|
/* 响应式适配 */
|
||||||
@media screen and (max-width: 320px) {
|
@media screen and (max-width: 320px) {
|
||||||
.tab-name {
|
.tab-name {
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-title {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-preview {
|
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1064,15 +1218,24 @@ const onRefresh = () => {
|
|||||||
.service-categories {
|
.service-categories {
|
||||||
grid-template-columns: repeat(4, 1fr);
|
grid-template-columns: repeat(4, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 平板和桌面端优化标签栏显示 */
|
||||||
|
.message-tabs {
|
||||||
|
justify-content: flex-start; /* 左对齐 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item {
|
||||||
|
padding: 0 24px; /* 增加点击区域 */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 平板设备 */
|
/* 平板设备 */
|
||||||
@media screen and (min-width: 768px) {
|
@media screen and (min-width: 768px) {
|
||||||
.messages-header {
|
.smart-navbar {
|
||||||
padding: 20px 30px;
|
padding: 0 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-tabs {
|
.tabs-container {
|
||||||
padding: 0 30px;
|
padding: 0 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1095,11 +1258,11 @@ const onRefresh = () => {
|
|||||||
background-color: #121212;
|
background-color: #121212;
|
||||||
}
|
}
|
||||||
|
|
||||||
.messages-header {
|
.smart-navbar {
|
||||||
background: linear-gradient(135deg, #2E7D32 0%, #1B5E20 100%);
|
background: linear-gradient(135deg, #2E7D32 0%, #1B5E20 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-tabs {
|
.tabs-container {
|
||||||
background-color: #1e1e1e;
|
background-color: #1e1e1e;
|
||||||
border-bottom-color: #333;
|
border-bottom-color: #333;
|
||||||
}
|
}
|
||||||
@@ -1155,5 +1318,13 @@ const onRefresh = () => {
|
|||||||
background: #333;
|
background: #333;
|
||||||
color: #ccc;
|
color: #ccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -25,13 +25,13 @@
|
|||||||
<view class="shop-info" @click="goToShop">
|
<view class="shop-info" @click="goToShop">
|
||||||
<image :src="merchant.shop_logo || '/static/default-shop.png'" class="shop-logo" />
|
<image :src="merchant.shop_logo || '/static/default-shop.png'" class="shop-logo" />
|
||||||
<view class="shop-details">
|
<view class="shop-details">
|
||||||
<text class="shop-name">{{ merchant.shop_name }}</text>
|
<text class="shop-name" @click.stop="goToShop">{{ merchant.shop_name }}</text>
|
||||||
<view class="shop-rating">
|
<view class="shop-stats-row">
|
||||||
<text class="rating-text">评分: {{ merchant.rating.toFixed(1) }}</text>
|
<text class="rating-text">评分: {{ merchant.rating.toFixed(1) }}</text>
|
||||||
<text class="sales-text">销量: {{ merchant.total_sales }}</text>
|
<text class="sales-text">销量: {{ merchant.total_sales }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<text class="enter-shop">进店 ></text>
|
<text class="enter-shop" @click.stop="goToShop">进店 ></text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 规格选择 -->
|
<!-- 规格选择 -->
|
||||||
@@ -50,6 +50,16 @@
|
|||||||
<!-- 底部操作栏 -->
|
<!-- 底部操作栏 -->
|
||||||
<view class="bottom-actions">
|
<view class="bottom-actions">
|
||||||
<view class="action-buttons">
|
<view class="action-buttons">
|
||||||
|
<view class="action-btn" @click="goToCart">
|
||||||
|
<text class="action-icon">🛒</text>
|
||||||
|
<text class="action-text">购物车</text>
|
||||||
|
</view>
|
||||||
|
<view class="action-btn" @click="toggleFavorite">
|
||||||
|
<text class="action-icon">{{ isFavorite ? '❤️' : '🤍' }}</text>
|
||||||
|
<text class="action-text">{{ isFavorite ? '已收藏' : '收藏' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="btn-group">
|
||||||
<button class="cart-btn" @click="addToCart">加入购物车</button>
|
<button class="cart-btn" @click="addToCart">加入购物车</button>
|
||||||
<button class="buy-btn" @click="buyNow">立即购买</button>
|
<button class="buy-btn" @click="buyNow">立即购买</button>
|
||||||
</view>
|
</view>
|
||||||
@@ -116,16 +126,55 @@ export default {
|
|||||||
showSpec: false,
|
showSpec: false,
|
||||||
selectedSkuId: '',
|
selectedSkuId: '',
|
||||||
selectedSpec: '',
|
selectedSpec: '',
|
||||||
quantity: 1
|
quantity: 1,
|
||||||
|
isFavorite: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLoad(options: any) {
|
onLoad(options: any) {
|
||||||
const productId = options.productId as string
|
const productId = options.productId as string
|
||||||
if (productId) {
|
if (productId) {
|
||||||
this.loadProductDetail(productId)
|
this.loadProductDetail(productId)
|
||||||
|
this.checkFavoriteStatus(productId)
|
||||||
|
this.saveFootprint(productId)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
saveFootprint(productId: string) {
|
||||||
|
const footprintData = uni.getStorageSync('footprints')
|
||||||
|
let footprints: any[] = []
|
||||||
|
|
||||||
|
if (footprintData) {
|
||||||
|
try {
|
||||||
|
footprints = JSON.parse(footprintData as string) as any[]
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse footprints', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除已存在的相同商品(为了将其移到最新位置)
|
||||||
|
footprints = footprints.filter(item => item.id !== productId)
|
||||||
|
|
||||||
|
// 添加到头部
|
||||||
|
footprints.unshift({
|
||||||
|
id: this.product.id,
|
||||||
|
name: this.product.name,
|
||||||
|
price: this.product.price,
|
||||||
|
original_price: this.product.original_price, // 添加原价
|
||||||
|
image: this.product.images[0],
|
||||||
|
sales: this.product.sales,
|
||||||
|
shopId: this.merchant.id,
|
||||||
|
shopName: this.merchant.shop_name,
|
||||||
|
viewTime: Date.now()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 限制数量,例如最近50条
|
||||||
|
if (footprints.length > 50) {
|
||||||
|
footprints = footprints.slice(0, 50)
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.setStorageSync('footprints', JSON.stringify(footprints))
|
||||||
|
},
|
||||||
|
|
||||||
loadProductDetail(productId: string) {
|
loadProductDetail(productId: string) {
|
||||||
// 模拟加载商品详情数据
|
// 模拟加载商品详情数据
|
||||||
this.product = {
|
this.product = {
|
||||||
@@ -222,6 +271,45 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取现有购物车数据
|
||||||
|
const cartData = uni.getStorageSync('cart')
|
||||||
|
let cartItems: any[] = []
|
||||||
|
|
||||||
|
if (cartData) {
|
||||||
|
try {
|
||||||
|
cartItems = JSON.parse(cartData as string) as any[]
|
||||||
|
} catch (e) {
|
||||||
|
console.error('解析购物车数据失败', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查商品是否已存在 (同一SKU)
|
||||||
|
const existingItem = cartItems.find((item: any) => item.id === this.selectedSkuId)
|
||||||
|
|
||||||
|
if (existingItem) {
|
||||||
|
existingItem.quantity += this.quantity
|
||||||
|
} else {
|
||||||
|
// 查找SKU信息
|
||||||
|
const sku = this.productSkus.find(s => s.id === this.selectedSkuId)
|
||||||
|
|
||||||
|
// 添加新商品
|
||||||
|
cartItems.push({
|
||||||
|
id: this.selectedSkuId, // 使用SKU ID作为购物车条目ID
|
||||||
|
productId: this.product.id,
|
||||||
|
shopId: this.merchant.id,
|
||||||
|
shopName: this.merchant.shop_name,
|
||||||
|
name: this.product.name,
|
||||||
|
price: sku ? sku.price : this.product.price,
|
||||||
|
image: (sku && sku.image_url) ? sku.image_url : this.product.images[0],
|
||||||
|
spec: this.selectedSpec,
|
||||||
|
quantity: this.quantity,
|
||||||
|
selected: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存回存储
|
||||||
|
uni.setStorageSync('cart', JSON.stringify(cartItems))
|
||||||
|
|
||||||
// 模拟添加到购物车
|
// 模拟添加到购物车
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '已添加到购物车',
|
title: '已添加到购物车',
|
||||||
@@ -248,6 +336,70 @@ export default {
|
|||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: `/pages/mall/consumer/shop-detail?merchantId=${this.merchant.id}`
|
url: `/pages/mall/consumer/shop-detail?merchantId=${this.merchant.id}`
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
checkFavoriteStatus(id: string) {
|
||||||
|
const storedFavorites = uni.getStorageSync('favorites')
|
||||||
|
if (storedFavorites) {
|
||||||
|
try {
|
||||||
|
const favorites = JSON.parse(storedFavorites as string) as any[]
|
||||||
|
this.isFavorite = favorites.some(item => item.id === id)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse favorites', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleFavorite() {
|
||||||
|
const storedFavorites = uni.getStorageSync('favorites')
|
||||||
|
let favorites: any[] = []
|
||||||
|
|
||||||
|
if (storedFavorites) {
|
||||||
|
try {
|
||||||
|
favorites = JSON.parse(storedFavorites as string) as any[]
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse favorites', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isFavorite) {
|
||||||
|
// 取消收藏
|
||||||
|
favorites = favorites.filter(item => item.id !== this.product.id)
|
||||||
|
uni.showToast({
|
||||||
|
title: '已取消收藏',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 添加收藏
|
||||||
|
favorites.push({
|
||||||
|
id: this.product.id,
|
||||||
|
name: this.product.name,
|
||||||
|
price: this.product.price,
|
||||||
|
image: this.product.images[0],
|
||||||
|
sales: this.product.sales,
|
||||||
|
shopId: this.merchant.id,
|
||||||
|
shopName: this.merchant.shop_name
|
||||||
|
})
|
||||||
|
uni.showToast({
|
||||||
|
title: '收藏成功',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.setStorageSync('favorites', JSON.stringify(favorites))
|
||||||
|
this.isFavorite = !this.isFavorite
|
||||||
|
},
|
||||||
|
|
||||||
|
goToHome() {
|
||||||
|
uni.switchTab({
|
||||||
|
url: '/pages/mall/consumer/home'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
goToCart() {
|
||||||
|
uni.switchTab({
|
||||||
|
url: '/pages/mall/consumer/cart'
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -328,6 +480,7 @@ export default {
|
|||||||
padding: 30rpx;
|
padding: 30rpx;
|
||||||
margin-bottom: 20rpx;
|
margin-bottom: 20rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: row; /* 显式横向排列 */
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,6 +493,9 @@ export default {
|
|||||||
|
|
||||||
.shop-details {
|
.shop-details {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column; /* 内部信息保持纵向,或者根据需要改为横向 */
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shop-name {
|
.shop-name {
|
||||||
@@ -349,8 +505,10 @@ export default {
|
|||||||
margin-bottom: 10rpx;
|
margin-bottom: 10rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shop-rating {
|
.shop-stats-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: row; /* 显式横向排列 */
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rating-text, .sales-text {
|
.rating-text, .sales-text {
|
||||||
@@ -414,21 +572,55 @@ export default {
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
padding: 20rpx 30rpx;
|
padding: 10rpx 20rpx;
|
||||||
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.1);
|
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.1);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row; /* 显式设置横向排列 */
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-buttons {
|
.action-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 20rpx;
|
flex-direction: row; /* 显式设置横向排列 */
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column; /* 图标文字保持纵向 */
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
min-width: 80rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-icon {
|
||||||
|
font-size: 40rpx;
|
||||||
|
margin-bottom: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-text {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row; /* 显式设置横向排列 */
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cart-btn, .buy-btn {
|
.cart-btn, .buy-btn {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 80rpx;
|
height: 72rpx;
|
||||||
border-radius: 40rpx;
|
line-height: 72rpx;
|
||||||
font-size: 28rpx;
|
border-radius: 36rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
border: none;
|
border: none;
|
||||||
|
margin: 0 10rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cart-btn {
|
.cart-btn {
|
||||||
|
|||||||
@@ -1,40 +1,110 @@
|
|||||||
<!-- 消费者端 - 个人中心 -->
|
<!-- 消费者端 - 个人中心 -->
|
||||||
<template>
|
<template>
|
||||||
<view class="consumer-profile">
|
<view class="consumer-profile">
|
||||||
<!-- 用户信息头部 -->
|
<!-- 智能顶部导航栏 - 与消息页保持一致 -->
|
||||||
<view class="profile-header">
|
<view class="smart-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||||
<image :src="userInfo.avatar_url || '/static/default-avatar.png'" class="user-avatar" @click="editProfile" />
|
<view class="nav-container">
|
||||||
<view class="user-info">
|
<!-- 头像 -->
|
||||||
<text class="user-name">{{ userInfo.nickname || userInfo.phone }}</text>
|
<image
|
||||||
<text class="user-level">{{ getUserLevel() }}</text>
|
:src="userInfo.avatar_url || '/static/default-avatar.png'"
|
||||||
<view class="user-stats">
|
class="nav-avatar"
|
||||||
<text class="stat-item">积分: {{ userStats.points }}</text>
|
@click="editProfile"
|
||||||
<text class="stat-item">余额: ¥{{ userStats.balance }}</text>
|
/>
|
||||||
|
|
||||||
|
<!-- 用户信息横向排列 (名字、积分、余额、优惠券) -->
|
||||||
|
<view class="nav-user-stats">
|
||||||
|
<text class="nav-user-name">{{ userInfo.nickname || userInfo.phone }}</text>
|
||||||
|
|
||||||
|
<view class="nav-stat-item">
|
||||||
|
<text class="nav-stat-label">积分</text>
|
||||||
|
<text class="nav-stat-value">{{ userStats.points }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="nav-stat-item">
|
||||||
|
<text class="nav-stat-label">余额</text>
|
||||||
|
<text class="nav-stat-value">¥{{ userStats.balance }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="nav-stat-item" @click="goToCoupons">
|
||||||
|
<text class="nav-stat-label">券</text>
|
||||||
|
<text class="nav-stat-value">{{ serviceCounts.coupons }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 设置按钮 (右侧) -->
|
||||||
|
<view class="nav-actions">
|
||||||
|
<view class="action-btn" @click="goToSettings">
|
||||||
|
<text class="action-icon">⚙️</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 导航栏占位符 - 恢复 -->
|
||||||
|
<view :style="{ height: (statusBarHeight + 10) + 'px' }"></view>
|
||||||
|
|
||||||
|
<!-- 我的服务 (移到订单上方) -->
|
||||||
|
<view class="my-services" style="margin-top: 10px;">
|
||||||
|
<view class="section-title">我的服务</view>
|
||||||
|
<view class="service-grid">
|
||||||
|
<view class="service-item" @click="goToCoupons">
|
||||||
|
<text class="service-icon">🎫</text>
|
||||||
|
<text class="service-text">优惠券</text>
|
||||||
|
<text v-if="serviceCounts.coupons > 0" class="service-badge">{{ serviceCounts.coupons }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="service-item" @click="goToAddress">
|
||||||
|
<text class="service-icon">📍</text>
|
||||||
|
<text class="service-text">收货地址</text>
|
||||||
|
</view>
|
||||||
|
<view class="service-item" @click="goToFavorites">
|
||||||
|
<text class="service-icon">❤️</text>
|
||||||
|
<text class="service-text">我的收藏</text>
|
||||||
|
<text v-if="serviceCounts.favorites > 0" class="service-badge">{{ serviceCounts.favorites }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="service-item" @click="goToFootprint">
|
||||||
|
<text class="service-icon">👣</text>
|
||||||
|
<text class="service-text">浏览足迹</text>
|
||||||
|
</view>
|
||||||
|
<view class="service-item" @click="goToRefund">
|
||||||
|
<text class="service-icon">🔄</text>
|
||||||
|
<text class="service-text">退款/售后</text>
|
||||||
|
</view>
|
||||||
|
<view class="service-item" @click="contactService">
|
||||||
|
<text class="service-icon">💬</text>
|
||||||
|
<text class="service-text">在线客服</text>
|
||||||
|
</view>
|
||||||
|
<view class="service-item" @click="goToMySubscriptions">
|
||||||
|
<text class="service-icon">🧩</text>
|
||||||
|
<text class="service-text">我的订阅</text>
|
||||||
|
</view>
|
||||||
|
<view class="service-item" @click="goToSubscriptions">
|
||||||
|
<text class="service-icon">📱</text>
|
||||||
|
<text class="service-text">软件订阅</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="settings-icon" @click="goToSettings">⚙️</view>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 订单状态快捷入口 -->
|
<!-- 订单状态快捷入口 -->
|
||||||
<view class="order-shortcuts">
|
<view class="order-shortcuts">
|
||||||
<view class="section-title">我的订单</view>
|
<view class="section-title">我的订单</view>
|
||||||
<view class="order-tabs">
|
<view class="order-tabs">
|
||||||
<view class="order-tab" @click="goToOrders('all')">
|
<view class="order-tab" :class="{ active: currentOrderTab === 'all' }" @click="switchOrderTab('all')">
|
||||||
<text class="tab-icon">📋</text>
|
<text class="tab-icon">📋</text>
|
||||||
<text class="tab-text">全部订单</text>
|
<text class="tab-text">全部</text>
|
||||||
<text v-if="orderCounts.total > 0" class="tab-badge">{{ orderCounts.total }}</text>
|
<text v-if="orderCounts.total > 0" class="tab-badge">{{ orderCounts.total }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="order-tab" @click="goToOrders('pending')">
|
<view class="order-tab" :class="{ active: currentOrderTab === 'pending' }" @click="switchOrderTab('pending')">
|
||||||
<text class="tab-icon">💰</text>
|
<text class="tab-icon">💰</text>
|
||||||
<text class="tab-text">待支付</text>
|
<text class="tab-text">待支付</text>
|
||||||
<text v-if="orderCounts.pending > 0" class="tab-badge">{{ orderCounts.pending }}</text>
|
<text v-if="orderCounts.pending > 0" class="tab-badge">{{ orderCounts.pending }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="order-tab" @click="goToOrders('shipped')">
|
<view class="order-tab" :class="{ active: currentOrderTab === 'shipped' }" @click="switchOrderTab('shipped')">
|
||||||
<text class="tab-icon">🚚</text>
|
<text class="tab-icon">🚚</text>
|
||||||
<text class="tab-text">待收货</text>
|
<text class="tab-text">待收货</text>
|
||||||
<text v-if="orderCounts.shipped > 0" class="tab-badge">{{ orderCounts.shipped }}</text>
|
<text v-if="orderCounts.shipped > 0" class="tab-badge">{{ orderCounts.shipped }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="order-tab" @click="goToOrders('completed')">
|
<view class="order-tab" :class="{ active: currentOrderTab === 'review' }" @click="switchOrderTab('review')">
|
||||||
<text class="tab-icon">⭐</text>
|
<text class="tab-icon">⭐</text>
|
||||||
<text class="tab-text">待评价</text>
|
<text class="tab-text">待评价</text>
|
||||||
<text v-if="orderCounts.review > 0" class="tab-badge">{{ orderCounts.review }}</text>
|
<text v-if="orderCounts.review > 0" class="tab-badge">{{ orderCounts.review }}</text>
|
||||||
@@ -42,25 +112,25 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 最近订单 -->
|
<!-- 最近订单列表 (根据Tab切换显示) -->
|
||||||
<view class="recent-orders">
|
<view class="recent-orders">
|
||||||
<view class="section-header">
|
<view class="section-header">
|
||||||
<text class="section-title">最近订单</text>
|
<text class="section-title">{{ getOrderSectionTitle() }}</text>
|
||||||
<text class="view-all" @click="goToOrders('all')">查看全部 ></text>
|
<text class="view-all" @click="goToOrders(currentOrderTab)">查看更多 ></text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-if="recentOrders.length === 0" class="empty-orders">
|
<view v-if="filteredOrders.length === 0" class="empty-orders">
|
||||||
<text class="empty-text">暂无订单记录</text>
|
<text class="empty-text">暂无相关订单记录</text>
|
||||||
<button class="start-shopping" @click="goShopping">去逛逛</button>
|
<button class="start-shopping" @click="goShopping">去逛逛</button>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-for="order in recentOrders" :key="order.id" class="order-item" @click="viewOrderDetail(order)">
|
<view v-for="order in filteredOrders" :key="order.id" class="order-item" @click="viewOrderDetail(order)">
|
||||||
<view class="order-header">
|
<view class="order-header">
|
||||||
<text class="order-no">订单号: {{ order.order_no }}</text>
|
<text class="order-no">订单号: {{ order.order_no }}</text>
|
||||||
<text class="order-status" :class="getOrderStatusClass(order.status)">{{ getOrderStatusText(order.status) }}</text>
|
<text class="order-status" :class="getOrderStatusClass(order.status)">{{ getOrderStatusText(order.status) }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="order-content">
|
<view class="order-content">
|
||||||
<image :src="getOrderMainImage(order)" class="order-image" />
|
<image :src="getOrderMainImage(order)" class="order-image" mode="aspectFill" />
|
||||||
<view class="order-info">
|
<view class="order-info">
|
||||||
<text class="order-title">{{ getOrderTitle(order) }}</text>
|
<text class="order-title">{{ getOrderTitle(order) }}</text>
|
||||||
<text class="order-amount">¥{{ order.actual_amount }}</text>
|
<text class="order-amount">¥{{ order.actual_amount }}</text>
|
||||||
@@ -76,7 +146,7 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 我的服务 -->
|
<!-- 我的服务 -->
|
||||||
<view class="my-services">
|
<!-- <view class="my-services">
|
||||||
<view class="section-title">我的服务</view>
|
<view class="section-title">我的服务</view>
|
||||||
<view class="service-grid">
|
<view class="service-grid">
|
||||||
<view class="service-item" @click="goToCoupons">
|
<view class="service-item" @click="goToCoupons">
|
||||||
@@ -114,7 +184,7 @@
|
|||||||
<text class="service-text">软件订阅</text>
|
<text class="service-text">软件订阅</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view> -->
|
||||||
|
|
||||||
<!-- 消费统计 -->
|
<!-- 消费统计 -->
|
||||||
<view class="consumption-stats">
|
<view class="consumption-stats">
|
||||||
@@ -251,16 +321,150 @@ export default {
|
|||||||
order_count: 0,
|
order_count: 0,
|
||||||
avg_amount: 0,
|
avg_amount: 0,
|
||||||
save_amount: 0
|
save_amount: 0
|
||||||
} as ConsumptionStatsType
|
} as ConsumptionStatsType,
|
||||||
|
statusBarHeight: 0,
|
||||||
|
currentOrderTab: 'all' as string, // 当前选中的订单Tab
|
||||||
|
allOrders: [] as Array<OrderType> // 存储所有订单数据
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLoad() {
|
onLoad() {
|
||||||
|
this.initPage()
|
||||||
this.loadUserProfile()
|
this.loadUserProfile()
|
||||||
|
this.loadMockOrders() // 加载Mock订单数据
|
||||||
},
|
},
|
||||||
onShow() {
|
computed: {
|
||||||
this.refreshData()
|
// 根据当前Tab筛选订单
|
||||||
|
filteredOrders(): Array<OrderType> {
|
||||||
|
if (this.currentOrderTab === 'all') {
|
||||||
|
return this.allOrders
|
||||||
|
} else if (this.currentOrderTab === 'pending') {
|
||||||
|
return this.allOrders.filter((order: OrderType): boolean => order.status === 1)
|
||||||
|
} else if (this.currentOrderTab === 'shipped') {
|
||||||
|
return this.allOrders.filter((order: OrderType): boolean => order.status === 2 || order.status === 3)
|
||||||
|
} else if (this.currentOrderTab === 'review') {
|
||||||
|
return this.allOrders.filter((order: OrderType): boolean => order.status === 4)
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
// 加载Mock订单数据
|
||||||
|
loadMockOrders() {
|
||||||
|
// 创建Mock数据
|
||||||
|
const mockData: Array<OrderType> = [
|
||||||
|
// 待支付订单
|
||||||
|
{
|
||||||
|
id: 'order_001',
|
||||||
|
order_no: 'ORD202401250001',
|
||||||
|
user_id: 'user_001',
|
||||||
|
merchant_id: 'merchant_001',
|
||||||
|
status: 1, // 待支付
|
||||||
|
total_amount: 299.00,
|
||||||
|
discount_amount: 30.00,
|
||||||
|
delivery_fee: 0.00,
|
||||||
|
actual_amount: 269.00,
|
||||||
|
payment_method: 1,
|
||||||
|
payment_status: 0,
|
||||||
|
delivery_address: {},
|
||||||
|
created_at: '2024-01-25T14:30:00'
|
||||||
|
},
|
||||||
|
// 待发货
|
||||||
|
{
|
||||||
|
id: 'order_002',
|
||||||
|
order_no: 'ORD202401240002',
|
||||||
|
user_id: 'user_001',
|
||||||
|
merchant_id: 'merchant_002',
|
||||||
|
status: 2, // 待发货
|
||||||
|
total_amount: 158.00,
|
||||||
|
discount_amount: 0,
|
||||||
|
delivery_fee: 6.00,
|
||||||
|
actual_amount: 164.00,
|
||||||
|
payment_method: 1,
|
||||||
|
payment_status: 1,
|
||||||
|
delivery_address: {},
|
||||||
|
created_at: '2024-01-24T09:20:00'
|
||||||
|
},
|
||||||
|
// 待收货
|
||||||
|
{
|
||||||
|
id: 'order_003',
|
||||||
|
order_no: 'ORD202401230003',
|
||||||
|
user_id: 'user_001',
|
||||||
|
merchant_id: 'merchant_001',
|
||||||
|
status: 3, // 待收货
|
||||||
|
total_amount: 89.90,
|
||||||
|
discount_amount: 10.00,
|
||||||
|
delivery_fee: 0.00,
|
||||||
|
actual_amount: 79.90,
|
||||||
|
payment_method: 1,
|
||||||
|
payment_status: 1,
|
||||||
|
delivery_address: {},
|
||||||
|
created_at: '2024-01-23T18:15:00'
|
||||||
|
},
|
||||||
|
// 待评价 (已完成)
|
||||||
|
{
|
||||||
|
id: 'order_004',
|
||||||
|
order_no: 'ORD202401200004',
|
||||||
|
user_id: 'user_001',
|
||||||
|
merchant_id: 'merchant_003',
|
||||||
|
status: 4, // 待评价
|
||||||
|
total_amount: 399.00,
|
||||||
|
discount_amount: 50.00,
|
||||||
|
delivery_fee: 0.00,
|
||||||
|
actual_amount: 349.00,
|
||||||
|
payment_method: 1,
|
||||||
|
payment_status: 1,
|
||||||
|
delivery_address: {},
|
||||||
|
created_at: '2024-01-20T11:30:00'
|
||||||
|
},
|
||||||
|
// 已完成 (已评价)
|
||||||
|
{
|
||||||
|
id: 'order_005',
|
||||||
|
order_no: 'ORD202401180005',
|
||||||
|
user_id: 'user_001',
|
||||||
|
merchant_id: 'merchant_001',
|
||||||
|
status: 5, // 已完成
|
||||||
|
total_amount: 128.00,
|
||||||
|
discount_amount: 0,
|
||||||
|
delivery_fee: 0.00,
|
||||||
|
actual_amount: 128.00,
|
||||||
|
payment_method: 1,
|
||||||
|
payment_status: 1,
|
||||||
|
delivery_address: {},
|
||||||
|
created_at: '2024-01-18T16:45:00'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
this.allOrders = mockData
|
||||||
|
this.recentOrders = mockData // 初始显示全部
|
||||||
|
|
||||||
|
// 更新角标统计
|
||||||
|
this.orderCounts = {
|
||||||
|
total: mockData.length,
|
||||||
|
pending: mockData.filter((o: OrderType): boolean => o.status === 1).length,
|
||||||
|
shipped: mockData.filter((o: OrderType): boolean => o.status === 2 || o.status === 3).length,
|
||||||
|
review: mockData.filter((o: OrderType): boolean => o.status === 4).length
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 切换订单Tab
|
||||||
|
switchOrderTab(tab: string) {
|
||||||
|
this.currentOrderTab = tab
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取当前订单部分标题
|
||||||
|
getOrderSectionTitle(): string {
|
||||||
|
const titles: Record<string, string> = {
|
||||||
|
'all': '全部订单',
|
||||||
|
'pending': '待支付订单',
|
||||||
|
'shipped': '待收货订单',
|
||||||
|
'review': '待评价订单'
|
||||||
|
}
|
||||||
|
return titles[this.currentOrderTab] || '我的订单'
|
||||||
|
},
|
||||||
|
|
||||||
|
initPage() {
|
||||||
|
const systemInfo = uni.getSystemInfoSync()
|
||||||
|
this.statusBarHeight = systemInfo.statusBarHeight || 0
|
||||||
|
},
|
||||||
loadUserProfile() {
|
loadUserProfile() {
|
||||||
// 模拟加载用户信息
|
// 模拟加载用户信息
|
||||||
this.userInfo = {
|
this.userInfo = {
|
||||||
@@ -364,6 +568,15 @@ export default {
|
|||||||
refreshData() {
|
refreshData() {
|
||||||
// 刷新页面数据
|
// 刷新页面数据
|
||||||
this.loadUserProfile()
|
this.loadUserProfile()
|
||||||
|
this.loadMockOrders() // 加载Mock订单数据
|
||||||
|
this.updateCouponCount() // 更新优惠券数量
|
||||||
|
},
|
||||||
|
|
||||||
|
updateCouponCount() {
|
||||||
|
// 从本地存储读取领取的优惠券数量并叠加到基础数量上
|
||||||
|
const baseCoupons = 5
|
||||||
|
const claimedCoupons = uni.getStorageSync('claimedCoupons') || 0
|
||||||
|
this.serviceCounts.coupons = baseCoupons + (claimedCoupons as number)
|
||||||
},
|
},
|
||||||
|
|
||||||
getUserLevel(): string {
|
getUserLevel(): string {
|
||||||
@@ -476,8 +689,9 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
goToAddress() {
|
goToAddress() {
|
||||||
|
// 暂时跳转到设置页的地址管理
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: '/pages/mall/consumer/address'
|
url: '/pages/mall/consumer/address-list'
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -542,115 +756,183 @@ export default {
|
|||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-header {
|
/* 智能顶部导航栏 */
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
.smart-navbar {
|
||||||
padding: 60rpx 30rpx 40rpx;
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: linear-gradient(135deg, #4CAF50 0%, #2E7D32 100%);
|
||||||
|
z-index: 1000;
|
||||||
|
box-shadow: 0 2px 12px rgba(76, 175, 80, 0.15);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-container {
|
||||||
|
padding: 0 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 导航栏用户信息区域 */
|
||||||
|
.nav-user-stats {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start; /* 靠左对齐,紧跟头像 */
|
||||||
|
margin-right: 12px;
|
||||||
|
overflow: hidden; /* 防止溢出 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-user-name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
margin-right: 12px;
|
||||||
|
max-width: 30%; /* 限制名字宽度 */
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-stat-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
margin-right: 8px;
|
||||||
|
flex-shrink: 0; /* 防止被压缩 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-stat-label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-stat-value {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-avatar {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 18px;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.8);
|
||||||
|
margin-right: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: #fff;
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 16px;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-avatar {
|
.action-icon {
|
||||||
width: 120rpx;
|
font-size: 18px;
|
||||||
height: 120rpx;
|
color: white;
|
||||||
border-radius: 60rpx;
|
|
||||||
margin-right: 30rpx;
|
|
||||||
border: 4rpx solid rgba(255, 255, 255, 0.3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-info {
|
/* 导航栏占位符 */
|
||||||
flex: 1;
|
.navbar-placeholder {
|
||||||
}
|
width: 100%;
|
||||||
|
flex-shrink: 0;
|
||||||
.user-name {
|
|
||||||
font-size: 36rpx;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 8rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-level {
|
|
||||||
font-size: 24rpx;
|
|
||||||
background-color: rgba(255, 255, 255, 0.2);
|
|
||||||
padding: 6rpx 12rpx;
|
|
||||||
border-radius: 12rpx;
|
|
||||||
margin-bottom: 15rpx;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-stats {
|
|
||||||
display: flex;
|
|
||||||
gap: 30rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-item {
|
|
||||||
font-size: 24rpx;
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-icon {
|
|
||||||
font-size: 32rpx;
|
|
||||||
padding: 10rpx;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.order-shortcuts, .recent-orders, .my-services, .consumption-stats, .account-security {
|
.order-shortcuts, .recent-orders, .my-services, .consumption-stats, .account-security {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
margin-bottom: 20rpx;
|
margin: 15px 15px; /* 顶部恢复 margin */
|
||||||
padding: 30rpx;
|
border-radius: 12px; /* 统一圆角 */
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
font-size: 32rpx;
|
font-size: 16px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #333;
|
color: #333;
|
||||||
margin-bottom: 25rpx;
|
margin-bottom: 16px;
|
||||||
}
|
|
||||||
|
|
||||||
.section-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 25rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.view-all {
|
|
||||||
font-size: 24rpx;
|
|
||||||
color: #007aff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.order-tabs {
|
.order-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: row; /* 显式横向排列 */
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.order-tab {
|
.order-tab {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: row; /* 关键:改为横向排列 */
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: center; /* 居中 */
|
||||||
position: relative;
|
position: relative;
|
||||||
|
padding: 8px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-icon {
|
.tab-icon {
|
||||||
font-size: 40rpx;
|
font-size: 20px;
|
||||||
margin-bottom: 10rpx;
|
margin-right: 6px; /* 图标和文字间距 */
|
||||||
|
margin-bottom: 0; /* 移除底部间距 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-text {
|
.tab-text {
|
||||||
font-size: 24rpx;
|
font-size: 14px;
|
||||||
color: #666;
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 选中状态的Tab */
|
||||||
|
.order-tab.active .tab-icon,
|
||||||
|
.order-tab.active .tab-text {
|
||||||
|
color: #4CAF50;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-tab.active {
|
||||||
|
background-color: #f0f9f0;
|
||||||
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-badge {
|
.tab-badge {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -8rpx;
|
top: 0;
|
||||||
right: 20rpx;
|
right: 10%; /* 调整位置 */
|
||||||
background-color: #ff4444;
|
background-color: #ff4444;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 20rpx;
|
font-size: 10px;
|
||||||
padding: 4rpx 8rpx;
|
padding: 1px 5px;
|
||||||
border-radius: 10rpx;
|
border-radius: 8px;
|
||||||
min-width: 32rpx;
|
min-width: 14px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-orders {
|
.empty-orders {
|
||||||
@@ -782,16 +1064,19 @@ export default {
|
|||||||
|
|
||||||
.service-grid {
|
.service-grid {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-direction: row;
|
||||||
gap: 30rpx;
|
flex-wrap: wrap; /* 允许换行 */
|
||||||
|
gap: 16px 0; /* 行间距16px,列间距由 flex 控制 */
|
||||||
|
justify-content: flex-start; /* 从左开始排列 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-item {
|
.service-item {
|
||||||
width: 30%;
|
width: 25%; /* 每行4个 */
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
box-sizing: border-box; /* 确保 padding 不影响宽度 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-icon {
|
.service-icon {
|
||||||
|
|||||||
@@ -3,14 +3,13 @@
|
|||||||
<!-- 搜索头部 -->
|
<!-- 搜索头部 -->
|
||||||
<view class="search-header" :style="{ paddingTop: statusBarHeight + 'px' }">
|
<view class="search-header" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||||
<view class="search-bar-container">
|
<view class="search-bar-container">
|
||||||
<!-- 返回按钮 -->
|
<!-- 返回按钮:小于号加粗 -->
|
||||||
<view class="back-btn" @click="goBack">
|
<view class="back-btn" @click="goBack">
|
||||||
<text class="back-icon">←</text>
|
<text class="back-icon"><</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 搜索框 -->
|
<!-- 搜索框 -->
|
||||||
<view class="search-input-container">
|
<view class="search-input-container">
|
||||||
<view class="search-icon">🔍</view>
|
|
||||||
<input
|
<input
|
||||||
class="search-input"
|
class="search-input"
|
||||||
type="text"
|
type="text"
|
||||||
@@ -21,14 +20,21 @@
|
|||||||
placeholder-class="placeholder"
|
placeholder-class="placeholder"
|
||||||
:focus="autoFocus"
|
:focus="autoFocus"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- 清除按钮 -->
|
||||||
<view v-if="searchKeyword" class="clear-btn" @click="clearSearch">
|
<view v-if="searchKeyword" class="clear-btn" @click="clearSearch">
|
||||||
<text class="clear-icon">×</text>
|
<text class="clear-icon">×</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 相机图标 -->
|
||||||
|
<view class="camera-btn" @click="openCamera">
|
||||||
|
<text class="camera-icon">📷</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 搜索按钮 -->
|
<!-- 搜索按钮:移入输入框内部 -->
|
||||||
<view class="search-btn" @click="onSearch">
|
<view class="inner-search-btn" @click="onSearch">
|
||||||
<text class="search-btn-text">搜索</text>
|
<text class="inner-search-text">搜索</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -149,9 +155,23 @@
|
|||||||
<view class="results-header">
|
<view class="results-header">
|
||||||
<text class="results-title">搜索结果</text>
|
<text class="results-title">搜索结果</text>
|
||||||
<view class="filter-tabs">
|
<view class="filter-tabs">
|
||||||
<text class="filter-tab active">综合</text>
|
<text
|
||||||
<text class="filter-tab">销量</text>
|
class="filter-tab"
|
||||||
<text class="filter-tab">价格</text>
|
:class="{ active: activeSort === 'default' }"
|
||||||
|
@click="switchSort('default')"
|
||||||
|
>综合</text>
|
||||||
|
<text
|
||||||
|
class="filter-tab"
|
||||||
|
:class="{ active: activeSort === 'sales' }"
|
||||||
|
@click="switchSort('sales')"
|
||||||
|
>销量</text>
|
||||||
|
<text
|
||||||
|
class="filter-tab"
|
||||||
|
:class="{ active: activeSort === 'price' }"
|
||||||
|
@click="switchSort('price')"
|
||||||
|
>
|
||||||
|
价格 {{ activeSort === 'price' ? (priceSortAsc ? '↑' : '↓') : '' }}
|
||||||
|
</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -175,7 +195,7 @@
|
|||||||
<text class="price-symbol">¥</text>
|
<text class="price-symbol">¥</text>
|
||||||
<text class="price-value">{{ product.price }}</text>
|
<text class="price-value">{{ product.price }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="add-cart-btn">
|
<view class="add-cart-btn" @click.stop="addToCart(product)">
|
||||||
<text class="cart-icon">+</text>
|
<text class="cart-icon">+</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -183,14 +203,14 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 空结果 -->
|
<!-- 空结果 - 仅在非加载状态且无结果时显示 -->
|
||||||
<view v-if="searchResults.length === 0 && !loading" class="empty-result">
|
<view v-if="!loading && searchResults.length === 0" class="empty-result">
|
||||||
<text class="empty-icon">🤔</text>
|
<text class="empty-icon">🤔</text>
|
||||||
<text class="empty-text">未找到相关商品</text>
|
<text class="empty-text">未找到相关商品</text>
|
||||||
<text class="empty-sub">换个关键词试试吧</text>
|
<text class="empty-sub">换个关键词试试吧</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 加载更多 -->
|
<!-- 加载更多/加载中 - 在加载状态或有更多数据时显示 -->
|
||||||
<view v-if="loading" class="loading-more">
|
<view v-if="loading" class="loading-more">
|
||||||
<view class="loading-spinner"></view>
|
<view class="loading-spinner"></view>
|
||||||
<text class="loading-text">加载中...</text>
|
<text class="loading-text">加载中...</text>
|
||||||
@@ -219,6 +239,8 @@ const loading = ref(false)
|
|||||||
const hasMore = ref(true)
|
const hasMore = ref(true)
|
||||||
const isError = ref(false) // 错误状态控制
|
const isError = ref(false) // 错误状态控制
|
||||||
const autoFocus = ref(true)
|
const autoFocus = ref(true)
|
||||||
|
const activeSort = ref('default') // 当前排序方式: default, sales, price
|
||||||
|
const priceSortAsc = ref(false) // 价格排序是否为升序
|
||||||
|
|
||||||
// 数据定义
|
// 数据定义
|
||||||
const searchHistory = ref<string[]>([])
|
const searchHistory = ref<string[]>([])
|
||||||
@@ -271,6 +293,31 @@ const initPage = () => {
|
|||||||
scrollHeight.value = windowHeight - (60 + statusBarHeight.value)
|
scrollHeight.value = windowHeight - (60 + statusBarHeight.value)
|
||||||
|
|
||||||
loadData()
|
loadData()
|
||||||
|
|
||||||
|
// 检查页面参数
|
||||||
|
const pages = getCurrentPages()
|
||||||
|
if (pages.length > 0) {
|
||||||
|
const currentPage = pages[pages.length - 1]
|
||||||
|
// @ts-ignore
|
||||||
|
const options = currentPage.options
|
||||||
|
if (options && options['keyword']) {
|
||||||
|
const keyword = decodeURIComponent(options['keyword'])
|
||||||
|
searchKeyword.value = keyword
|
||||||
|
|
||||||
|
if (options['type'] === 'family') {
|
||||||
|
// 如果是家庭常备药类型,直接添加到历史并搜索
|
||||||
|
addToHistory(keyword)
|
||||||
|
// 立即显示结果区域并设置为加载中
|
||||||
|
showResults.value = true
|
||||||
|
loading.value = true
|
||||||
|
// 确保searchResults不为空数组导致闪烁(虽然loading=true已经拦截了empty-result,但双重保险)
|
||||||
|
// 此时不要置空searchResults,或者给一个初始值
|
||||||
|
|
||||||
|
// 直接调用搜索,移除setTimeout,防止中间状态
|
||||||
|
performSearch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('初始化失败', e)
|
console.error('初始化失败', e)
|
||||||
isError.value = true
|
isError.value = true
|
||||||
@@ -279,7 +326,7 @@ const initPage = () => {
|
|||||||
|
|
||||||
// 加载基础数据
|
// 加载基础数据
|
||||||
const loadData = () => {
|
const loadData = () => {
|
||||||
loading.value = true
|
// loading.value = true // 不使用全局loading,避免影响搜索状态
|
||||||
isError.value = false
|
isError.value = false
|
||||||
|
|
||||||
// 模拟网络请求
|
// 模拟网络请求
|
||||||
@@ -288,10 +335,10 @@ const loadData = () => {
|
|||||||
loadSearchHistory()
|
loadSearchHistory()
|
||||||
hotSearchList.value = mockDatabase.hot
|
hotSearchList.value = mockDatabase.hot
|
||||||
guessList.value = mockDatabase.guess
|
guessList.value = mockDatabase.guess
|
||||||
loading.value = false
|
// loading.value = false // 不使用全局loading
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
isError.value = true
|
isError.value = true
|
||||||
loading.value = false
|
// loading.value = false
|
||||||
}
|
}
|
||||||
}, 500)
|
}, 500)
|
||||||
}
|
}
|
||||||
@@ -391,37 +438,84 @@ const selectSuggestion = (suggestion: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const performSearch = () => {
|
const performSearch = () => {
|
||||||
|
// 再次强制设置状态,确保万无一失
|
||||||
showResults.value = true
|
showResults.value = true
|
||||||
loading.value = true
|
loading.value = true
|
||||||
searchResults.value = [] // 清空旧结果
|
// 注意:这里不要清空 searchResults.value = [],否则如果 loading 状态切换有微小延迟,可能会短暂满足 "无数据且非加载" 的条件
|
||||||
|
// 保持旧数据直到新数据回来,或者依靠 loading 状态完全遮罩
|
||||||
|
|
||||||
// 模拟搜索请求
|
// 模拟搜索请求
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
loading.value = false
|
|
||||||
// 生成模拟结果
|
// 生成模拟结果
|
||||||
searchResults.value = Array.from({ length: 6 }, (_, i) => ({
|
const newResults = Array.from({ length: 6 }, (_, i) => ({
|
||||||
id: `s${i}`,
|
id: `s${Date.now()}${i}`, // 确保ID唯一
|
||||||
|
shopId: i % 2 === 0 ? 'shop_self' : `shop_${i}_${Date.now()}`,
|
||||||
|
shopName: i % 2 === 0 ? '平台自营大药房' : '阿里健康大药房',
|
||||||
name: `${searchKeyword.value}相关药品-${i+1}`,
|
name: `${searchKeyword.value}相关药品-${i+1}`,
|
||||||
specification: '10g*12袋',
|
specification: '10g*12袋',
|
||||||
price: (Math.random() * 50 + 10).toFixed(1),
|
price: (Math.random() * 50 + 10).toFixed(1),
|
||||||
image: `https://picsum.photos/300/300?random=s${i}`,
|
image: '/static/images/default-product.png', // 使用本地默认图片
|
||||||
sales: Math.floor(Math.random() * 1000),
|
sales: Math.floor(Math.random() * 1000),
|
||||||
tag: i % 2 === 0 ? '自营' : ''
|
tag: i % 2 === 0 ? '自营' : ''
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// 数据准备好后再关闭 loading,确保无缝衔接
|
||||||
|
searchResults.value = newResults
|
||||||
|
// 应用当前排序
|
||||||
|
sortResults()
|
||||||
|
|
||||||
|
loading.value = false
|
||||||
hasMore.value = true
|
hasMore.value = true
|
||||||
}, 800)
|
}, 800)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 切换排序
|
||||||
|
const switchSort = (type: string) => {
|
||||||
|
if (type === 'price') {
|
||||||
|
if (activeSort.value === 'price') {
|
||||||
|
priceSortAsc.value = !priceSortAsc.value
|
||||||
|
} else {
|
||||||
|
activeSort.value = 'price'
|
||||||
|
priceSortAsc.value = true // 默认升序
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
activeSort.value = type
|
||||||
|
}
|
||||||
|
sortResults()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行排序逻辑
|
||||||
|
const sortResults = () => {
|
||||||
|
const list = [...searchResults.value]
|
||||||
|
if (activeSort.value === 'sales') {
|
||||||
|
// 销量降序
|
||||||
|
list.sort((a, b) => b.sales - a.sales)
|
||||||
|
} else if (activeSort.value === 'price') {
|
||||||
|
// 价格排序
|
||||||
|
list.sort((a, b) => {
|
||||||
|
const p1 = parseFloat(a.price)
|
||||||
|
const p2 = parseFloat(b.price)
|
||||||
|
return priceSortAsc.value ? (p1 - p2) : (p2 - p1)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 综合排序(这里简单按ID倒序模拟)
|
||||||
|
list.sort((a, b) => (a.id > b.id ? -1 : 1))
|
||||||
|
}
|
||||||
|
searchResults.value = list
|
||||||
|
}
|
||||||
|
|
||||||
const loadMore = () => {
|
const loadMore = () => {
|
||||||
if (loading.value || !hasMore.value) return
|
if (loading.value || !hasMore.value) return
|
||||||
loading.value = true
|
loading.value = true
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const newItems = Array.from({ length: 4 }, (_, i) => ({
|
const newItems = Array.from({ length: 4 }, (_, i) => ({
|
||||||
id: `more${Date.now()}${i}`,
|
id: `more${Date.now()}${i}`,
|
||||||
|
shopId: i % 2 === 0 ? 'shop_self' : `shop_more_${i}_${Date.now()}`,
|
||||||
|
shopName: i % 2 === 0 ? '平台自营大药房' : '好药师大药房',
|
||||||
name: `${searchKeyword.value}更多药品-${i+1}`,
|
name: `${searchKeyword.value}更多药品-${i+1}`,
|
||||||
specification: '盒装',
|
specification: '盒装',
|
||||||
price: (Math.random() * 50 + 10).toFixed(1),
|
price: (Math.random() * 50 + 10).toFixed(1),
|
||||||
image: `https://picsum.photos/300/300?random=m${i}`,
|
image: '/static/images/default-product.png',
|
||||||
sales: Math.floor(Math.random() * 500),
|
sales: Math.floor(Math.random() * 500),
|
||||||
tag: ''
|
tag: ''
|
||||||
}))
|
}))
|
||||||
@@ -442,12 +536,78 @@ const refreshGuessList = () => {
|
|||||||
const viewProductDetail = (item: any) => {
|
const viewProductDetail = (item: any) => {
|
||||||
// 跳转详情页逻辑
|
// 跳转详情页逻辑
|
||||||
console.log('查看商品', item)
|
console.log('查看商品', item)
|
||||||
uni.showToast({ title: '点击了商品: ' + item.name, icon: 'none' })
|
uni.navigateTo({
|
||||||
|
url: `/pages/mall/consumer/product-detail?productId=${item.id}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加到购物车
|
||||||
|
const addToCart = (product: any) => {
|
||||||
|
// 获取现有购物车数据
|
||||||
|
const cartData = uni.getStorageSync('cart')
|
||||||
|
let cartItems: any[] = []
|
||||||
|
|
||||||
|
if (cartData) {
|
||||||
|
try {
|
||||||
|
cartItems = JSON.parse(cartData as string) as any[]
|
||||||
|
} catch (e) {
|
||||||
|
console.error('解析购物车数据失败', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查商品是否已存在
|
||||||
|
const existingItem = cartItems.find((item: any) => item.id === product.id)
|
||||||
|
|
||||||
|
if (existingItem) {
|
||||||
|
existingItem.quantity++
|
||||||
|
} else {
|
||||||
|
// 添加新商品
|
||||||
|
cartItems.push({
|
||||||
|
id: product.id,
|
||||||
|
shopId: product.shopId || 'shop_search_default',
|
||||||
|
shopName: product.shopName || (product.tag === '自营' ? '平台自营大药房' : '优质大药房'),
|
||||||
|
name: product.name,
|
||||||
|
price: product.price,
|
||||||
|
image: product.image,
|
||||||
|
spec: product.specification || '默认规格',
|
||||||
|
quantity: 1,
|
||||||
|
selected: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存回存储
|
||||||
|
uni.setStorageSync('cart', JSON.stringify(cartItems))
|
||||||
|
|
||||||
|
uni.showToast({
|
||||||
|
title: '已添加到购物车',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const openCamera = () => {
|
||||||
|
uni.chooseImage({
|
||||||
|
count: 1,
|
||||||
|
sourceType: ['camera'],
|
||||||
|
success: (res) => {
|
||||||
|
console.log('拍摄图片路径:', res.tempFilePaths[0])
|
||||||
|
uni.showToast({ title: '已启用相机', icon: 'none' })
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
console.error('启用相机失败', err)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const goBack = () => {
|
const goBack = () => {
|
||||||
|
if (showResults.value) {
|
||||||
|
// 如果在搜索结果页,先返回到搜索初始页
|
||||||
|
showResults.value = false
|
||||||
|
searchKeyword.value = ''
|
||||||
|
} else {
|
||||||
|
// 如果在搜索初始页,则返回上一页
|
||||||
uni.navigateBack()
|
uni.navigateBack()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -463,41 +623,46 @@ const goBack = () => {
|
|||||||
.search-header {
|
.search-header {
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
position: sticky;
|
/* #ifdef APP-PLUS */
|
||||||
top: 0;
|
padding-top: 0; /* 在App端由style动态控制 */
|
||||||
z-index: 100;
|
/* #endif */
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-bar-container {
|
.search-bar-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: row; /* UVUE 必须显式设置 row */
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 10px 16px;
|
padding: 10px 16px;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
width: 100%; /* 确保占满宽度 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.back-btn {
|
.back-btn {
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px; /* 固定宽度防止压缩 */
|
||||||
|
height: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.back-icon {
|
.back-icon {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
color: #333;
|
color: #333;
|
||||||
|
font-weight: bold;
|
||||||
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-input-container {
|
.search-input-container {
|
||||||
flex: 1;
|
flex: 1; /* 占据剩余空间 */
|
||||||
height: 36px;
|
height: 40px; /*稍微增高一点以容纳按钮*/
|
||||||
background-color: #f0f0f0;
|
background-color: #f0f0f0;
|
||||||
border-radius: 18px;
|
border-radius: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: row; /* UVUE 必须显式设置 row */
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 12px;
|
padding: 0 4px 0 12px;
|
||||||
}
|
|
||||||
|
|
||||||
.search-icon {
|
|
||||||
font-size: 16px;
|
|
||||||
margin-right: 8px;
|
|
||||||
color: #999;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-input {
|
.search-input {
|
||||||
@@ -505,6 +670,7 @@ const goBack = () => {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #333;
|
color: #333;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
background-color: transparent; /* 确保背景透明 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholder {
|
.placeholder {
|
||||||
@@ -513,6 +679,10 @@ const goBack = () => {
|
|||||||
|
|
||||||
.clear-btn {
|
.clear-btn {
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
|
margin-right: 2px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clear-icon {
|
.clear-icon {
|
||||||
@@ -520,13 +690,37 @@ const goBack = () => {
|
|||||||
color: #999;
|
color: #999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-btn {
|
.camera-btn {
|
||||||
padding: 4px 0;
|
padding: 4px 8px 4px 4px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-right-width: 1px; /* UVUE 边框写法 */
|
||||||
|
border-right-style: solid;
|
||||||
|
border-right-color: #ddd;
|
||||||
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-btn-text {
|
.camera-icon {
|
||||||
font-size: 15px;
|
font-size: 20px;
|
||||||
color: #4CAF50;
|
}
|
||||||
|
|
||||||
|
/* 内部搜索按钮样式 */
|
||||||
|
.inner-search-btn {
|
||||||
|
padding: 0 16px;
|
||||||
|
background-color: #87CEEB;
|
||||||
|
border-radius: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner-search-text {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #ffffff;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -540,22 +734,27 @@ const goBack = () => {
|
|||||||
/* 模块通用头部 */
|
/* 模块通用头部 */
|
||||||
.section-header {
|
.section-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: row; /* UVUE 显式设置 row */
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #333;
|
color: #333;
|
||||||
|
flex: 1; /* 占据左侧空间 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-right {
|
.header-right {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: row; /* UVUE 显式设置 row */
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
flex-shrink: 0; /* 防止被压缩 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.clear-text {
|
.clear-text {
|
||||||
@@ -570,12 +769,16 @@ const goBack = () => {
|
|||||||
/* 搜索历史 */
|
/* 搜索历史 */
|
||||||
.search-history {
|
.search-history {
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
|
padding: 0 4px; /* 微调内边距 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-tags {
|
.history-tags {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-direction: row; /* UVUE 显式设置 row */
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
flex-wrap: wrap; /* 允许换行 */
|
||||||
|
padding: 0 4px;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-tag {
|
.history-tag {
|
||||||
@@ -585,7 +788,7 @@ const goBack = () => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
max-width: 100%;
|
flex-shrink: 0; /* 防止被压缩 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-text {
|
.history-text {
|
||||||
@@ -619,17 +822,21 @@ const goBack = () => {
|
|||||||
|
|
||||||
.hot-tags {
|
.hot-tags {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-direction: row; /* UVUE 显式设置 row */
|
||||||
|
flex-wrap: wrap; /* 允许换行 */
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
padding: 0 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hot-tag {
|
.hot-tag {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
border-radius: 4px;
|
border-radius: 16px; /* 增加圆角,像胶囊一样 */
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
flex-shrink: 0; /* 防止被压缩 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.hot-tag.hot {
|
.hot-tag.hot {
|
||||||
@@ -773,9 +980,12 @@ const goBack = () => {
|
|||||||
|
|
||||||
.results-header {
|
.results-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: row; /* UVUE 显式设置 row */
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
|
flex-wrap: wrap; /* 允许换行以适应小屏 */
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.results-title {
|
.results-title {
|
||||||
@@ -786,12 +996,16 @@ const goBack = () => {
|
|||||||
|
|
||||||
.filter-tabs {
|
.filter-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: row; /* UVUE 显式设置 row */
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
flex: 1; /* 自适应填充剩余空间 */
|
||||||
|
justify-content: flex-end; /* 靠右对齐 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-tab {
|
.filter-tab {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #666;
|
color: #666;
|
||||||
|
padding: 4px 8px; /* 增加点击区域 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-tab.active {
|
.filter-tab.active {
|
||||||
@@ -800,22 +1014,56 @@ const goBack = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.results-list {
|
.results-list {
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-direction: column;
|
grid-template-columns: repeat(2, 1fr); /* 默认移动端双列 */
|
||||||
gap: 12px;
|
gap: 10px;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式布局 */
|
||||||
|
/* 平板设备 (768px以上) */
|
||||||
|
@media screen and (min-width: 768px) {
|
||||||
|
.results-list {
|
||||||
|
grid-template-columns: repeat(3, 1fr); /* 平板显示3列 */
|
||||||
|
gap: 16px;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guess-grid {
|
||||||
|
grid-template-columns: repeat(4, 1fr); /* 猜你喜欢在平板上显示4列 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 桌面设备 (1024px以上) */
|
||||||
|
@media screen and (min-width: 1024px) {
|
||||||
|
.results-list {
|
||||||
|
grid-template-columns: repeat(4, 1fr); /* 桌面显示4列 */
|
||||||
|
gap: 20px;
|
||||||
|
padding: 0 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guess-grid {
|
||||||
|
grid-template-columns: repeat(6, 1fr); /* 猜你喜欢在桌面上显示6列 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 桌面端调整图片高度 */
|
||||||
|
.product-image {
|
||||||
|
height: 160px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-item {
|
.result-item {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 10px;
|
padding: 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
flex-direction: column; /* 垂直排列 */
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-image {
|
.product-image {
|
||||||
width: 100px;
|
width: 100%;
|
||||||
height: 100px;
|
height: 120px; /* 调整图片高度 */
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-color: #f0f0f0;
|
background-color: #f0f0f0;
|
||||||
}
|
}
|
||||||
@@ -828,44 +1076,47 @@ const goBack = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.product-name {
|
.product-name {
|
||||||
font-size: 15px;
|
font-size: 13px; /* 减小字号 */
|
||||||
color: #333;
|
color: #333;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
line-height: 1.4;
|
line-height: 1.3;
|
||||||
|
height: 34px; /* 限制高度 */
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-tags-row {
|
.product-tags-row {
|
||||||
margin-top: 4px;
|
margin-top: 2px;
|
||||||
}
|
display: none; /* 隐藏标签以保持简洁 */
|
||||||
|
|
||||||
.product-tag {
|
|
||||||
font-size: 10px;
|
|
||||||
color: #ff5000;
|
|
||||||
border: 1px solid #ff5000;
|
|
||||||
padding: 1px 4px;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-spec {
|
.product-spec {
|
||||||
font-size: 12px;
|
display: none; /* 隐藏规格 */
|
||||||
color: #999;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-bottom {
|
.product-bottom {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: flex-end;
|
align-items: center; /* 垂直居中 */
|
||||||
margin-top: 8px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.price-box {
|
.price-box {
|
||||||
color: #ff5000;
|
color: #ff5000;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-symbol {
|
||||||
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.price-value {
|
.price-value {
|
||||||
font-size: 18px;
|
font-size: 16px; /* 减小价格字号 */
|
||||||
font-weight: bold;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-cart-btn {
|
.add-cart-btn {
|
||||||
@@ -880,7 +1131,7 @@ const goBack = () => {
|
|||||||
|
|
||||||
.cart-icon {
|
.cart-icon {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 16px;
|
font-size: 14px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
375
pages/mall/consumer/shop-detail.uvue
Normal file
375
pages/mall/consumer/shop-detail.uvue
Normal file
@@ -0,0 +1,375 @@
|
|||||||
|
<template>
|
||||||
|
<view class="shop-detail-page">
|
||||||
|
<!-- 店铺头部信息 -->
|
||||||
|
<view class="shop-header">
|
||||||
|
<image :src="merchant.shop_banner || '/static/default-banner.png'" class="shop-banner" mode="aspectFill" />
|
||||||
|
<view class="shop-info-card">
|
||||||
|
<image :src="merchant.shop_logo || '/static/default-shop.png'" class="shop-logo" />
|
||||||
|
<view class="shop-basic-info">
|
||||||
|
<text class="shop-name">{{ merchant.shop_name }}</text>
|
||||||
|
<view class="shop-stats">
|
||||||
|
<text class="stat-item">⭐ {{ merchant.rating.toFixed(1) }}</text>
|
||||||
|
<text class="stat-item">销量 {{ merchant.total_sales }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<button class="follow-btn" @click="toggleFollow">{{ isFollowed ? '已关注' : '+ 关注' }}</button>
|
||||||
|
</view>
|
||||||
|
<text class="shop-desc">{{ merchant.shop_description || '这家店很懒,什么都没写~' }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 商品列表 -->
|
||||||
|
<view class="product-section">
|
||||||
|
<view class="section-title">全部商品</view>
|
||||||
|
<view class="product-grid">
|
||||||
|
<view v-for="product in products" :key="product.id" class="product-item" @click="goToProduct(product.id)">
|
||||||
|
<image :src="product.images[0]" class="product-image" mode="aspectFill" />
|
||||||
|
<view class="product-info">
|
||||||
|
<text class="product-name">{{ product.name }}</text>
|
||||||
|
<view class="price-row">
|
||||||
|
<view class="price-left">
|
||||||
|
<text class="product-price">¥{{ product.price }}</text>
|
||||||
|
<text class="product-sales">已售 {{ product.sales }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="cart-btn" @click.stop="addToCart(product)">
|
||||||
|
<text class="cart-icon">🛒</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="uts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { MerchantType, ProductType } from '@/types/mall-types.uts'
|
||||||
|
|
||||||
|
const merchant = ref<MerchantType>({
|
||||||
|
id: '',
|
||||||
|
user_id: '',
|
||||||
|
shop_name: '',
|
||||||
|
shop_logo: '',
|
||||||
|
shop_banner: '',
|
||||||
|
shop_description: '',
|
||||||
|
contact_name: '',
|
||||||
|
contact_phone: '',
|
||||||
|
shop_status: 0,
|
||||||
|
rating: 0,
|
||||||
|
total_sales: 0,
|
||||||
|
created_at: ''
|
||||||
|
} as MerchantType)
|
||||||
|
|
||||||
|
const products = ref<ProductType[]>([])
|
||||||
|
const isFollowed = ref(false)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const pages = getCurrentPages()
|
||||||
|
const options = pages[pages.length - 1].options as any
|
||||||
|
const merchantId = options['merchantId'] as string
|
||||||
|
|
||||||
|
if (merchantId) {
|
||||||
|
loadShopData(merchantId)
|
||||||
|
loadShopProducts(merchantId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const loadShopData = (id: string) => {
|
||||||
|
// 模拟加载店铺数据
|
||||||
|
merchant.value = {
|
||||||
|
id: id,
|
||||||
|
user_id: 'user_001',
|
||||||
|
shop_name: '优质好店',
|
||||||
|
shop_logo: '/static/shop-logo.png',
|
||||||
|
shop_banner: '/static/shop-banner.png',
|
||||||
|
shop_description: '专注品质生活,为您提供最优质的商品和服务。',
|
||||||
|
contact_name: '店主小王',
|
||||||
|
contact_phone: '13800138000',
|
||||||
|
shop_status: 1,
|
||||||
|
rating: 4.8,
|
||||||
|
total_sales: 15680,
|
||||||
|
created_at: '2023-06-01'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadShopProducts = (id: string) => {
|
||||||
|
// 模拟加载店铺商品列表
|
||||||
|
products.value = [
|
||||||
|
{
|
||||||
|
id: 'prod_001',
|
||||||
|
merchant_id: id,
|
||||||
|
category_id: 'cat_001',
|
||||||
|
name: '精选好物商品 A',
|
||||||
|
description: '商品描述 A',
|
||||||
|
images: ['/static/product1.jpg'],
|
||||||
|
price: 199.99,
|
||||||
|
original_price: 299.99,
|
||||||
|
stock: 100,
|
||||||
|
sales: 1256,
|
||||||
|
status: 1,
|
||||||
|
created_at: '2024-01-15'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'prod_002',
|
||||||
|
merchant_id: id,
|
||||||
|
category_id: 'cat_001',
|
||||||
|
name: '精选好物商品 B',
|
||||||
|
description: '商品描述 B',
|
||||||
|
images: ['/static/product2.jpg'],
|
||||||
|
price: 299.00,
|
||||||
|
original_price: 399.00,
|
||||||
|
stock: 50,
|
||||||
|
sales: 856,
|
||||||
|
status: 1,
|
||||||
|
created_at: '2024-01-16'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'prod_003',
|
||||||
|
merchant_id: id,
|
||||||
|
category_id: 'cat_002',
|
||||||
|
name: '精选好物商品 C',
|
||||||
|
description: '商品描述 C',
|
||||||
|
images: ['/static/product3.jpg'],
|
||||||
|
price: 99.00,
|
||||||
|
original_price: 129.00,
|
||||||
|
stock: 200,
|
||||||
|
sales: 3256,
|
||||||
|
status: 1,
|
||||||
|
created_at: '2024-01-17'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleFollow = () => {
|
||||||
|
isFollowed.value = !isFollowed.value
|
||||||
|
uni.showToast({
|
||||||
|
title: isFollowed.value ? '关注成功' : '已取消关注',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const addToCart = (product: ProductType) => {
|
||||||
|
// 获取现有购物车数据
|
||||||
|
const cartData = uni.getStorageSync('cart')
|
||||||
|
let cartItems: any[] = []
|
||||||
|
|
||||||
|
if (cartData) {
|
||||||
|
try {
|
||||||
|
cartItems = JSON.parse(cartData as string) as any[]
|
||||||
|
} catch (e) {
|
||||||
|
console.error('解析购物车数据失败', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查商品是否已存在
|
||||||
|
const existingItem = cartItems.find((item: any) => item.productId === product.id)
|
||||||
|
|
||||||
|
if (existingItem) {
|
||||||
|
existingItem.quantity++
|
||||||
|
} else {
|
||||||
|
// 添加新商品
|
||||||
|
cartItems.push({
|
||||||
|
id: product.id, // 简单使用产品ID作为购物车ID,实际可能有规格
|
||||||
|
productId: product.id,
|
||||||
|
shopId: merchant.value.id,
|
||||||
|
shopName: merchant.value.shop_name,
|
||||||
|
name: product.name,
|
||||||
|
price: product.price,
|
||||||
|
image: product.images[0],
|
||||||
|
spec: '默认规格',
|
||||||
|
quantity: 1,
|
||||||
|
selected: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存回存储
|
||||||
|
uni.setStorageSync('cart', JSON.stringify(cartItems))
|
||||||
|
|
||||||
|
uni.showToast({
|
||||||
|
title: '已添加到购物车',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToProduct = (id: string) => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/mall/consumer/product-detail?productId=${id}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.shop-detail-page {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-header {
|
||||||
|
background-color: #fff;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-banner {
|
||||||
|
width: 100%;
|
||||||
|
height: 150px;
|
||||||
|
background-color: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-info-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 15px;
|
||||||
|
margin-top: -30px; /* Logo 向上重叠 banner */
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-logo {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
background-color: #fff;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-basic-info {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-top: 30px; /* 给 logo 上浮留空间 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-name {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-stats {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
margin-right: 12px;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.follow-btn {
|
||||||
|
font-size: 14px;
|
||||||
|
background-color: #ff4444;
|
||||||
|
color: white;
|
||||||
|
padding: 6px 16px;
|
||||||
|
border-radius: 20px;
|
||||||
|
margin-top: 30px; /* 对齐 */
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-desc {
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
padding: 10px 15px 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-section {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding-left: 8px;
|
||||||
|
border-left: 4px solid #ff4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-item {
|
||||||
|
width: calc(50% - 5px);
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 170px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info {
|
||||||
|
padding: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-name {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 40px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-left {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cart-btn {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
background-color: #ff4444;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cart-icon {
|
||||||
|
font-size: 14px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-price {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #ff4444;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-sales {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user