继续完善购物逻辑闭环,consumer模块完成度75%
This commit is contained in:
@@ -1021,4 +1021,13 @@ function buildSupabaseFilterQuery(filter : UTSJSONObject | null) : string {
|
|||||||
return params.join('&');
|
return params.join('&');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 Supabase 客户端实例
|
||||||
|
* @param url 项目 URL
|
||||||
|
* @param key 项目匿名密钥 (Anon Key)
|
||||||
|
*/
|
||||||
|
export function createClient(url : string, key : string) : AkSupa {
|
||||||
|
return new AkSupa(url, key);
|
||||||
|
}
|
||||||
|
|
||||||
export default AkSupa;
|
export default AkSupa;
|
||||||
69
pages.json
69
pages.json
@@ -1,5 +1,12 @@
|
|||||||
{
|
{
|
||||||
"pages": [
|
"pages": [
|
||||||
|
{
|
||||||
|
"path": "pages/user/login",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "用户登录",
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/mall/consumer/index",
|
"path": "pages/mall/consumer/index",
|
||||||
"style": {
|
"style": {
|
||||||
@@ -81,6 +88,68 @@
|
|||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "编辑地址"
|
"navigationBarTitleText": "编辑地址"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/mall/consumer/checkout",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "确认订单"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/mall/consumer/payment",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "收银台"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/mall/consumer/payment-success",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "支付成功",
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/mall/consumer/orders",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "我的订单",
|
||||||
|
"enablePullDownRefresh": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/mall/consumer/order-detail",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "订单详情"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/mall/consumer/logistics",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "物流详情"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/mall/consumer/review",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "评价晒单"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/mall/consumer/refund",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "退款/售后"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/mall/consumer/apply-refund",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "申请售后"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/mall/consumer/refund-review",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "服务评价"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"tabBar": {
|
"tabBar": {
|
||||||
|
|||||||
@@ -20,6 +20,11 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="form-group">
|
<view class="form-group">
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="label">智能填写</text>
|
||||||
|
<textarea class="smart-textarea" v-model="smartInput" placeholder="粘贴姓名+电话+地址,自动识别填充" @input="parseSmartInput" maxlength="200"></textarea>
|
||||||
|
<text class="smart-tip">示例:张三 13800138000 北京市朝阳区三里屯SOHO A座</text>
|
||||||
|
</view>
|
||||||
<view class="form-item">
|
<view class="form-item">
|
||||||
<text class="label">标签</text>
|
<text class="label">标签</text>
|
||||||
<view class="tags-container">
|
<view class="tags-container">
|
||||||
@@ -65,6 +70,7 @@ const isEdit = ref(false)
|
|||||||
const addressId = ref('')
|
const addressId = ref('')
|
||||||
const regionString = ref('')
|
const regionString = ref('')
|
||||||
const tags = ['家', '公司', '学校']
|
const tags = ['家', '公司', '学校']
|
||||||
|
const smartInput = ref('')
|
||||||
|
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
name: '',
|
name: '',
|
||||||
@@ -203,6 +209,41 @@ const saveAddress = () => {
|
|||||||
}, 1500)
|
}, 1500)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const parseSmartInput = () => {
|
||||||
|
const input = smartInput.value.trim()
|
||||||
|
if (!input) return
|
||||||
|
|
||||||
|
// 提取手机号
|
||||||
|
const phoneRegex = /(1[3-9]\d{9})/
|
||||||
|
const phoneMatch = input.match(phoneRegex)
|
||||||
|
if (phoneMatch) {
|
||||||
|
formData.phone = phoneMatch[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取姓名(取第一个2-4位中文)
|
||||||
|
const nameRegex = /([\u4e00-\u9fa5]{2,4})/
|
||||||
|
const nameMatch = input.match(nameRegex)
|
||||||
|
if (nameMatch) {
|
||||||
|
formData.name = nameMatch[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 去掉姓名和电话后剩余作为地址
|
||||||
|
let addrText = input
|
||||||
|
if (formData.name) addrText = addrText.replace(formData.name, '')
|
||||||
|
if (formData.phone) addrText = addrText.replace(formData.phone, '')
|
||||||
|
addrText = addrText.replace(/[,,;;\s]+/g, ' ').trim()
|
||||||
|
|
||||||
|
// 解析省市区
|
||||||
|
const pattern1 = /^(.*?省)?(.*?市)?(.*?[区县])?(.*)$/
|
||||||
|
const m = addrText.match(pattern1)
|
||||||
|
if (m) {
|
||||||
|
const [, province, city, district, detail] = m
|
||||||
|
regionString.value = `${(province||'').trim()} ${(city||'').trim()} ${(district||'').trim()}`.trim()
|
||||||
|
formData.detail = (detail||'').trim()
|
||||||
|
} else {
|
||||||
|
formData.detail = addrText
|
||||||
|
}
|
||||||
|
}
|
||||||
const deleteAddress = () => {
|
const deleteAddress = () => {
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: '提示',
|
title: '提示',
|
||||||
|
|||||||
@@ -45,11 +45,27 @@ type Address = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const addresses = ref<Address[]>([])
|
const addresses = ref<Address[]>([])
|
||||||
|
const selectionMode = ref<boolean>(false)
|
||||||
|
let openerEventChannel: any = null
|
||||||
|
|
||||||
onShow(() => {
|
onShow(() => {
|
||||||
loadAddresses()
|
loadAddresses()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
try {
|
||||||
|
const ec = uni.getOpenerEventChannel()
|
||||||
|
openerEventChannel = ec
|
||||||
|
ec?.on('setSelectMode', (data: any) => {
|
||||||
|
if (data && typeof data.selectMode === 'boolean') {
|
||||||
|
selectionMode.value = data.selectMode
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const loadAddresses = () => {
|
const loadAddresses = () => {
|
||||||
const storedAddresses = uni.getStorageSync('addresses')
|
const storedAddresses = uni.getStorageSync('addresses')
|
||||||
if (storedAddresses) {
|
if (storedAddresses) {
|
||||||
@@ -95,9 +111,21 @@ const editAddress = (id: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const selectAddress = (item: Address) => {
|
const selectAddress = (item: Address) => {
|
||||||
// 如果是选择地址模式(例如从订单确认页过来),则返回并传递地址
|
if (selectionMode.value && openerEventChannel) {
|
||||||
// 目前暂未实现选择模式,仅作为普通点击
|
openerEventChannel.emit('addressSelected', {
|
||||||
|
id: item.id,
|
||||||
|
recipient_name: item.name,
|
||||||
|
phone: item.phone,
|
||||||
|
province: item.province,
|
||||||
|
city: item.city,
|
||||||
|
district: item.district,
|
||||||
|
detail: item.detail,
|
||||||
|
is_default: item.isDefault
|
||||||
|
})
|
||||||
|
uni.navigateBack()
|
||||||
|
} else {
|
||||||
editAddress(item.id)
|
editAddress(item.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -55,10 +55,78 @@
|
|||||||
</scroll-view>
|
</scroll-view>
|
||||||
|
|
||||||
<!-- 添加地址按钮 -->
|
<!-- 添加地址按钮 -->
|
||||||
<view class="add-address-btn" @click="addNewAddress">
|
<view class="add-address-btn" @click="showNewAddressForm = true">
|
||||||
<text class="btn-icon">+</text>
|
<text class="btn-icon">+</text>
|
||||||
<text class="btn-text">添加新地址</text>
|
<text class="btn-text">添加新地址</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 新建地址表单弹窗 -->
|
||||||
|
<view v-if="showNewAddressForm" class="address-form-mask" @click="cancelNewAddress">
|
||||||
|
<view class="address-form-popup" @click.stop>
|
||||||
|
<view class="form-header">
|
||||||
|
<text class="form-title">新建收货地址</text>
|
||||||
|
<text class="form-close" @click="cancelNewAddress">×</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view class="form-content" scroll-y>
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">收货人</text>
|
||||||
|
<input class="form-input" v-model="newAddress.recipient_name"
|
||||||
|
placeholder="请输入收货人姓名" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">手机号</text>
|
||||||
|
<input class="form-input" v-model="newAddress.phone"
|
||||||
|
placeholder="请输入手机号码" type="number" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 地址智能填写 -->
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">智能填写地址</text>
|
||||||
|
<textarea class="form-textarea smart-address-input"
|
||||||
|
v-model="smartAddressInput"
|
||||||
|
placeholder="请输入完整地址,系统将自动识别省市区和详细地址"
|
||||||
|
@blur="parseSmartAddress"
|
||||||
|
maxlength="200" />
|
||||||
|
<text class="smart-tip">例如:北京市朝阳区三里屯SOHO A座</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">所在地区</text>
|
||||||
|
<view class="region-inputs">
|
||||||
|
<input class="form-input region-input" v-model="newAddress.province"
|
||||||
|
placeholder="省" readonly />
|
||||||
|
<input class="form-input region-input" v-model="newAddress.city"
|
||||||
|
placeholder="市" readonly />
|
||||||
|
<input class="form-input region-input" v-model="newAddress.district"
|
||||||
|
placeholder="区/县" readonly />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">详细地址</text>
|
||||||
|
<textarea class="form-textarea" v-model="newAddress.detail"
|
||||||
|
placeholder="街道、小区、楼栋、门牌号等"
|
||||||
|
maxlength="100" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-item checkbox-item">
|
||||||
|
<view class="checkbox-wrapper" @click="newAddress.is_default = !newAddress.is_default">
|
||||||
|
<view :class="['checkbox', { checked: newAddress.is_default }]">
|
||||||
|
<text v-if="newAddress.is_default" class="checkbox-check">✓</text>
|
||||||
|
</view>
|
||||||
|
<text class="checkbox-label">设为默认地址</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<view class="form-buttons">
|
||||||
|
<button class="form-cancel-btn" @click="cancelNewAddress">取消</button>
|
||||||
|
<button class="form-submit-btn" @click="saveNewAddress">保存</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -83,6 +151,17 @@ type AddressType = {
|
|||||||
const addressList = ref<Array<AddressType>>([])
|
const addressList = ref<Array<AddressType>>([])
|
||||||
const fromSelect = ref<boolean>(false)
|
const fromSelect = ref<boolean>(false)
|
||||||
const selectCallback = ref<any>(null)
|
const selectCallback = ref<any>(null)
|
||||||
|
const showNewAddressForm = ref<boolean>(false)
|
||||||
|
const newAddress = ref<any>({
|
||||||
|
recipient_name: '',
|
||||||
|
phone: '',
|
||||||
|
province: '',
|
||||||
|
city: '',
|
||||||
|
district: '',
|
||||||
|
detail: '',
|
||||||
|
is_default: false
|
||||||
|
})
|
||||||
|
const smartAddressInput = ref<string>('')
|
||||||
|
|
||||||
// 生命周期
|
// 生命周期
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -95,6 +174,16 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadAddresses()
|
loadAddresses()
|
||||||
|
|
||||||
|
// 监听地址更新事件(从checkout页面或其他页面)
|
||||||
|
uni.$on('addressUpdated', (updatedAddressList: any) => {
|
||||||
|
addressList.value = updatedAddressList
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 组件卸载时移除事件监听
|
||||||
|
onUnmounted(() => {
|
||||||
|
uni.$off('addressUpdated')
|
||||||
})
|
})
|
||||||
|
|
||||||
// 加载地址列表
|
// 加载地址列表
|
||||||
@@ -287,11 +376,193 @@ const setAsDefault = async (address: AddressType) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加新地址
|
// 解析智能地址
|
||||||
const addNewAddress = () => {
|
const parseSmartAddress = () => {
|
||||||
uni.navigateTo({
|
const input = smartAddressInput.value.trim()
|
||||||
url: '/pages/mall/consumer/address-edit'
|
if (!input) return
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
newAddress.value.recipient_name = ''
|
||||||
|
newAddress.value.phone = ''
|
||||||
|
newAddress.value.province = ''
|
||||||
|
newAddress.value.city = ''
|
||||||
|
newAddress.value.district = ''
|
||||||
|
newAddress.value.detail = ''
|
||||||
|
|
||||||
|
// 尝试匹配手机号码(11位数字)
|
||||||
|
const phoneRegex = /(1[3-9]\d{9})/g
|
||||||
|
const phoneMatches = input.match(phoneRegex)
|
||||||
|
if (phoneMatches && phoneMatches.length > 0) {
|
||||||
|
newAddress.value.phone = phoneMatches[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试匹配收件人姓名(中文姓名,2-4个汉字)
|
||||||
|
const nameRegex = /([\u4e00-\u9fa5]{2,4})/g
|
||||||
|
const nameMatches = input.match(nameRegex)
|
||||||
|
if (nameMatches && nameMatches.length > 0) {
|
||||||
|
// 取第一个匹配的中文姓名作为收件人
|
||||||
|
newAddress.value.recipient_name = nameMatches[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取地址部分(移除姓名和手机号)
|
||||||
|
let addressText = input
|
||||||
|
if (newAddress.value.recipient_name) {
|
||||||
|
addressText = addressText.replace(newAddress.value.recipient_name, '')
|
||||||
|
}
|
||||||
|
if (newAddress.value.phone) {
|
||||||
|
addressText = addressText.replace(newAddress.value.phone, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理地址文本(移除多余的空格和标点)
|
||||||
|
addressText = addressText.replace(/[,,;;\s]+/g, ' ').trim()
|
||||||
|
|
||||||
|
// 地址解析逻辑
|
||||||
|
const patterns = [
|
||||||
|
// 匹配格式:省市区详细地址
|
||||||
|
/^(.*?省)?(.*?市)?(.*?[区县])?(.*)$/,
|
||||||
|
// 匹配格式:省市详细地址
|
||||||
|
/^(.*?省)?(.*?市)?(.*)$/
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
const match = addressText.match(pattern)
|
||||||
|
if (match) {
|
||||||
|
const [, province, city, district, detail] = match
|
||||||
|
|
||||||
|
if (province) newAddress.value.province = province.replace('省', '').trim()
|
||||||
|
if (city) newAddress.value.city = city.replace('市', '').trim()
|
||||||
|
if (district) newAddress.value.district = district.trim()
|
||||||
|
if (detail) newAddress.value.detail = detail.trim()
|
||||||
|
|
||||||
|
// 如果详细地址为空,但还有剩余内容,则作为详细地址
|
||||||
|
if (!newAddress.value.detail && district && detail) {
|
||||||
|
newAddress.value.detail = detail.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有匹配到模式,尝试简单分割
|
||||||
|
if (!newAddress.value.province && !newAddress.value.city && !newAddress.value.district) {
|
||||||
|
// 尝试按常见分隔符分割
|
||||||
|
const parts = addressText.split(/[省市县区]/)
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
newAddress.value.province = parts[0] || ''
|
||||||
|
newAddress.value.city = parts[1] || ''
|
||||||
|
newAddress.value.detail = parts.slice(2).join('').trim() || addressText
|
||||||
|
} else {
|
||||||
|
newAddress.value.detail = addressText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果地址部分为空,但还有剩余文本,则作为详细地址
|
||||||
|
if (!newAddress.value.detail && addressText.trim()) {
|
||||||
|
newAddress.value.detail = addressText.trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存新地址
|
||||||
|
const saveNewAddress = async () => {
|
||||||
|
// 验证表单
|
||||||
|
if (!newAddress.value.recipient_name || !newAddress.value.phone || !newAddress.value.detail) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '请填写完整信息',
|
||||||
|
icon: 'none'
|
||||||
})
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = getCurrentUserId()
|
||||||
|
if (!userId) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '请先登录',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data, error } = await supa
|
||||||
|
.from('user_addresses')
|
||||||
|
.insert({
|
||||||
|
user_id: userId,
|
||||||
|
recipient_name: newAddress.value.recipient_name,
|
||||||
|
phone: newAddress.value.phone,
|
||||||
|
province: newAddress.value.province,
|
||||||
|
city: newAddress.value.city,
|
||||||
|
district: newAddress.value.district,
|
||||||
|
detail: newAddress.value.detail,
|
||||||
|
is_default: newAddress.value.is_default,
|
||||||
|
created_at: new Date().toISOString()
|
||||||
|
})
|
||||||
|
.select()
|
||||||
|
.single()
|
||||||
|
|
||||||
|
if (error !== null) {
|
||||||
|
console.error('保存地址失败:', error)
|
||||||
|
uni.showToast({
|
||||||
|
title: '保存失败',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是默认地址,取消其他默认地址
|
||||||
|
if (newAddress.value.is_default) {
|
||||||
|
addressList.value.forEach(addr => {
|
||||||
|
addr.is_default = false
|
||||||
|
})
|
||||||
|
|
||||||
|
// 更新数据库中的其他地址
|
||||||
|
await supa
|
||||||
|
.from('user_addresses')
|
||||||
|
.update({ is_default: false })
|
||||||
|
.eq('user_id', userId)
|
||||||
|
.neq('id', data.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加到列表
|
||||||
|
addressList.value.unshift(data)
|
||||||
|
|
||||||
|
// 发布地址更新事件,让checkout页面也能获取到
|
||||||
|
uni.$emit('addressUpdated', addressList.value)
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
resetNewAddressForm()
|
||||||
|
|
||||||
|
uni.showToast({
|
||||||
|
title: '地址保存成功',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('保存地址异常:', err)
|
||||||
|
uni.showToast({
|
||||||
|
title: '保存失败',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置新建地址表单
|
||||||
|
const resetNewAddressForm = () => {
|
||||||
|
showNewAddressForm.value = false
|
||||||
|
newAddress.value = {
|
||||||
|
recipient_name: '',
|
||||||
|
phone: '',
|
||||||
|
province: '',
|
||||||
|
city: '',
|
||||||
|
district: '',
|
||||||
|
detail: '',
|
||||||
|
is_default: false
|
||||||
|
}
|
||||||
|
smartAddressInput.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消新建地址
|
||||||
|
const cancelNewAddress = () => {
|
||||||
|
resetNewAddressForm()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -471,4 +742,177 @@ const addNewAddress = () => {
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 新建地址表单弹窗样式 */
|
||||||
|
.address-form-mask {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1001;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-form-popup {
|
||||||
|
background-color: #ffffff;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 500px;
|
||||||
|
max-height: 80vh;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-header {
|
||||||
|
padding: 20px 15px;
|
||||||
|
border-bottom: 1px solid #e5e5e5;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-close {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #999999;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 15px;
|
||||||
|
max-height: 50vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333333;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333333;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input[readonly] {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
color: #666666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.region-inputs {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.region-input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 80px;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333333;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smart-address-input {
|
||||||
|
min-height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smart-tip {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999999;
|
||||||
|
margin-top: 5px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-item {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-right: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox.checked {
|
||||||
|
background-color: #007aff;
|
||||||
|
border-color: #007aff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-check {
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-buttons {
|
||||||
|
display: flex;
|
||||||
|
padding: 15px;
|
||||||
|
border-top: 1px solid #e5e5e5;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-cancel-btn {
|
||||||
|
flex: 1;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
color: #333333;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-submit-btn {
|
||||||
|
flex: 1;
|
||||||
|
background-color: #007aff;
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
293
pages/mall/consumer/apply-refund.uvue
Normal file
293
pages/mall/consumer/apply-refund.uvue
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
<template>
|
||||||
|
<view class="apply-refund-page">
|
||||||
|
<view class="section">
|
||||||
|
<view class="section-title">退款类型</view>
|
||||||
|
<radio-group @change="handleTypeChange" class="type-group">
|
||||||
|
<label class="type-item">
|
||||||
|
<radio value="1" :checked="refundType === 1" color="#ff4444" />
|
||||||
|
<text>仅退款</text>
|
||||||
|
</label>
|
||||||
|
<label class="type-item">
|
||||||
|
<radio value="2" :checked="refundType === 2" color="#ff4444" />
|
||||||
|
<text>退货退款</text>
|
||||||
|
</label>
|
||||||
|
</radio-group>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section">
|
||||||
|
<view class="section-title">退款原因</view>
|
||||||
|
<picker @change="handleReasonChange" :range="reasonList" class="picker">
|
||||||
|
<view class="picker-content">
|
||||||
|
<text v-if="refundReason">{{ refundReason }}</text>
|
||||||
|
<text v-else class="placeholder">请选择退款原因</text>
|
||||||
|
<text class="arrow">></text>
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section">
|
||||||
|
<view class="section-title">退款金额</view>
|
||||||
|
<view class="amount-input-wrap">
|
||||||
|
<text class="currency">¥</text>
|
||||||
|
<input
|
||||||
|
type="digit"
|
||||||
|
v-model="refundAmount"
|
||||||
|
class="amount-input"
|
||||||
|
:placeholder="`最多可退 ¥${maxAmount}`"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<text class="amount-tip">最多可退 ¥{{ maxAmount }},含发货邮费 ¥{{ deliveryFee }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section">
|
||||||
|
<view class="section-title">退款说明</view>
|
||||||
|
<textarea
|
||||||
|
v-model="description"
|
||||||
|
class="desc-input"
|
||||||
|
placeholder="选填:补充详细的退款说明,有助于商家快速处理"
|
||||||
|
maxlength="200"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="submit-bar">
|
||||||
|
<button class="submit-btn" @click="submitRefund" :loading="submitting">提交申请</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="uts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
// import supa from '@/components/supadb/aksupainstance.uts'
|
||||||
|
|
||||||
|
const orderId = ref('')
|
||||||
|
const orderItemId = ref('') // Optional, if refunding specific item
|
||||||
|
const refundType = ref(1) // 1: Only Refund, 2: Return & Refund
|
||||||
|
const refundReason = ref('')
|
||||||
|
const refundAmount = ref('')
|
||||||
|
const description = ref('')
|
||||||
|
const maxAmount = ref(0)
|
||||||
|
const deliveryFee = ref(0)
|
||||||
|
const submitting = ref(false)
|
||||||
|
|
||||||
|
const reasonList = [
|
||||||
|
'多拍/错拍/不想要',
|
||||||
|
'快递一直未送达',
|
||||||
|
'未按约定时间发货',
|
||||||
|
'快递无记录',
|
||||||
|
'空包裹/少货/错发',
|
||||||
|
'质量问题',
|
||||||
|
'其他'
|
||||||
|
]
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const pages = getCurrentPages()
|
||||||
|
const currentPage = pages[pages.length - 1]
|
||||||
|
const options = currentPage.options as any
|
||||||
|
|
||||||
|
if (options.orderId) {
|
||||||
|
orderId.value = options.orderId
|
||||||
|
loadOrderInfo()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const loadOrderInfo = async () => {
|
||||||
|
try {
|
||||||
|
const { data, error } = await supa
|
||||||
|
.from('orders')
|
||||||
|
.select('actual_amount, delivery_fee')
|
||||||
|
.eq('id', orderId.value)
|
||||||
|
.single()
|
||||||
|
|
||||||
|
if (error == null && data != null) {
|
||||||
|
maxAmount.value = data['actual_amount'] as number
|
||||||
|
deliveryFee.value = data['delivery_fee'] as number
|
||||||
|
refundAmount.value = maxAmount.value.toString()
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('加载订单信息失败', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTypeChange = (e: any) => {
|
||||||
|
refundType.value = parseInt(e.detail.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleReasonChange = (e: any) => {
|
||||||
|
const index = e.detail.value as number
|
||||||
|
refundReason.value = reasonList[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitRefund = async () => {
|
||||||
|
if (!refundReason.value) {
|
||||||
|
uni.showToast({ title: '请选择退款原因', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const amount = parseFloat(refundAmount.value)
|
||||||
|
if (isNaN(amount) || amount <= 0 || amount > maxAmount.value) {
|
||||||
|
uni.showToast({ title: '请输入有效的退款金额', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
submitting.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userStore = uni.getStorageSync('userInfo')
|
||||||
|
const userId = userStore?.id
|
||||||
|
|
||||||
|
// 1. Create Refund Record
|
||||||
|
/* const { data, error } = await supa
|
||||||
|
.from('refunds')
|
||||||
|
.insert({
|
||||||
|
user_id: userId,
|
||||||
|
order_id: orderId.value,
|
||||||
|
refund_no: 'REF' + Date.now(),
|
||||||
|
refund_type: refundType.value,
|
||||||
|
refund_reason: refundReason.value,
|
||||||
|
refund_amount: amount,
|
||||||
|
description: description.value,
|
||||||
|
status: 1, // 待处理
|
||||||
|
status_history: [{
|
||||||
|
status: 1,
|
||||||
|
remark: '用户提交申请',
|
||||||
|
created_at: new Date().toISOString()
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error != null) throw error */
|
||||||
|
|
||||||
|
// MOCK SUBMIT
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||||
|
|
||||||
|
// 2. Update Order Status (Optional, e.g. to "After-sales")
|
||||||
|
// Assuming status 6 is "After-sales/Refund"
|
||||||
|
/*
|
||||||
|
await supa
|
||||||
|
.from('orders')
|
||||||
|
.update({ status: 6 })
|
||||||
|
.eq('id', orderId.value)
|
||||||
|
*/
|
||||||
|
|
||||||
|
uni.showToast({ title: '提交成功', icon: 'success' })
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.redirectTo({
|
||||||
|
url: '/pages/mall/consumer/refund'
|
||||||
|
})
|
||||||
|
}, 1500)
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('提交退款失败', err)
|
||||||
|
uni.showToast({ title: '提交失败,请重试', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.apply-refund-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding: 15px;
|
||||||
|
padding-bottom: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow {
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-input-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.currency {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-input {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-tip {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 100px;
|
||||||
|
font-size: 14px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-bar {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background-color: #ffffff;
|
||||||
|
padding: 15px;
|
||||||
|
box-shadow: 0 -2px 10px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
background-color: #ff4444;
|
||||||
|
color: #ffffff;
|
||||||
|
border-radius: 22px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,12 +1,6 @@
|
|||||||
<!-- 结算页面 -->
|
<!-- 结算页面 -->
|
||||||
<template>
|
<template>
|
||||||
<view class="checkout-page">
|
<view class="checkout-page">
|
||||||
<!-- 顶部栏 -->
|
|
||||||
<view class="checkout-header">
|
|
||||||
<text class="back-btn" @click="goBack">‹</text>
|
|
||||||
<text class="header-title">订单结算</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<scroll-view class="checkout-content" scroll-y>
|
<scroll-view class="checkout-content" scroll-y>
|
||||||
<!-- 收货地址 -->
|
<!-- 收货地址 -->
|
||||||
<view class="address-section" @click="selectAddress">
|
<view class="address-section" @click="selectAddress">
|
||||||
@@ -107,12 +101,120 @@
|
|||||||
</view>
|
</view>
|
||||||
<button class="submit-btn" @click="submitOrder">提交订单</button>
|
<button class="submit-btn" @click="submitOrder">提交订单</button>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 地址选择弹窗 -->
|
||||||
|
<view v-if="showAddressPopup" class="address-popup-mask" @click="showAddressPopup = false">
|
||||||
|
<view class="address-popup" @click.stop>
|
||||||
|
<view class="popup-header">
|
||||||
|
<text class="popup-title">选择收货地址</text>
|
||||||
|
<text class="popup-close" @click="showAddressPopup = false">×</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view class="address-list-container" scroll-y>
|
||||||
|
<!-- 地址列表 -->
|
||||||
|
<view v-if="addressList.length > 0">
|
||||||
|
<view v-for="address in addressList" :key="address.id"
|
||||||
|
class="popup-address-item" @click="handleSelectAddress(address)">
|
||||||
|
<view class="popup-address-header">
|
||||||
|
<text class="popup-address-name">{{ address.recipient_name }}</text>
|
||||||
|
<text class="popup-address-phone">{{ address.phone }}</text>
|
||||||
|
<view v-if="address.is_default" class="popup-default-tag">
|
||||||
|
<text class="popup-tag-text">默认</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<text class="popup-address-detail">{{ getFullAddress(address) }}</text>
|
||||||
|
<view v-if="selectedAddress && selectedAddress.id === address.id" class="popup-selected-indicator">
|
||||||
|
<text>✓</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<view v-else class="popup-empty-address">
|
||||||
|
<text class="popup-empty-icon">📍</text>
|
||||||
|
<text class="popup-empty-text">暂无收货地址</text>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<!-- 新建地址按钮 -->
|
||||||
|
<view class="popup-add-address-btn" @click="handleAddNewAddress">
|
||||||
|
<text class="popup-btn-icon">+</text>
|
||||||
|
<text class="popup-btn-text">新建收货地址</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 新建地址表单弹窗 -->
|
||||||
|
<view v-if="showNewAddressForm" class="address-form-mask" @click="cancelNewAddress">
|
||||||
|
<view class="address-form-popup" @click.stop>
|
||||||
|
<view class="form-header">
|
||||||
|
<text class="form-title">新建收货地址</text>
|
||||||
|
<text class="form-close" @click="cancelNewAddress">×</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view class="form-content" scroll-y>
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">收货人</text>
|
||||||
|
<input class="form-input" v-model="newAddress.recipient_name"
|
||||||
|
placeholder="请输入收货人姓名" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">手机号</text>
|
||||||
|
<input class="form-input" v-model="newAddress.phone"
|
||||||
|
placeholder="请输入手机号码" type="number" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">智能填写地址</text>
|
||||||
|
<textarea class="form-textarea smart-address-input"
|
||||||
|
v-model="smartAddressInput"
|
||||||
|
placeholder="粘贴如:北京市朝阳区三里屯SOHO A座 张三 13800138000"
|
||||||
|
@input="parseSmartAddress"
|
||||||
|
maxlength="200" />
|
||||||
|
<text class="smart-tip">自动识别:地址+姓名+电话(支持粘贴文本)</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">所在地区</text>
|
||||||
|
<view class="region-inputs">
|
||||||
|
<input class="form-input region-input" v-model="newAddress.province"
|
||||||
|
placeholder="省" readonly />
|
||||||
|
<input class="form-input region-input" v-model="newAddress.city"
|
||||||
|
placeholder="市" readonly />
|
||||||
|
<input class="form-input region-input" v-model="newAddress.district"
|
||||||
|
placeholder="区/县" readonly />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">详细地址</text>
|
||||||
|
<textarea class="form-textarea" v-model="newAddress.detail"
|
||||||
|
placeholder="街道、小区、楼栋、门牌号等"
|
||||||
|
maxlength="100" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-item checkbox-item">
|
||||||
|
<view class="checkbox-wrapper" @click="newAddress.is_default = !newAddress.is_default">
|
||||||
|
<view :class="['checkbox', { checked: newAddress.is_default }]">
|
||||||
|
<text v-if="newAddress.is_default" class="checkbox-check">✓</text>
|
||||||
|
</view>
|
||||||
|
<text class="checkbox-label">设为默认地址</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<view class="form-buttons">
|
||||||
|
<button class="form-cancel-btn" @click="cancelNewAddress">取消</button>
|
||||||
|
<button class="form-submit-btn" @click="saveNewAddress">保存</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<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 CheckoutItemType = {
|
type CheckoutItemType = {
|
||||||
id: string
|
id: string
|
||||||
@@ -150,6 +252,20 @@ const deliveryOptions = ref<Array<DeliveryOptionType>>([
|
|||||||
const selectedDelivery = ref<string>('standard')
|
const selectedDelivery = ref<string>('standard')
|
||||||
const selectedCoupon = ref<UserCouponType | null>(null)
|
const selectedCoupon = ref<UserCouponType | null>(null)
|
||||||
const remark = ref<string>('')
|
const remark = ref<string>('')
|
||||||
|
const showAddressPopup = ref<boolean>(false)
|
||||||
|
const addressList = ref<Array<any>>([])
|
||||||
|
const newAddress = ref<any>({
|
||||||
|
recipient_name: '',
|
||||||
|
phone: '',
|
||||||
|
province: '',
|
||||||
|
city: '',
|
||||||
|
district: '',
|
||||||
|
detail: '',
|
||||||
|
is_default: false
|
||||||
|
})
|
||||||
|
const showNewAddressForm = ref<boolean>(false)
|
||||||
|
const showSaveConfirm = ref<boolean>(false)
|
||||||
|
const smartAddressInput = ref<string>('')
|
||||||
|
|
||||||
// 计算属性
|
// 计算属性
|
||||||
const totalAmount = computed(() => {
|
const totalAmount = computed(() => {
|
||||||
@@ -180,6 +296,24 @@ const actualAmount = computed(() => {
|
|||||||
// 生命周期
|
// 生命周期
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadCheckoutData()
|
loadCheckoutData()
|
||||||
|
|
||||||
|
// 监听地址更新事件
|
||||||
|
uni.$on('addressUpdated', (updatedAddressList: any) => {
|
||||||
|
addressList.value = updatedAddressList
|
||||||
|
|
||||||
|
// 如果当前没有选中地址,尝试选择默认地址
|
||||||
|
if (!selectedAddress.value && addressList.value.length > 0) {
|
||||||
|
const defaultAddress = addressList.value.find(addr => addr.is_default)
|
||||||
|
if (defaultAddress) {
|
||||||
|
selectedAddress.value = defaultAddress
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 组件卸载时移除事件监听
|
||||||
|
onUnmounted(() => {
|
||||||
|
uni.$off('addressUpdated')
|
||||||
})
|
})
|
||||||
|
|
||||||
// 加载结算数据
|
// 加载结算数据
|
||||||
@@ -196,25 +330,67 @@ const loadCheckoutData = () => {
|
|||||||
|
|
||||||
// 加载默认地址
|
// 加载默认地址
|
||||||
const loadDefaultAddress = async () => {
|
const loadDefaultAddress = async () => {
|
||||||
const userId = getCurrentUserId()
|
// 从本地存储加载地址数据
|
||||||
if (!userId) return
|
const storedAddresses = uni.getStorageSync('addresses')
|
||||||
|
if (storedAddresses) {
|
||||||
try {
|
try {
|
||||||
const { data, error } = await supa
|
const addresses = JSON.parse(storedAddresses as string) as any[]
|
||||||
.from('user_addresses')
|
if (addresses && addresses.length > 0) {
|
||||||
.select('*')
|
// 查找默认地址
|
||||||
.eq('user_id', userId)
|
const defaultAddress = addresses.find((addr: any) => addr.isDefault === true)
|
||||||
.eq('is_default', true)
|
if (defaultAddress) {
|
||||||
.single()
|
// 转换地址格式以匹配selectedAddress的结构
|
||||||
|
selectedAddress.value = {
|
||||||
if (error !== null) {
|
id: defaultAddress.id,
|
||||||
console.error('加载默认地址失败:', error)
|
recipient_name: defaultAddress.name,
|
||||||
return
|
phone: defaultAddress.phone,
|
||||||
|
province: defaultAddress.province,
|
||||||
|
city: defaultAddress.city,
|
||||||
|
district: defaultAddress.district,
|
||||||
|
detail: defaultAddress.detail,
|
||||||
|
is_default: defaultAddress.isDefault
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果没有默认地址,使用第一个地址
|
||||||
|
const firstAddress = addresses[0]
|
||||||
|
selectedAddress.value = {
|
||||||
|
id: firstAddress.id,
|
||||||
|
recipient_name: firstAddress.name,
|
||||||
|
phone: firstAddress.phone,
|
||||||
|
province: firstAddress.province,
|
||||||
|
city: firstAddress.city,
|
||||||
|
district: firstAddress.district,
|
||||||
|
detail: firstAddress.detail,
|
||||||
|
is_default: firstAddress.isDefault
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedAddress.value = data
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('加载默认地址异常:', err)
|
console.error('解析地址数据失败:', err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果没有地址数据,尝试使用Mock数据初始化(为了演示效果)
|
||||||
|
const mockAddress = {
|
||||||
|
id: 'addr_mock_default',
|
||||||
|
name: '测试用户',
|
||||||
|
phone: '13800138000',
|
||||||
|
province: '北京市',
|
||||||
|
city: '北京市',
|
||||||
|
district: '朝阳区',
|
||||||
|
detail: '三里屯SOHO A座',
|
||||||
|
isDefault: true
|
||||||
|
}
|
||||||
|
uni.setStorageSync('addresses', JSON.stringify([mockAddress]))
|
||||||
|
selectedAddress.value = {
|
||||||
|
id: mockAddress.id,
|
||||||
|
recipient_name: mockAddress.name,
|
||||||
|
phone: mockAddress.phone,
|
||||||
|
province: mockAddress.province,
|
||||||
|
city: mockAddress.city,
|
||||||
|
district: mockAddress.district,
|
||||||
|
detail: mockAddress.detail,
|
||||||
|
is_default: mockAddress.isDefault
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,6 +405,276 @@ const getFullAddress = (address: any): string => {
|
|||||||
return `${address.province}${address.city}${address.district}${address.detail}`
|
return `${address.province}${address.city}${address.district}${address.detail}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加载地址列表
|
||||||
|
const loadAddressList = async () => {
|
||||||
|
// 从本地存储加载地址数据
|
||||||
|
const storedAddresses = uni.getStorageSync('addresses')
|
||||||
|
if (storedAddresses) {
|
||||||
|
try {
|
||||||
|
const addresses = JSON.parse(storedAddresses as string) as any[]
|
||||||
|
if (addresses && addresses.length > 0) {
|
||||||
|
// 转换地址格式以匹配addressList的结构
|
||||||
|
addressList.value = addresses.map((addr: any) => ({
|
||||||
|
id: addr.id,
|
||||||
|
recipient_name: addr.name,
|
||||||
|
phone: addr.phone,
|
||||||
|
province: addr.province,
|
||||||
|
city: addr.city,
|
||||||
|
district: addr.district,
|
||||||
|
detail: addr.detail,
|
||||||
|
is_default: addr.isDefault
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
addressList.value = []
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('解析地址数据失败:', err)
|
||||||
|
addressList.value = []
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addressList.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择地址
|
||||||
|
const handleSelectAddress = (address: any) => {
|
||||||
|
selectedAddress.value = address
|
||||||
|
showAddressPopup.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新建地址
|
||||||
|
const handleAddNewAddress = () => {
|
||||||
|
showNewAddressForm.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存新地址
|
||||||
|
const saveNewAddress = async () => {
|
||||||
|
// 验证表单
|
||||||
|
if (!newAddress.value.recipient_name || !newAddress.value.phone || !newAddress.value.detail) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '请填写完整信息',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = getCurrentUserId()
|
||||||
|
if (!userId) return
|
||||||
|
|
||||||
|
// 触发保存确认弹窗
|
||||||
|
showSaveConfirm.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理保存确认
|
||||||
|
const handleSaveConfirm = async (save: boolean) => {
|
||||||
|
showSaveConfirm.value = false
|
||||||
|
|
||||||
|
const newAddressData = {
|
||||||
|
id: `addr_${Date.now()}`,
|
||||||
|
name: newAddress.value.recipient_name,
|
||||||
|
phone: newAddress.value.phone,
|
||||||
|
province: newAddress.value.province,
|
||||||
|
city: newAddress.value.city,
|
||||||
|
district: newAddress.value.district,
|
||||||
|
detail: newAddress.value.detail,
|
||||||
|
isDefault: newAddress.value.is_default,
|
||||||
|
label: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
if (save) {
|
||||||
|
// 从本地存储加载现有地址
|
||||||
|
const storedAddresses = uni.getStorageSync('addresses')
|
||||||
|
let addresses: any[] = []
|
||||||
|
if (storedAddresses) {
|
||||||
|
try {
|
||||||
|
addresses = JSON.parse(storedAddresses as string) as any[]
|
||||||
|
} catch (e) {
|
||||||
|
console.error('解析地址数据失败:', e)
|
||||||
|
addresses = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是默认地址,取消其他默认地址
|
||||||
|
if (newAddressData.isDefault) {
|
||||||
|
addresses.forEach(addr => {
|
||||||
|
addr.isDefault = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是第一个地址且未设置默认,则自动设为默认
|
||||||
|
if (addresses.length === 0 && !newAddressData.isDefault) {
|
||||||
|
newAddressData.isDefault = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加到地址列表
|
||||||
|
addresses.unshift(newAddressData)
|
||||||
|
|
||||||
|
// 保存到本地存储
|
||||||
|
uni.setStorageSync('addresses', JSON.stringify(addresses))
|
||||||
|
|
||||||
|
// 发布地址更新事件,让address-list页面也能获取到
|
||||||
|
uni.$emit('addressUpdated', addresses.map((addr) => ({
|
||||||
|
id: addr.id,
|
||||||
|
recipient_name: addr.name,
|
||||||
|
phone: addr.phone,
|
||||||
|
province: addr.province,
|
||||||
|
city: addr.city,
|
||||||
|
district: addr.district,
|
||||||
|
detail: addr.detail,
|
||||||
|
is_default: addr.isDefault
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为checkout页面格式并添加到当前地址列表
|
||||||
|
const checkoutFormatAddress = {
|
||||||
|
id: newAddressData.id,
|
||||||
|
recipient_name: newAddressData.name,
|
||||||
|
phone: newAddressData.phone,
|
||||||
|
province: newAddressData.province,
|
||||||
|
city: newAddressData.city,
|
||||||
|
district: newAddressData.district,
|
||||||
|
detail: newAddressData.detail,
|
||||||
|
is_default: newAddressData.isDefault
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是默认地址,取消其他默认地址
|
||||||
|
if (checkoutFormatAddress.is_default) {
|
||||||
|
addressList.value.forEach(addr => {
|
||||||
|
addr.is_default = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加到当前地址列表
|
||||||
|
addressList.value.unshift(checkoutFormatAddress)
|
||||||
|
|
||||||
|
// 如果选择了保存,address-list 已通过事件收到更新;未保存则不影响地址簿
|
||||||
|
|
||||||
|
// 如果保存的是默认地址,或者当前没有选中地址,则选中这个新地址
|
||||||
|
if (checkoutFormatAddress.is_default || !selectedAddress.value) {
|
||||||
|
selectedAddress.value = checkoutFormatAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
newAddress.value = {
|
||||||
|
recipient_name: '',
|
||||||
|
phone: '',
|
||||||
|
province: '',
|
||||||
|
city: '',
|
||||||
|
district: '',
|
||||||
|
detail: '',
|
||||||
|
is_default: false
|
||||||
|
}
|
||||||
|
smartAddressInput.value = ''
|
||||||
|
|
||||||
|
showNewAddressForm.value = false
|
||||||
|
|
||||||
|
uni.showToast({
|
||||||
|
title: '地址保存成功',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析智能地址
|
||||||
|
const parseSmartAddress = () => {
|
||||||
|
const input = smartAddressInput.value.trim()
|
||||||
|
if (!input) return
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
newAddress.value.recipient_name = ''
|
||||||
|
newAddress.value.phone = ''
|
||||||
|
newAddress.value.province = ''
|
||||||
|
newAddress.value.city = ''
|
||||||
|
newAddress.value.district = ''
|
||||||
|
newAddress.value.detail = ''
|
||||||
|
|
||||||
|
// 尝试匹配手机号码(11位数字)
|
||||||
|
const phoneRegex = /(1[3-9]\d{9})/g
|
||||||
|
const phoneMatches = input.match(phoneRegex)
|
||||||
|
if (phoneMatches && phoneMatches.length > 0) {
|
||||||
|
newAddress.value.phone = phoneMatches[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试匹配收件人姓名(中文姓名,2-4个汉字)
|
||||||
|
const nameRegex = /([\u4e00-\u9fa5]{2,4})/g
|
||||||
|
const nameMatches = input.match(nameRegex)
|
||||||
|
if (nameMatches && nameMatches.length > 0) {
|
||||||
|
// 取第一个匹配的中文姓名作为收件人
|
||||||
|
newAddress.value.recipient_name = nameMatches[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取地址部分(移除姓名和手机号)
|
||||||
|
let addressText = input
|
||||||
|
if (newAddress.value.recipient_name) {
|
||||||
|
addressText = addressText.replace(newAddress.value.recipient_name, '')
|
||||||
|
}
|
||||||
|
if (newAddress.value.phone) {
|
||||||
|
addressText = addressText.replace(newAddress.value.phone, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理地址文本(移除多余的空格和标点)
|
||||||
|
addressText = addressText.replace(/[,,;;\s]+/g, ' ').trim()
|
||||||
|
|
||||||
|
// 地址解析逻辑
|
||||||
|
const patterns = [
|
||||||
|
// 匹配格式:省市区详细地址
|
||||||
|
/^(.*?省)?(.*?市)?(.*?[区县])?(.*)$/,
|
||||||
|
// 匹配格式:省市详细地址
|
||||||
|
/^(.*?省)?(.*?市)?(.*)$/
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
const match = addressText.match(pattern)
|
||||||
|
if (match) {
|
||||||
|
const [, province, city, district, detail] = match
|
||||||
|
|
||||||
|
if (province) newAddress.value.province = province.replace('省', '').trim()
|
||||||
|
if (city) newAddress.value.city = city.replace('市', '').trim()
|
||||||
|
if (district) newAddress.value.district = district.trim()
|
||||||
|
if (detail) newAddress.value.detail = detail.trim()
|
||||||
|
|
||||||
|
// 如果详细地址为空,但还有剩余内容,则作为详细地址
|
||||||
|
if (!newAddress.value.detail && district && detail) {
|
||||||
|
newAddress.value.detail = detail.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有匹配到模式,尝试简单分割
|
||||||
|
if (!newAddress.value.province && !newAddress.value.city && !newAddress.value.district) {
|
||||||
|
// 尝试按常见分隔符分割
|
||||||
|
const parts = addressText.split(/[省市县区]/)
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
newAddress.value.province = parts[0] || ''
|
||||||
|
newAddress.value.city = parts[1] || ''
|
||||||
|
newAddress.value.detail = parts.slice(2).join('').trim() || addressText
|
||||||
|
} else {
|
||||||
|
newAddress.value.detail = addressText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果地址部分为空,但还有剩余文本,则作为详细地址
|
||||||
|
if (!newAddress.value.detail && addressText.trim()) {
|
||||||
|
newAddress.value.detail = addressText.trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消新建地址
|
||||||
|
const cancelNewAddress = () => {
|
||||||
|
showNewAddressForm.value = false
|
||||||
|
newAddress.value = {
|
||||||
|
recipient_name: '',
|
||||||
|
phone: '',
|
||||||
|
province: '',
|
||||||
|
city: '',
|
||||||
|
district: '',
|
||||||
|
detail: '',
|
||||||
|
is_default: false
|
||||||
|
}
|
||||||
|
smartAddressInput.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
// 获取规格文本
|
// 获取规格文本
|
||||||
const getSpecText = (specs: any): string => {
|
const getSpecText = (specs: any): string => {
|
||||||
if (!specs) return ''
|
if (!specs) return ''
|
||||||
@@ -242,14 +688,8 @@ const getSpecText = (specs: any): string => {
|
|||||||
|
|
||||||
// 选择地址
|
// 选择地址
|
||||||
const selectAddress = () => {
|
const selectAddress = () => {
|
||||||
uni.navigateTo({
|
showAddressPopup.value = true
|
||||||
url: '/pages/mall/consumer/address',
|
loadAddressList()
|
||||||
events: {
|
|
||||||
addressSelected: (address: any) => {
|
|
||||||
selectedAddress.value = address
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 选择配送方式
|
// 选择配送方式
|
||||||
@@ -282,6 +722,12 @@ const submitOrder = async () => {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// 模拟提交成功跳转
|
||||||
|
// 实际项目中应等待API返回
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/mall/consumer/payment?orderId=ORDER_MOCK_${Date.now()}&amount=${actualAmount.value}`
|
||||||
|
})
|
||||||
|
return;
|
||||||
|
|
||||||
const userId = getCurrentUserId()
|
const userId = getCurrentUserId()
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
@@ -313,7 +759,7 @@ const submitOrder = async () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 创建订单
|
// 创建订单
|
||||||
const { data: order, error: orderError } = await supa
|
/* const { data: order, error: orderError } = await supa
|
||||||
.from('orders')
|
.from('orders')
|
||||||
.insert(orderData)
|
.insert(orderData)
|
||||||
.select()
|
.select()
|
||||||
@@ -364,6 +810,15 @@ const submitOrder = async () => {
|
|||||||
// 跳转到支付页面
|
// 跳转到支付页面
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: `/pages/mall/consumer/payment?orderId=${order.id}&amount=${actualAmount.value}`
|
url: `/pages/mall/consumer/payment?orderId=${order.id}&amount=${actualAmount.value}`
|
||||||
|
}) */
|
||||||
|
|
||||||
|
// MOCK ORDER SUBMISSION
|
||||||
|
// 模拟创建成功
|
||||||
|
const mockOrderId = `order_mock_${Date.now()}`
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500))
|
||||||
|
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/mall/consumer/payment?orderId=${mockOrderId}&amount=${actualAmount.value}`
|
||||||
})
|
})
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -393,7 +848,7 @@ const clearShoppingCart = async () => {
|
|||||||
const productIds = checkoutItems.value.map(item => item.product_id)
|
const productIds = checkoutItems.value.map(item => item.product_id)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { error } = await supa
|
/* const { error } = await supa
|
||||||
.from('shopping_cart')
|
.from('shopping_cart')
|
||||||
.delete()
|
.delete()
|
||||||
.eq('user_id', userId)
|
.eq('user_id', userId)
|
||||||
@@ -401,7 +856,7 @@ const clearShoppingCart = async () => {
|
|||||||
|
|
||||||
if (error !== null) {
|
if (error !== null) {
|
||||||
console.error('清空购物车失败:', error)
|
console.error('清空购物车失败:', error)
|
||||||
}
|
} */
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('清空购物车异常:', err)
|
console.error('清空购物车异常:', err)
|
||||||
}
|
}
|
||||||
@@ -421,21 +876,16 @@ const goBack = () => {
|
|||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 删除未使用的顶部栏样式 */
|
||||||
.checkout-header {
|
.checkout-header {
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
border-bottom: 1px solid #e5e5e5;
|
border-bottom: 1px solid #e5e5e5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.back-btn {
|
|
||||||
font-size: 24px;
|
|
||||||
color: #333333;
|
|
||||||
padding: 5px;
|
|
||||||
margin-right: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-title {
|
.header-title {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@@ -736,4 +1186,323 @@ const goBack = () => {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 地址选择弹窗样式 */
|
||||||
|
.address-popup-mask {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 9998;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-popup {
|
||||||
|
background-color: #ffffff;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 70vh;
|
||||||
|
border-radius: 20px 20px 0 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-header {
|
||||||
|
padding: 20px 15px;
|
||||||
|
border-bottom: 1px solid #e5e5e5;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-close {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #999999;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-list-container {
|
||||||
|
flex: 1;
|
||||||
|
padding: 15px;
|
||||||
|
max-height: 50vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-address-item {
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
border-radius: 8px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-address-item:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-address-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-address-name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333333;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-address-phone {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666666;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-default-tag {
|
||||||
|
background-color: #ff4757;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-tag-text {
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-address-detail {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333333;
|
||||||
|
line-height: 1.4;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-selected-indicator {
|
||||||
|
position: absolute;
|
||||||
|
top: 15px;
|
||||||
|
right: 15px;
|
||||||
|
color: #007aff;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-empty-address {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-empty-icon {
|
||||||
|
font-size: 60px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-empty-text {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #999999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-add-address-btn {
|
||||||
|
background-color: #007aff;
|
||||||
|
margin: 15px;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-btn-icon {
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 20px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-btn-text {
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 新建地址表单弹窗样式 */
|
||||||
|
.address-form-mask {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-form-popup {
|
||||||
|
background-color: #ffffff;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 500px;
|
||||||
|
max-height: 80vh;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-header {
|
||||||
|
padding: 20px 15px;
|
||||||
|
border-bottom: 1px solid #e5e5e5;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-close {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #999999;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 15px;
|
||||||
|
max-height: 50vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333333;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333333;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input[readonly] {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
color: #666666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.region-inputs {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.region-input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 80px;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333333;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smart-address-input {
|
||||||
|
min-height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smart-tip {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999999;
|
||||||
|
margin-top: 5px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-item {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-right: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox.checked {
|
||||||
|
background-color: #007aff;
|
||||||
|
border-color: #007aff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-check {
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-buttons {
|
||||||
|
display: flex;
|
||||||
|
padding: 15px;
|
||||||
|
border-top: 1px solid #e5e5e5;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-cancel-btn {
|
||||||
|
flex: 1;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
color: #333333;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-submit-btn {
|
||||||
|
flex: 1;
|
||||||
|
background-color: #007aff;
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
204
pages/mall/consumer/logistics.uvue
Normal file
204
pages/mall/consumer/logistics.uvue
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
<template>
|
||||||
|
<view class="logistics-page">
|
||||||
|
<view class="logistics-header">
|
||||||
|
<view class="product-info">
|
||||||
|
<image class="product-image" :src="productImage" mode="aspectFill"></image>
|
||||||
|
<view class="info-right">
|
||||||
|
<text class="status-text">{{ logisticsStatus }}</text>
|
||||||
|
<text class="courier-name">{{ courierName }}: {{ trackingNo }}</text>
|
||||||
|
<text class="phone-text">官方电话: {{ courierPhone }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="logistics-body">
|
||||||
|
<view class="track-list">
|
||||||
|
<view
|
||||||
|
v-for="(item, index) in trackList"
|
||||||
|
:key="index"
|
||||||
|
class="track-item"
|
||||||
|
:class="{ first: index === 0 }"
|
||||||
|
>
|
||||||
|
<view class="node-icon">
|
||||||
|
<view class="dot"></view>
|
||||||
|
<view class="line" v-if="index !== trackList.length - 1"></view>
|
||||||
|
</view>
|
||||||
|
<view class="node-content">
|
||||||
|
<text class="track-desc">{{ item.desc }}</text>
|
||||||
|
<text class="track-time">{{ item.time }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="uts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
|
||||||
|
const orderId = ref('')
|
||||||
|
const productImage = ref('/static/product1.jpg')
|
||||||
|
const logisticsStatus = ref('运输中')
|
||||||
|
const courierName = ref('顺丰速运')
|
||||||
|
const courierPhone = ref('95338')
|
||||||
|
const trackingNo = ref('SF1234567890')
|
||||||
|
|
||||||
|
type TrackItem = {
|
||||||
|
desc: string
|
||||||
|
time: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const trackList = ref<TrackItem[]>([
|
||||||
|
{
|
||||||
|
desc: '【深圳市】快件已到达 深圳南山集散中心',
|
||||||
|
time: '2024-01-26 14:30:00'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: '【广州市】快件已从 广州转运中心 发出,准备发往 深圳南山集散中心',
|
||||||
|
time: '2024-01-26 09:20:00'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: '【广州市】快件已到达 广州转运中心',
|
||||||
|
time: '2024-01-25 22:15:00'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: '【杭州市】商家已发货',
|
||||||
|
time: '2024-01-25 18:00:00'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const pages = getCurrentPages()
|
||||||
|
const currentPage = pages[pages.length - 1]
|
||||||
|
const options = currentPage.options as any
|
||||||
|
if (options.orderId) {
|
||||||
|
orderId.value = options.orderId
|
||||||
|
// 这里可以根据orderId去请求真实的物流信息
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.logistics-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logistics-header {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-image {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-right: 15px;
|
||||||
|
background-color: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-right {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #ff5000;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.courier-name {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logistics-body {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 20px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-item {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
padding-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-item:last-child {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-icon {
|
||||||
|
width: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #ccc;
|
||||||
|
margin-top: 6px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.first .dot {
|
||||||
|
background-color: #ff5000;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
margin-top: 4px;
|
||||||
|
box-shadow: 0 0 0 4px rgba(255, 80, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.line {
|
||||||
|
width: 1px;
|
||||||
|
background-color: #eee;
|
||||||
|
flex: 1;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-desc {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.first .track-desc {
|
||||||
|
color: #ff5000;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-time {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -91,6 +91,7 @@
|
|||||||
<button v-if="order.status === 2" class="remind-btn" @click="remindDelivery">提醒发货</button>
|
<button v-if="order.status === 2" class="remind-btn" @click="remindDelivery">提醒发货</button>
|
||||||
<button v-if="order.status === 3" class="confirm-btn" @click="confirmReceive">确认收货</button>
|
<button v-if="order.status === 3" class="confirm-btn" @click="confirmReceive">确认收货</button>
|
||||||
<button v-if="order.status === 4" class="review-btn" @click="goToReview">评价商品</button>
|
<button v-if="order.status === 4" class="review-btn" @click="goToReview">评价商品</button>
|
||||||
|
<button v-if="order.status >= 2 && order.status <= 4" class="refund-btn" @click="applyRefund">申请售后</button>
|
||||||
<button v-if="order.status <= 2" class="cancel-btn" @click="cancelOrder">取消订单</button>
|
<button v-if="order.status <= 2" class="cancel-btn" @click="cancelOrder">取消订单</button>
|
||||||
<button class="service-btn" @click="contactService">联系客服</button>
|
<button class="service-btn" @click="contactService">联系客服</button>
|
||||||
</view>
|
</view>
|
||||||
@@ -310,6 +311,12 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
applyRefund() {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/mall/consumer/apply-refund?orderId=${this.order.id}`
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
cancelOrder() {
|
cancelOrder() {
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: '取消订单',
|
title: '取消订单',
|
||||||
@@ -578,6 +585,12 @@ export default {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.refund-btn {
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #666;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
.cancel-btn {
|
.cancel-btn {
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
color: #666;
|
color: #666;
|
||||||
|
|||||||
@@ -147,6 +147,8 @@
|
|||||||
|
|
||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import { ref, reactive, onMounted, computed } from 'vue'
|
import { ref, reactive, onMounted, computed } from 'vue'
|
||||||
|
import { onShow, onLoad } from '@dcloudio/uni-app'
|
||||||
|
// // import supa from '@/components/supadb/aksupainstance.uts'
|
||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
const orders = ref<any[]>([])
|
const orders = ref<any[]>([])
|
||||||
@@ -317,19 +319,96 @@ const filteredOrders = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 生命周期
|
// 生命周期
|
||||||
onMounted(() => {
|
onLoad((options) => {
|
||||||
|
if (options['status']) {
|
||||||
|
const status = options['status'] as string
|
||||||
|
if (['all', 'pending', 'shipping', 'delivering', 'completed', 'cancelled'].includes(status)) {
|
||||||
|
activeTab.value = status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (options['type']) {
|
||||||
|
const type = options['type'] as string
|
||||||
|
if (type === 'pending') activeTab.value = 'pending'
|
||||||
|
else if (type === 'shipped') activeTab.value = 'delivering' // 映射到待收货
|
||||||
|
else if (type === 'review') activeTab.value = 'completed' // 映射到已完成
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
loadOrders()
|
loadOrders()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 加载订单数据
|
// 加载订单数据
|
||||||
const loadOrders = () => {
|
const loadOrders = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
const userStore = uni.getStorageSync('userInfo')
|
||||||
|
const userId = userStore?.id
|
||||||
|
|
||||||
// 模拟API请求延迟
|
if (!userId) {
|
||||||
setTimeout(() => {
|
|
||||||
orders.value = [...mockOrders]
|
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}, 800)
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Mock Data Loading
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500)) // Simulate network delay
|
||||||
|
|
||||||
|
// Use filtered mock orders based on user ID logic if needed, but for now just return all mock orders
|
||||||
|
// In a real app we'd filter by userId
|
||||||
|
|
||||||
|
/* const { data, error } = await supa
|
||||||
|
.from('orders')
|
||||||
|
.select('*, order_items(*)')
|
||||||
|
.eq('user_id', userId)
|
||||||
|
.order('created_at', { ascending: false })
|
||||||
|
|
||||||
|
if (error != null) {
|
||||||
|
console.error('加载订单失败:', error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data != null) {
|
||||||
|
orders.value = data.map((order: any) => ({
|
||||||
|
id: order.id,
|
||||||
|
order_no: order.order_no,
|
||||||
|
status: order.status,
|
||||||
|
create_time: formatDate(order.created_at),
|
||||||
|
product_amount: order.total_amount,
|
||||||
|
shipping_fee: order.delivery_fee,
|
||||||
|
total_amount: order.actual_amount,
|
||||||
|
products: (order.order_items as any[]).map((item: any) => ({
|
||||||
|
id: item.product_id,
|
||||||
|
name: item.product_name,
|
||||||
|
price: item.price,
|
||||||
|
image: '/static/products/1.jpg', // 默认图片
|
||||||
|
spec: formatSpec(item.sku_specifications),
|
||||||
|
quantity: item.quantity
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
} */
|
||||||
|
|
||||||
|
// Using Mock Data
|
||||||
|
orders.value = mockOrders
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('加载订单异常:', err)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDate = (isoString: string): string => {
|
||||||
|
if (!isoString) return ''
|
||||||
|
const date = new Date(isoString)
|
||||||
|
return `${date.getFullYear()}-${(date.getMonth()+1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatSpec = (specs: any): string => {
|
||||||
|
if (!specs) return ''
|
||||||
|
if (typeof specs === 'object') {
|
||||||
|
return Object.keys(specs).map(key => `${key}:${specs[key]}`).join(' ')
|
||||||
|
}
|
||||||
|
return String(specs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换标签
|
// 切换标签
|
||||||
@@ -381,21 +460,8 @@ const onRefresh = () => {
|
|||||||
const loadMore = () => {
|
const loadMore = () => {
|
||||||
if (loadingMore.value || !hasMore.value) return
|
if (loadingMore.value || !hasMore.value) return
|
||||||
|
|
||||||
loadingMore.value = true
|
// 暂未实现分页,直接返回
|
||||||
|
hasMore.value = false
|
||||||
// 模拟加载更多数据
|
|
||||||
setTimeout(() => {
|
|
||||||
const newOrders = [...mockOrders].map((order, index) => ({
|
|
||||||
...order,
|
|
||||||
id: `${order.id}_${page.value}${index}`,
|
|
||||||
order_no: `${order.order_no}_${page.value}${index}`
|
|
||||||
}))
|
|
||||||
|
|
||||||
orders.value = [...orders.value, ...newOrders]
|
|
||||||
loadingMore.value = false
|
|
||||||
page.value++
|
|
||||||
hasMore.value = orders.value.length < 20
|
|
||||||
}, 1200)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 订单操作函数
|
// 订单操作函数
|
||||||
|
|||||||
173
pages/mall/consumer/payment-success.uvue
Normal file
173
pages/mall/consumer/payment-success.uvue
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
<template>
|
||||||
|
<view class="payment-success-page">
|
||||||
|
<view class="success-content">
|
||||||
|
<view class="icon-wrapper">
|
||||||
|
<text class="success-icon">✓</text>
|
||||||
|
</view>
|
||||||
|
<text class="success-title">支付成功</text>
|
||||||
|
<text class="success-desc">您的订单已支付成功,我们将尽快为您发货</text>
|
||||||
|
|
||||||
|
<view class="order-info" v-if="orderId">
|
||||||
|
<text class="info-text">订单编号:{{ orderNo }}</text>
|
||||||
|
<text class="info-text">支付金额:¥{{ amount.toFixed(2) }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="action-buttons">
|
||||||
|
<button class="btn primary-btn" @click="viewOrder">查看订单</button>
|
||||||
|
<button class="btn secondary-btn" @click="goHome">返回首页</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="uts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
// import supa from '@/components/supadb/aksupainstance.uts' // 暂时注释掉数据库连接
|
||||||
|
|
||||||
|
const orderId = ref('')
|
||||||
|
const orderNo = ref('')
|
||||||
|
const amount = ref(0)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const pages = getCurrentPages()
|
||||||
|
const currentPage = pages[pages.length - 1]
|
||||||
|
const options = currentPage.options as any
|
||||||
|
|
||||||
|
if (options.orderId) {
|
||||||
|
orderId.value = options.orderId
|
||||||
|
orderNo.value = options.orderId // 使用订单ID作为订单号
|
||||||
|
amount.value = parseFloat(options.amount || 0)
|
||||||
|
// loadOrderInfo() // 暂时注释掉数据库查询
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// const loadOrderInfo = async () => {
|
||||||
|
// try {
|
||||||
|
// const { data, error } = await supa
|
||||||
|
// .from('orders')
|
||||||
|
// .select('order_no, actual_amount')
|
||||||
|
// .eq('id', orderId.value)
|
||||||
|
// .single()
|
||||||
|
//
|
||||||
|
// if (error == null && data != null) {
|
||||||
|
// orderNo.value = data['order_no'] as string
|
||||||
|
// amount.value = data['actual_amount'] as number
|
||||||
|
// }
|
||||||
|
// } catch (err) {
|
||||||
|
// console.error('加载订单信息失败', err)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
const viewOrder = () => {
|
||||||
|
// 跳转到订单列表或订单详情
|
||||||
|
// 这里跳转到订单列表页,并选中对应tab(如果有)
|
||||||
|
uni.redirectTo({
|
||||||
|
url: '/pages/mall/consumer/orders?status=shipping'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const goHome = () => {
|
||||||
|
uni.switchTab({
|
||||||
|
url: '/pages/mall/consumer/index'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.payment-success-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: #ffffff;
|
||||||
|
padding: 0 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-wrapper {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 40px;
|
||||||
|
background-color: #4cd964;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 4px 10px rgba(76, 217, 100, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-icon {
|
||||||
|
font-size: 40px;
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333333;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-desc {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #999999;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-info {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666666;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 45px;
|
||||||
|
border-radius: 22.5px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-btn {
|
||||||
|
background-color: #007aff;
|
||||||
|
color: #ffffff;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary-btn {
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #666666;
|
||||||
|
border: 1px solid #cccccc;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
|
|
||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import { ref, onMounted, watch } from 'vue'
|
import { ref, onMounted, watch } from 'vue'
|
||||||
import supa from '@/components/supadb/aksupainstance.uts'
|
// import { supabase as supa } from '@/components/supadb/aksupainstance.uts'
|
||||||
|
|
||||||
type PaymentMethodType = {
|
type PaymentMethodType = {
|
||||||
id: string
|
id: string
|
||||||
@@ -141,7 +141,7 @@ onMounted(() => {
|
|||||||
// 加载订单信息
|
// 加载订单信息
|
||||||
const loadOrderInfo = async () => {
|
const loadOrderInfo = async () => {
|
||||||
try {
|
try {
|
||||||
const { data, error } = await supa
|
/* const { data, error } = await supa
|
||||||
.from('orders')
|
.from('orders')
|
||||||
.select('order_no, actual_amount')
|
.select('order_no, actual_amount')
|
||||||
.eq('id', orderId.value)
|
.eq('id', orderId.value)
|
||||||
@@ -155,7 +155,11 @@ const loadOrderInfo = async () => {
|
|||||||
if (data) {
|
if (data) {
|
||||||
orderNo.value = data.order_no
|
orderNo.value = data.order_no
|
||||||
amount.value = data.actual_amount || amount.value
|
amount.value = data.actual_amount || amount.value
|
||||||
}
|
} */
|
||||||
|
|
||||||
|
// MOCK DATA
|
||||||
|
orderNo.value = 'ORD_MOCK_' + Date.now()
|
||||||
|
// Amount already set from options or default
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('加载订单信息异常:', err)
|
console.error('加载订单信息异常:', err)
|
||||||
}
|
}
|
||||||
@@ -202,7 +206,7 @@ const loadUserBalance = async () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 这里假设有用户钱包表
|
// 这里假设有用户钱包表
|
||||||
const { data, error } = await supa
|
/* const { data, error } = await supa
|
||||||
.from('user_wallets')
|
.from('user_wallets')
|
||||||
.select('balance')
|
.select('balance')
|
||||||
.eq('user_id', userId)
|
.eq('user_id', userId)
|
||||||
@@ -213,7 +217,10 @@ const loadUserBalance = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
userBalance.value = data?.balance || 0
|
userBalance.value = data?.balance || 0 */
|
||||||
|
|
||||||
|
// MOCK BALANCE
|
||||||
|
userBalance.value = 10000.00
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('加载用户余额异常:', err)
|
console.error('加载用户余额异常:', err)
|
||||||
}
|
}
|
||||||
@@ -302,7 +309,7 @@ const confirmPayment = async () => {
|
|||||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||||
|
|
||||||
// 更新订单状态
|
// 更新订单状态
|
||||||
const { error } = await supa
|
/* const { error } = await supa
|
||||||
.from('orders')
|
.from('orders')
|
||||||
.update({
|
.update({
|
||||||
status: 2, // 待发货
|
status: 2, // 待发货
|
||||||
@@ -319,7 +326,7 @@ const confirmPayment = async () => {
|
|||||||
// 余额支付需要扣减余额
|
// 余额支付需要扣减余额
|
||||||
if (selectedMethod.value === 'balance') {
|
if (selectedMethod.value === 'balance') {
|
||||||
await updateUserBalance(-amount.value)
|
await updateUserBalance(-amount.value)
|
||||||
}
|
} */
|
||||||
|
|
||||||
// 支付成功
|
// 支付成功
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
@@ -363,7 +370,7 @@ const updateUserBalance = async (change: number) => {
|
|||||||
if (!userId) return
|
if (!userId) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data: wallet, error: walletError } = await supa
|
/* const { data: wallet, error: walletError } = await supa
|
||||||
.from('user_wallets')
|
.from('user_wallets')
|
||||||
.select('balance')
|
.select('balance')
|
||||||
.eq('user_id', userId)
|
.eq('user_id', userId)
|
||||||
@@ -402,8 +409,7 @@ const updateUserBalance = async (change: number) => {
|
|||||||
console.error('记录余额变动失败:', recordError)
|
console.error('记录余额变动失败:', recordError)
|
||||||
}
|
}
|
||||||
|
|
||||||
userBalance.value = newBalance
|
userBalance.value = newBalance */
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('更新余额异常:', err)
|
console.error('更新余额异常:', err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -326,9 +326,27 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sku = this.productSkus.find(s => s.id === this.selectedSkuId)
|
||||||
|
|
||||||
|
const selectedItem = {
|
||||||
|
id: this.selectedSkuId,
|
||||||
|
product_id: this.product.id,
|
||||||
|
sku_id: this.selectedSkuId,
|
||||||
|
product_name: this.product.name,
|
||||||
|
product_image: (sku && sku.image_url) ? sku.image_url : this.product.images[0],
|
||||||
|
sku_specifications: sku ? sku.specifications : {},
|
||||||
|
price: sku ? sku.price : this.product.price,
|
||||||
|
quantity: this.quantity
|
||||||
|
}
|
||||||
|
|
||||||
// 跳转到订单确认页
|
// 跳转到订单确认页
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: `/pages/mall/consumer/order-confirm?productId=${this.product.id}&skuId=${this.selectedSkuId}&quantity=${this.quantity}`
|
url: '/pages/mall/consumer/checkout',
|
||||||
|
success: (res) => {
|
||||||
|
res.eventChannel.emit('acceptData', {
|
||||||
|
selectedItems: [selectedItem]
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -248,6 +248,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { UserType, OrderType } from '@/types/mall-types.uts'
|
import { UserType, OrderType } from '@/types/mall-types.uts'
|
||||||
|
// import { supabase as supa } from '@/components/supadb/aksupainstance.uts'
|
||||||
|
|
||||||
type UserStatsType = {
|
type UserStatsType = {
|
||||||
points: number
|
points: number
|
||||||
@@ -330,7 +331,10 @@ export default {
|
|||||||
onLoad() {
|
onLoad() {
|
||||||
this.initPage()
|
this.initPage()
|
||||||
this.loadUserProfile()
|
this.loadUserProfile()
|
||||||
this.loadMockOrders() // 加载Mock订单数据
|
this.loadOrders()
|
||||||
|
},
|
||||||
|
onShow() {
|
||||||
|
this.refreshData()
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
// 根据当前Tab筛选订单
|
// 根据当前Tab筛选订单
|
||||||
@@ -348,100 +352,47 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
// 加载Mock订单数据
|
// 加载订单数据
|
||||||
loadMockOrders() {
|
async loadOrders() {
|
||||||
// 创建Mock数据
|
const userStore = uni.getStorageSync('userInfo')
|
||||||
const mockData: Array<OrderType> = [
|
const userId = userStore?.id
|
||||||
// 待支付订单
|
if (!userId) return
|
||||||
{
|
|
||||||
id: 'order_001',
|
try {
|
||||||
order_no: 'ORD202401250001',
|
/* const { data, error } = await supa
|
||||||
user_id: 'user_001',
|
.from('orders')
|
||||||
merchant_id: 'merchant_001',
|
.select('*')
|
||||||
status: 1, // 待支付
|
.eq('user_id', userId)
|
||||||
total_amount: 299.00,
|
.order('created_at', { ascending: false })
|
||||||
discount_amount: 30.00,
|
|
||||||
delivery_fee: 0.00,
|
if (error != null) {
|
||||||
actual_amount: 269.00,
|
console.error('加载订单失败', error)
|
||||||
payment_method: 1,
|
return
|
||||||
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
|
if (data != null) {
|
||||||
this.recentOrders = mockData // 初始显示全部
|
this.allOrders = data as any[]
|
||||||
|
this.recentOrders = this.allOrders
|
||||||
|
|
||||||
// 更新角标统计
|
// 更新角标统计
|
||||||
this.orderCounts = {
|
this.orderCounts = {
|
||||||
total: mockData.length,
|
total: this.allOrders.length,
|
||||||
pending: mockData.filter((o: OrderType): boolean => o.status === 1).length,
|
pending: this.allOrders.filter((o: any) => o.status === 1).length,
|
||||||
shipped: mockData.filter((o: OrderType): boolean => o.status === 2 || o.status === 3).length,
|
shipped: this.allOrders.filter((o: any) => o.status === 2 || o.status === 3).length,
|
||||||
review: mockData.filter((o: OrderType): boolean => o.status === 4).length
|
review: this.allOrders.filter((o: any) => o.status === 4).length
|
||||||
|
}
|
||||||
|
} */
|
||||||
|
|
||||||
|
// MOCK ORDERS
|
||||||
|
this.allOrders = this.recentOrders
|
||||||
|
this.orderCounts = {
|
||||||
|
total: this.allOrders.length,
|
||||||
|
pending: this.allOrders.filter((o: any) => o.status === 1).length,
|
||||||
|
shipped: this.allOrders.filter((o: any) => o.status === 2 || o.status === 3).length,
|
||||||
|
review: this.allOrders.filter((o: any) => o.status === 4).length
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载订单异常', e)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -568,7 +519,7 @@ export default {
|
|||||||
refreshData() {
|
refreshData() {
|
||||||
// 刷新页面数据
|
// 刷新页面数据
|
||||||
this.loadUserProfile()
|
this.loadUserProfile()
|
||||||
this.loadMockOrders() // 加载Mock订单数据
|
this.loadOrders()
|
||||||
this.updateCouponCount() // 更新优惠券数量
|
this.updateCouponCount() // 更新优惠券数量
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
163
pages/mall/consumer/refund-review.uvue
Normal file
163
pages/mall/consumer/refund-review.uvue
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
<template>
|
||||||
|
<view class="review-page">
|
||||||
|
<view class="header">
|
||||||
|
<text class="title">服务评价</text>
|
||||||
|
<text class="subtitle">请对本次售后服务进行评价</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="rating-section">
|
||||||
|
<text class="label">服务评分</text>
|
||||||
|
<view class="stars">
|
||||||
|
<text
|
||||||
|
v-for="i in 5"
|
||||||
|
:key="i"
|
||||||
|
class="star"
|
||||||
|
:class="{ active: i <= rating }"
|
||||||
|
@click="setRating(i)"
|
||||||
|
>★</text>
|
||||||
|
</view>
|
||||||
|
<text class="rating-text">{{ ratingText }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="comment-section">
|
||||||
|
<textarea
|
||||||
|
v-model="comment"
|
||||||
|
class="comment-input"
|
||||||
|
placeholder="请输入您的评价内容,您的建议是我们改进的动力"
|
||||||
|
maxlength="200"
|
||||||
|
/>
|
||||||
|
<text class="word-count">{{ comment.length }}/200</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<button class="submit-btn" @click="submitReview" :loading="submitting">提交评价</button>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="uts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
|
const rating = ref(5)
|
||||||
|
const comment = ref('')
|
||||||
|
const submitting = ref(false)
|
||||||
|
|
||||||
|
const ratingText = computed(() => {
|
||||||
|
const texts = ['非常不满意', '不满意', '一般', '满意', '非常满意']
|
||||||
|
return texts[rating.value - 1]
|
||||||
|
})
|
||||||
|
|
||||||
|
const setRating = (val: number) => {
|
||||||
|
rating.value = val
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitReview = () => {
|
||||||
|
if (submitting.value) return
|
||||||
|
submitting.value = true
|
||||||
|
|
||||||
|
// 模拟提交
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.showToast({
|
||||||
|
title: '评价成功',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.navigateBack()
|
||||||
|
}, 1500)
|
||||||
|
|
||||||
|
submitting.value = false
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.review-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #ffffff;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rating-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stars {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star {
|
||||||
|
font-size: 32px;
|
||||||
|
color: #ddd;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star.active {
|
||||||
|
color: #ffca28;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rating-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-section {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 120px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 15px;
|
||||||
|
font-size: 14px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-count {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 10px;
|
||||||
|
right: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
background-color: #007aff;
|
||||||
|
color: #ffffff;
|
||||||
|
border-radius: 25px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
height: 50px;
|
||||||
|
line-height: 50px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -361,7 +361,7 @@ const submitReview = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 提交商品评价
|
// 提交商品评价
|
||||||
const productReviews = orderItems.value.map((item, index) => ({
|
/* const productReviews = orderItems.value.map((item, index) => ({
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
product_id: item.product_id,
|
product_id: item.product_id,
|
||||||
order_id: orderId.value,
|
order_id: orderId.value,
|
||||||
@@ -416,7 +416,10 @@ const submitReview = async () => {
|
|||||||
|
|
||||||
if (orderError !== null) {
|
if (orderError !== null) {
|
||||||
console.error('更新订单状态失败:', orderError)
|
console.error('更新订单状态失败:', orderError)
|
||||||
}
|
} */
|
||||||
|
|
||||||
|
// MOCK SUBMIT
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||||
|
|
||||||
// 显示成功提示
|
// 显示成功提示
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import supa, { supaReady } from '@/components/supadb/aksupainstance.uts'
|
import { supabase, ensureSupabaseReady } from '@/components/supadb/aksupainstance.uts'
|
||||||
import type { DeviceInfo, DeviceParams } from './types.uts'
|
import type { DeviceInfo, DeviceParams } from './types.uts'
|
||||||
|
|
||||||
// 服务响应类型
|
// 服务响应类型
|
||||||
@@ -17,8 +17,8 @@ export class SenseDataService {
|
|||||||
*/
|
*/
|
||||||
static async getDevices(params: DeviceParams): Promise<ServiceResponse<Array<DeviceInfo>>> {
|
static async getDevices(params: DeviceParams): Promise<ServiceResponse<Array<DeviceInfo>>> {
|
||||||
try {
|
try {
|
||||||
await supaReady
|
await ensureSupabaseReady()
|
||||||
const res = await supa.from(SenseDataService.TABLE_NAME)
|
const res = await supabase.from(SenseDataService.TABLE_NAME)
|
||||||
.select('*', {})
|
.select('*', {})
|
||||||
.eq('user_id', params.user_id)
|
.eq('user_id', params.user_id)
|
||||||
.execute()
|
.execute()
|
||||||
@@ -46,8 +46,8 @@ export class SenseDataService {
|
|||||||
*/
|
*/
|
||||||
static async bindDevice(deviceData: UTSJSONObject): Promise<ServiceResponse<DeviceInfo>> {
|
static async bindDevice(deviceData: UTSJSONObject): Promise<ServiceResponse<DeviceInfo>> {
|
||||||
try {
|
try {
|
||||||
await supaReady
|
await ensureSupabaseReady()
|
||||||
const res = await supa.from(SenseDataService.TABLE_NAME)
|
const res = await supabase.from(SenseDataService.TABLE_NAME)
|
||||||
.insert(deviceData)
|
.insert(deviceData)
|
||||||
.select('*', {})
|
.select('*', {})
|
||||||
.single()
|
.single()
|
||||||
@@ -75,8 +75,8 @@ export class SenseDataService {
|
|||||||
*/
|
*/
|
||||||
static async unbindDevice(deviceId: string): Promise<ServiceResponse<null>> {
|
static async unbindDevice(deviceId: string): Promise<ServiceResponse<null>> {
|
||||||
try {
|
try {
|
||||||
await supaReady
|
await ensureSupabaseReady()
|
||||||
const res = await supa.from(SenseDataService.TABLE_NAME)
|
const res = await supabase.from(SenseDataService.TABLE_NAME)
|
||||||
.delete()
|
.delete()
|
||||||
.eq('id', deviceId)
|
.eq('id', deviceId)
|
||||||
.execute()
|
.execute()
|
||||||
@@ -102,8 +102,8 @@ export class SenseDataService {
|
|||||||
*/
|
*/
|
||||||
static async updateDevice(deviceId: string, configData: UTSJSONObject): Promise<ServiceResponse<DeviceInfo>> {
|
static async updateDevice(deviceId: string, configData: UTSJSONObject): Promise<ServiceResponse<DeviceInfo>> {
|
||||||
try {
|
try {
|
||||||
await supaReady
|
await ensureSupabaseReady()
|
||||||
const res = await supa.from(SenseDataService.TABLE_NAME)
|
const res = await supabase.from(SenseDataService.TABLE_NAME)
|
||||||
.update(configData)
|
.update(configData)
|
||||||
.eq('id', deviceId)
|
.eq('id', deviceId)
|
||||||
.select('*', {})
|
.select('*', {})
|
||||||
|
|||||||
@@ -85,8 +85,9 @@
|
|||||||
<script lang="uts">
|
<script lang="uts">
|
||||||
import {HOME_REDIRECT,TABORPAGE} from '@/ak/config.uts'
|
import {HOME_REDIRECT,TABORPAGE} from '@/ak/config.uts'
|
||||||
import type { AkReqOptions, AkReqResponse, AkReqError } from '@/uni_modules/ak-req/index.uts';
|
import type { AkReqOptions, AkReqResponse, AkReqError } from '@/uni_modules/ak-req/index.uts';
|
||||||
import supa from '@/components/supadb/aksupainstance.uts';
|
import { supabase as supa } from '@/components/supadb/aksupainstance.uts';
|
||||||
import { getCurrentUser, logout } from '@/utils/store.uts';
|
import { getCurrentUser, logout, setUserProfile, setIsLoggedIn } from '@/utils/store.uts';
|
||||||
|
import type { UserProfile } from '@/types/mall-types.uts';
|
||||||
import { switchLocale, getCurrentLocale } from '@/utils/utils.uts';
|
import { switchLocale, getCurrentLocale } from '@/utils/utils.uts';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -190,7 +191,7 @@
|
|||||||
} else {
|
} else {
|
||||||
uni.removeStorageSync('rememberEmail');
|
uni.removeStorageSync('rememberEmail');
|
||||||
} // Call signin method from Supabase
|
} // Call signin method from Supabase
|
||||||
const result = await supa.signIn(
|
/* const result = await supa.signIn(
|
||||||
this.email,
|
this.email,
|
||||||
this.password);
|
this.password);
|
||||||
if (result.user !== null) {
|
if (result.user !== null) {
|
||||||
@@ -204,7 +205,31 @@
|
|||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(this.$t('user.login.login_failed'));
|
throw new Error(this.$t('user.login.login_failed'));
|
||||||
}
|
} */
|
||||||
|
|
||||||
|
// MOCK LOGIN START
|
||||||
|
const mockProfile = {
|
||||||
|
id: 'mock-user-id-123',
|
||||||
|
username: 'Mock User',
|
||||||
|
email: this.email,
|
||||||
|
role: 'consumer',
|
||||||
|
avatar_url: '/static/avatar/default.png',
|
||||||
|
bio: 'This is a mock user.',
|
||||||
|
preferred_language: 'zh-CN',
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString()
|
||||||
|
} as UserProfile;
|
||||||
|
|
||||||
|
setUserProfile(mockProfile);
|
||||||
|
setIsLoggedIn(true);
|
||||||
|
|
||||||
|
// 模拟延迟
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
|
||||||
|
uni.showToast({ title: this.$t('user.login.login_success'), icon: 'success' });
|
||||||
|
uni.switchTab({ url: '/pages/mall/consumer/index' });
|
||||||
|
// MOCK LOGIN END
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Login error:", err);
|
console.error("Login error:", err);
|
||||||
|
|
||||||
|
|||||||
@@ -433,3 +433,29 @@ export interface MockData {
|
|||||||
coupons: CouponTemplateType[]
|
coupons: CouponTemplateType[]
|
||||||
products: ProductType[]
|
products: ProductType[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 用户基础信息类型 (兼容 pages/user/types.uts)
|
||||||
|
export type UserProfile = {
|
||||||
|
id?: string;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
gender?: string;
|
||||||
|
birthday?: string;
|
||||||
|
height_cm?: number;
|
||||||
|
weight_kg?: number;
|
||||||
|
bio?: string;
|
||||||
|
avatar_url?: string;
|
||||||
|
preferred_language?: string;
|
||||||
|
role?: string;
|
||||||
|
school_id?: string;
|
||||||
|
grade_id?: string;
|
||||||
|
class_id?: string;
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserStats = {
|
||||||
|
trainings: number;
|
||||||
|
points: number;
|
||||||
|
streak: number;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import supa, { supaReady } from '@/components/supadb/aksupainstance.uts'
|
import { supabase, ensureSupabaseReady } from '@/components/supadb/aksupainstance.uts'
|
||||||
import type { UserProfile } from '@/pages/user/types.uts'
|
import type { UserProfile } from '@/types/mall-types.uts'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 确保用户资料存在,如果不存在则创建基础资料
|
* 确保用户资料存在,如果不存在则创建基础资料
|
||||||
@@ -8,7 +8,7 @@ import type { UserProfile } from '@/pages/user/types.uts'
|
|||||||
*/
|
*/
|
||||||
export async function ensureUserProfile(sessionUser: UTSJSONObject): Promise<UserProfile | null> {
|
export async function ensureUserProfile(sessionUser: UTSJSONObject): Promise<UserProfile | null> {
|
||||||
try {
|
try {
|
||||||
await supaReady
|
await ensureSupabaseReady()
|
||||||
|
|
||||||
// 从 sessionUser 中获取用户ID和邮箱
|
// 从 sessionUser 中获取用户ID和邮箱
|
||||||
const userId = sessionUser.getString('id')
|
const userId = sessionUser.getString('id')
|
||||||
@@ -20,7 +20,7 @@ export async function ensureUserProfile(sessionUser: UTSJSONObject): Promise<Use
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 检查用户是否已存在
|
// 检查用户是否已存在
|
||||||
const checkRes = await supa.from('ak_users')
|
const checkRes = await supabase.from('ak_users')
|
||||||
.select('*', {})
|
.select('*', {})
|
||||||
.eq('id', userId)
|
.eq('id', userId)
|
||||||
.single()
|
.single()
|
||||||
@@ -30,7 +30,7 @@ export async function ensureUserProfile(sessionUser: UTSJSONObject): Promise<Use
|
|||||||
// 用户已存在,返回现有资料
|
// 用户已存在,返回现有资料
|
||||||
const existingUser = checkRes.data as UTSJSONObject
|
const existingUser = checkRes.data as UTSJSONObject
|
||||||
return {
|
return {
|
||||||
id: existingUser.getString('id'),
|
id: existingUser.getString('id') ?? '',
|
||||||
username: existingUser.getString('username') ?? '',
|
username: existingUser.getString('username') ?? '',
|
||||||
email: existingUser.getString('email') ?? email,
|
email: existingUser.getString('email') ?? email,
|
||||||
gender: existingUser.getString('gender'),
|
gender: existingUser.getString('gender'),
|
||||||
@@ -40,10 +40,9 @@ export async function ensureUserProfile(sessionUser: UTSJSONObject): Promise<Use
|
|||||||
bio: existingUser.getString('bio'),
|
bio: existingUser.getString('bio'),
|
||||||
avatar_url: existingUser.getString('avatar_url'),
|
avatar_url: existingUser.getString('avatar_url'),
|
||||||
preferred_language: existingUser.getString('preferred_language'),
|
preferred_language: existingUser.getString('preferred_language'),
|
||||||
role: existingUser.getString('role'),
|
role: existingUser.getString('role') ?? 'consumer',
|
||||||
school_id: existingUser.getString('school_id'),
|
created_at: existingUser.getString('created_at'),
|
||||||
grade_id: existingUser.getString('grade_id'),
|
updated_at: existingUser.getString('updated_at')
|
||||||
class_id: existingUser.getString('class_id')
|
|
||||||
} as UserProfile
|
} as UserProfile
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +52,7 @@ export async function ensureUserProfile(sessionUser: UTSJSONObject): Promise<Use
|
|||||||
newUserData.set('email', email)
|
newUserData.set('email', email)
|
||||||
newUserData.set('username', email.split('@')[0] ?? 'user') // 默认用户名为邮箱前缀
|
newUserData.set('username', email.split('@')[0] ?? 'user') // 默认用户名为邮箱前缀
|
||||||
|
|
||||||
const insertRes = await supa.from('ak_users')
|
const insertRes = await supabase.from('ak_users')
|
||||||
.insert(newUserData)
|
.insert(newUserData)
|
||||||
.select('*', {})
|
.select('*', {})
|
||||||
.single()
|
.single()
|
||||||
@@ -62,7 +61,7 @@ export async function ensureUserProfile(sessionUser: UTSJSONObject): Promise<Use
|
|||||||
if (insertRes.status >= 200 && insertRes.status < 300 && insertRes.data != null) {
|
if (insertRes.status >= 200 && insertRes.status < 300 && insertRes.data != null) {
|
||||||
const newUser = insertRes.data as UTSJSONObject
|
const newUser = insertRes.data as UTSJSONObject
|
||||||
return {
|
return {
|
||||||
id: newUser.getString('id'),
|
id: newUser.getString('id') ?? '',
|
||||||
username: newUser.getString('username') ?? '',
|
username: newUser.getString('username') ?? '',
|
||||||
email: newUser.getString('email') ?? email,
|
email: newUser.getString('email') ?? email,
|
||||||
gender: newUser.getString('gender'),
|
gender: newUser.getString('gender'),
|
||||||
@@ -72,10 +71,9 @@ export async function ensureUserProfile(sessionUser: UTSJSONObject): Promise<Use
|
|||||||
bio: newUser.getString('bio'),
|
bio: newUser.getString('bio'),
|
||||||
avatar_url: newUser.getString('avatar_url'),
|
avatar_url: newUser.getString('avatar_url'),
|
||||||
preferred_language: newUser.getString('preferred_language'),
|
preferred_language: newUser.getString('preferred_language'),
|
||||||
role: newUser.getString('role'),
|
role: newUser.getString('role') ?? 'consumer',
|
||||||
school_id: newUser.getString('school_id'),
|
created_at: newUser.getString('created_at'),
|
||||||
grade_id: newUser.getString('grade_id'),
|
updated_at: newUser.getString('updated_at')
|
||||||
class_id: newUser.getString('class_id')
|
|
||||||
} as UserProfile
|
} as UserProfile
|
||||||
} else {
|
} else {
|
||||||
console.error('创建用户资料失败:', insertRes.status)
|
console.error('创建用户资料失败:', insertRes.status)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import supa, { supaReady } from '@/components/supadb/aksupainstance.uts'
|
import { supabase as supa, ensureSupabaseReady } from '@/components/supadb/aksupainstance.uts'
|
||||||
import type { UserProfile, UserStats } from '@/pages/user/types.uts'
|
import type { UserProfile, UserStats } from '@/pages/user/types.uts'
|
||||||
import type { DeviceInfo } from '@/pages/sense/types.uts'
|
import type { DeviceInfo } from '@/pages/sense/types.uts'
|
||||||
import { SenseDataService, type DeviceParams } from '@/pages/sense/senseDataService.uts'
|
import { SenseDataService, type DeviceParams } from '@/pages/sense/senseDataService.uts'
|
||||||
@@ -50,7 +50,7 @@ export const setUserProfile = (profile : UserProfile) => {
|
|||||||
// 获取当前用户信息(含补全 profile)
|
// 获取当前用户信息(含补全 profile)
|
||||||
export async function getCurrentUser() : Promise<UserProfile | null> {
|
export async function getCurrentUser() : Promise<UserProfile | null> {
|
||||||
try {
|
try {
|
||||||
await supaReady
|
await ensureSupabaseReady()
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
const sessionInfo = supa.getSession()
|
const sessionInfo = supa.getSession()
|
||||||
|
|||||||
Reference in New Issue
Block a user