consumer模块完成度95%,优化安卓端界面和小程序测试3
This commit is contained in:
60
doc_mall/consumer/backup_pages/COPY_IMAGES.md
Normal file
60
doc_mall/consumer/backup_pages/COPY_IMAGES.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# 图片资源复制说明
|
||||
|
||||
## 需要复制的图片文件
|
||||
|
||||
请从 `CRMEB/template/uni-app/pages/users/static/` 目录复制以下文件到 `static/user/` 目录:
|
||||
|
||||
### 必需文件
|
||||
|
||||
1. **phone_1.png** - 手机号输入框图标
|
||||
- 尺寸:24rpx × 34rpx
|
||||
- 用途:手机号输入框左侧图标
|
||||
|
||||
2. **code_1.png** - 密码输入框图标
|
||||
- 尺寸:28rpx × 32rpx
|
||||
- 用途:密码输入框左侧图标
|
||||
|
||||
3. **code_2.png** - 验证码输入框图标
|
||||
- 尺寸:28rpx × 32rpx
|
||||
- 用途:验证码输入框左侧图标
|
||||
|
||||
### 可选文件
|
||||
|
||||
4. **logo2.png** - Logo 图片(可选)
|
||||
- 用途:注册/找回密码页面 Logo
|
||||
- 如果不存在,将使用 `/static/logo.png`
|
||||
|
||||
## 复制方法
|
||||
|
||||
### Windows PowerShell
|
||||
```powershell
|
||||
# 在项目根目录执行
|
||||
Copy-Item "CRMEB\template\uni-app\pages\users\static\phone_1.png" -Destination "static\user\"
|
||||
Copy-Item "CRMEB\template\uni-app\pages\users\static\code_1.png" -Destination "static\user\"
|
||||
Copy-Item "CRMEB\template\uni-app\pages\users\static\code_2.png" -Destination "static\user\"
|
||||
Copy-Item "CRMEB\template\uni-app\pages\users\static\logo2.png" -Destination "static\user\" -ErrorAction SilentlyContinue
|
||||
```
|
||||
|
||||
### 手动复制
|
||||
1. 打开 `CRMEB/template/uni-app/pages/users/static/` 目录
|
||||
2. 复制上述文件到 `static/user/` 目录
|
||||
3. 确保文件路径正确
|
||||
|
||||
## 文件结构
|
||||
|
||||
复制后的目录结构应该是:
|
||||
```
|
||||
static/
|
||||
└── user/
|
||||
├── phone_1.png
|
||||
├── code_1.png
|
||||
├── code_2.png
|
||||
├── logo2.png (可选)
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 如果图片文件不存在,页面会显示占位符或空白
|
||||
- 建议使用原 CRMEB 项目的图片资源以保持设计一致性
|
||||
- 图片路径在代码中使用:`/static/user/phone_1.png`
|
||||
20
doc_mall/consumer/backup_pages/README.md
Normal file
20
doc_mall/consumer/backup_pages/README.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# 用户相关图片资源
|
||||
|
||||
## 说明
|
||||
这些图片资源来自 CRMEB 项目,用于登录注册页面。
|
||||
|
||||
## 需要复制的图片文件
|
||||
|
||||
请从 `CRMEB/template/uni-app/pages/users/static/` 目录复制以下文件到此目录:
|
||||
|
||||
1. `phone_1.png` - 手机号输入框图标(24rpx × 34rpx)
|
||||
2. `code_1.png` - 密码输入框图标(28rpx × 32rpx)
|
||||
3. `code_2.png` - 验证码输入框图标(28rpx × 32rpx)
|
||||
4. `logo2.png` - Logo 图片(可选,用于注册/找回密码页面)
|
||||
|
||||
## 使用方式
|
||||
|
||||
在页面中使用:
|
||||
```vue
|
||||
<image src="/static/user/phone_1.png" />
|
||||
```
|
||||
463
doc_mall/consumer/backup_pages/address-edit_gbk.txt
Normal file
463
doc_mall/consumer/backup_pages/address-edit_gbk.txt
Normal file
@@ -0,0 +1,463 @@
|
||||
<template>
|
||||
<view class="page-container">
|
||||
<scroll-view class="address-edit-page" direction="vertical">
|
||||
<view class="form-group">
|
||||
<view class="form-item">
|
||||
<text class="label">鏀惰揣浜?/text>
|
||||
<input class="input" v-model="formData.name" placeholder="璇峰~鍐欐敹璐т汉濮撳悕" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">鎵嬫満鍙风爜</text>
|
||||
<input class="input" v-model="formData.phone" type="number" maxlength="11" placeholder="璇峰~鍐欐墜鏈哄彿鐮? />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">鎵?鍦ㄥ湴鍖?/text>
|
||||
<input class="input" v-model="regionString" placeholder="鐪佸競鍖哄幙銆佷埂闀囩瓑" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">璇︾粏鍦板潃</text>
|
||||
<input class="input" v-model="formData.detail" placeholder="琛楅亾銆佹ゼ鐗屽彿绛? />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<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 鍖椾含甯傛湞闃冲尯涓夐噷灞疭OHO A搴?/text>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">鏍囩</text>
|
||||
<view class="tags-container">
|
||||
<text
|
||||
v-for="tag in tags"
|
||||
:key="tag"
|
||||
class="tag-item"
|
||||
:class="{ active: formData.label === tag }"
|
||||
@click="selectTag(tag)"
|
||||
>{{ tag }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-item switch-item">
|
||||
<text class="label">璁句负榛樿鏀惰揣鍦板潃</text>
|
||||
<switch :checked="formData.isDefault" color="#ff5000" @change="onSwitchChange" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="footer-btn">
|
||||
<button class="save-btn" @click="saveAddress">淇濆瓨</button>
|
||||
<button v-if="isEdit" class="delete-btn" @click="deleteAddress">鍒犻櫎鏀惰揣鍦板潃</button>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { supabaseService, AddAddressParams, UpdateAddressParams } from '@/utils/supabaseService.uts'
|
||||
|
||||
type Address = {
|
||||
id: string
|
||||
name: string
|
||||
phone: string
|
||||
province: string
|
||||
city: string
|
||||
district: string
|
||||
detail: string
|
||||
isDefault: boolean
|
||||
label?: string
|
||||
}
|
||||
|
||||
const isEdit = ref(false)
|
||||
const addressId = ref('')
|
||||
const regionString = ref('')
|
||||
const tags = ['瀹?, '鍏徃', '瀛︽牎']
|
||||
const smartInput = ref('')
|
||||
|
||||
type AddressForm = {
|
||||
name: string
|
||||
phone: string
|
||||
detail: string
|
||||
isDefault: boolean
|
||||
label: string
|
||||
}
|
||||
|
||||
const formData = reactive({
|
||||
name: '',
|
||||
phone: '',
|
||||
detail: '',
|
||||
isDefault: false,
|
||||
label: ''
|
||||
} as AddressForm)
|
||||
|
||||
const loadAddress = async (id: string) => {
|
||||
try {
|
||||
// 浠嶴upabase鍔犺浇鍦板潃璇︽儏
|
||||
const address = await supabaseService.getAddressById(id)
|
||||
if (address != null) {
|
||||
formData.name = address.recipient_name
|
||||
formData.phone = address.phone
|
||||
formData.detail = address.detail_address
|
||||
formData.isDefault = address.is_default
|
||||
formData.label = address.label ?? ''
|
||||
regionString.value = `${address.province} ${address.city} ${address.district}`.trim()
|
||||
} else {
|
||||
// 濡傛灉Supabase娌℃湁鎵惧埌锛屽皾璇曚粠鏈湴瀛樺偍鍔犺浇
|
||||
const storedAddresses = uni.getStorageSync('addresses')
|
||||
if (storedAddresses != null) {
|
||||
const addresses = JSON.parse(storedAddresses as string) as Address[]
|
||||
const localAddress = addresses.find(item => item.id === id)
|
||||
if (localAddress != null) {
|
||||
formData.name = localAddress.name
|
||||
formData.phone = localAddress.phone
|
||||
formData.detail = localAddress.detail
|
||||
formData.isDefault = localAddress.isDefault
|
||||
formData.label = localAddress.label ?? ''
|
||||
regionString.value = `${localAddress.province} ${localAddress.city} ${localAddress.district}`.trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('鍔犺浇鍦板潃璇︽儏澶辫触:', error)
|
||||
// 澶辫触鏃朵粠鏈湴瀛樺偍鍔犺浇
|
||||
const storedAddresses = uni.getStorageSync('addresses')
|
||||
if (storedAddresses != null) {
|
||||
try {
|
||||
const addresses = JSON.parse(storedAddresses as string) as Address[]
|
||||
const address = addresses.find(item => item.id === id)
|
||||
if (address != null) {
|
||||
formData.name = address.name
|
||||
formData.phone = address.phone
|
||||
formData.detail = address.detail
|
||||
formData.isDefault = address.isDefault
|
||||
formData.label = address.label ?? ''
|
||||
regionString.value = `${address.province} ${address.city} ${address.district}`.trim()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('瑙f瀽鏈湴鍦板潃鏁版嵁澶辫触', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onLoad((options) => {
|
||||
if (options['id'] != null) {
|
||||
isEdit.value = true
|
||||
addressId.value = options['id'] as string
|
||||
loadAddress(addressId.value)
|
||||
}
|
||||
})
|
||||
|
||||
const selectTag = (tag: string) => {
|
||||
if (formData.label === tag) {
|
||||
formData.label = ''
|
||||
} else {
|
||||
formData.label = tag
|
||||
}
|
||||
}
|
||||
|
||||
const onSwitchChange = (e: UniSwitchChangeEvent) => {
|
||||
formData.isDefault = e.detail.value
|
||||
}
|
||||
|
||||
const saveAddress = async () => {
|
||||
if (formData.name == '') {
|
||||
uni.showToast({ title: '璇峰~鍐欐敹璐т汉', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (formData.phone == '') {
|
||||
uni.showToast({ title: '璇峰~鍐欐墜鏈哄彿鐮?, icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (regionString.value == '') {
|
||||
uni.showToast({ title: '璇峰~鍐欐墍鍦ㄥ湴鍖?, icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (formData.detail == '') {
|
||||
uni.showToast({ title: '璇峰~鍐欒缁嗗湴鍧?', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
// 绠?鍗曡В鏋愬湴鍖猴紙杩欓噷绠?鍖栧鐞嗭紝瀹為檯搴斾娇鐢ㄩ?夋嫨鍣級
|
||||
const regions = regionString.value.split(' ')
|
||||
const province = regions[0] ?? ''
|
||||
const city = regions[1] ?? ''
|
||||
const district = regions.slice(2).join(' ')
|
||||
|
||||
// 鏋勫缓鍦板潃瀵硅薄
|
||||
const addressData = {
|
||||
recipient_name: formData.name,
|
||||
phone: formData.phone,
|
||||
province: province,
|
||||
city: city,
|
||||
district: district,
|
||||
detail_address: formData.detail,
|
||||
postal_code: '', // 濡傛灉闇?瑕佸彲浠ユ坊鍔犻偖鏀跨紪鐮佸瓧娈?
|
||||
is_default: formData.isDefault,
|
||||
label: formData.label
|
||||
} as AddAddressParams
|
||||
|
||||
let success = false
|
||||
|
||||
if (isEdit.value) {
|
||||
// 鏇存柊鍦板潃
|
||||
const updateData = {
|
||||
recipient_name: formData.name,
|
||||
phone: formData.phone,
|
||||
province: province,
|
||||
city: city,
|
||||
district: district,
|
||||
detail_address: formData.detail,
|
||||
postal_code: '',
|
||||
is_default: formData.isDefault,
|
||||
label: formData.label
|
||||
} as UpdateAddressParams
|
||||
success = await supabaseService.updateAddress(addressId.value, updateData)
|
||||
} else {
|
||||
// 娣诲姞鏂板湴鍧?
|
||||
success = await supabaseService.addAddress(addressData)
|
||||
}
|
||||
|
||||
if (success) {
|
||||
// 鍚屾椂鏇存柊鏈湴瀛樺偍浣滀负缂撳瓨
|
||||
const storedAddresses = uni.getStorageSync('addresses')
|
||||
let addresses: Address[] = []
|
||||
if (storedAddresses != null) {
|
||||
try {
|
||||
addresses = JSON.parse(storedAddresses as string) as Address[]
|
||||
} catch (e) {
|
||||
addresses = []
|
||||
}
|
||||
}
|
||||
|
||||
// 濡傛灉璁句负榛樿锛屽彇娑堝叾浠栭粯璁?
|
||||
if (formData.isDefault) {
|
||||
addresses.forEach(item => {
|
||||
item.isDefault = false
|
||||
})
|
||||
}
|
||||
|
||||
if (isEdit.value) {
|
||||
const index = addresses.findIndex(item => item.id === addressId.value)
|
||||
if (index !== -1) {
|
||||
addresses[index] = {
|
||||
...addresses[index],
|
||||
name: formData.name,
|
||||
phone: formData.phone,
|
||||
province: province,
|
||||
city: city,
|
||||
district: district,
|
||||
detail: formData.detail,
|
||||
isDefault: formData.isDefault,
|
||||
label: formData.label
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const newAddress: Address = {
|
||||
id: `addr_${Date.now()}`, // 涓存椂ID锛屽疄闄呯敱Supabase鐢熸垚
|
||||
name: formData.name,
|
||||
phone: formData.phone,
|
||||
province: province,
|
||||
city: city,
|
||||
district: district,
|
||||
detail: formData.detail,
|
||||
isDefault: formData.isDefault,
|
||||
label: formData.label
|
||||
}
|
||||
addresses.push(newAddress)
|
||||
}
|
||||
|
||||
uni.setStorageSync('addresses', JSON.stringify(addresses))
|
||||
|
||||
uni.showToast({
|
||||
title: '淇濆瓨鎴愬姛',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
} else {
|
||||
console.error('淇濆瓨鍦板潃澶辫触')
|
||||
uni.showToast({
|
||||
title: '淇濆瓨澶辫触',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const parseSmartInput = () => {
|
||||
const input = smartInput.value.trim()
|
||||
if (input == '') return
|
||||
|
||||
// 鎻愬彇鎵嬫満鍙?
|
||||
const phoneRegex = /(1[3-9]\d{9})/
|
||||
const phoneMatch = input.match(phoneRegex)
|
||||
if (phoneMatch != null) {
|
||||
formData.phone = phoneMatch[0] ?? ''
|
||||
}
|
||||
|
||||
// 鎻愬彇濮撳悕锛堝彇绗竴涓?-4浣嶄腑鏂囷級
|
||||
const nameRegex = /([\u4e00-\u9fa5]{2,4})/
|
||||
const nameMatch = input.match(nameRegex)
|
||||
if (nameMatch != null) {
|
||||
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()
|
||||
|
||||
// 瑙f瀽鐪佸競鍖?
|
||||
const pattern1 = /^(.*?鐪??(.*?甯??(.*?[鍖哄幙])?(.*)$/
|
||||
const m = addrText.match(pattern1)
|
||||
if (m != null) {
|
||||
const province = m[1] ?? ''
|
||||
const city = m[2] ?? ''
|
||||
const district = m[3] ?? ''
|
||||
const detail = m[4] ?? ''
|
||||
regionString.value = `${province.trim()} ${city.trim()} ${district.trim()}`.trim()
|
||||
formData.detail = detail.trim()
|
||||
} else {
|
||||
formData.detail = addrText
|
||||
}
|
||||
}
|
||||
const deleteAddress = () => {
|
||||
uni.showModal({
|
||||
title: '鎻愮ず',
|
||||
content: '纭畾瑕佸垹闄よ鍦板潃鍚楋紵',
|
||||
success: (res: UniShowModalResult) => {
|
||||
if (res.confirm) {
|
||||
// 璋冪敤Supabase鏈嶅姟鍒犻櫎鍦板潃
|
||||
supabaseService.deleteAddress(addressId.value).then((success) => {
|
||||
if (success) {
|
||||
// 鍚屾椂浠庢湰鍦板瓨鍌ㄤ腑绉婚櫎
|
||||
const storedAddresses = uni.getStorageSync('addresses')
|
||||
if (storedAddresses != null) {
|
||||
try {
|
||||
let addresses = JSON.parse(storedAddresses as string) as Address[]
|
||||
addresses = addresses.filter(item => item.id !== addressId.value)
|
||||
uni.setStorageSync('addresses', JSON.stringify(addresses))
|
||||
} catch (e) {
|
||||
console.error('瑙f瀽鏈湴鍦板潃鏁版嵁澶辫触', e)
|
||||
}
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: '鍒犻櫎鎴愬姛',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
} else {
|
||||
console.error('鍒犻櫎鍦板潃澶辫触')
|
||||
uni.showToast({
|
||||
title: '鍒犻櫎澶辫触',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.page-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.address-edit-page {
|
||||
flex: 1; /* Replace min-height: 100vh */
|
||||
}
|
||||
|
||||
.form-group {
|
||||
background-color: white;
|
||||
margin-bottom: 15px;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
.form-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.label {
|
||||
width: 80px;
|
||||
font-size: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.input {
|
||||
flex: 1;
|
||||
font-size: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.switch-item {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.tags-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tag-item {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
border: 1px solid #ddd;
|
||||
padding: 4px 12px;
|
||||
border-radius: 15px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.tag-item.active {
|
||||
background-color: #ff5000;
|
||||
color: white;
|
||||
border-color: #ff5000;
|
||||
}
|
||||
|
||||
.footer-btn {
|
||||
margin-top: 30px;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
background-color: #ff5000;
|
||||
color: white;
|
||||
border-radius: 25px;
|
||||
font-size: 16px;
|
||||
height: 44px;
|
||||
line-height: 44px;
|
||||
border: none;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
background-color: white;
|
||||
color: #333;
|
||||
border-radius: 25px;
|
||||
font-size: 16px;
|
||||
height: 44px;
|
||||
line-height: 44px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
221
doc_mall/consumer/backup_pages/address-list copy.uvue
Normal file
221
doc_mall/consumer/backup_pages/address-list copy.uvue
Normal file
@@ -0,0 +1,221 @@
|
||||
<template>
|
||||
<view class="address-list-page">
|
||||
<view class="address-list">
|
||||
<view v-if="addresses.length === 0" class="empty-state">
|
||||
<text class="empty-icon">馃搷</text>
|
||||
<text class="empty-text">鏆傛棤鏀惰揣鍦板潃</text>
|
||||
</view>
|
||||
|
||||
<view v-else v-for="(item, index) in addresses" :key="item.id" class="address-item" @click="selectAddress(item)">
|
||||
<view class="item-content">
|
||||
<view class="item-header">
|
||||
<text class="user-name">{{ item.name }}</text>
|
||||
<text class="user-phone">{{ item.phone }}</text>
|
||||
<text v-if="item.isDefault" class="default-tag">榛樿</text>
|
||||
<text v-if="item.label" class="label-tag">{{ item.label }}</text>
|
||||
</view>
|
||||
<text class="address-text">{{ getFullAddress(item) }}</text>
|
||||
</view>
|
||||
<view class="item-actions">
|
||||
<view class="action-item" @click.stop="editAddress(item.id)">
|
||||
<text class="action-icon">馃摑</text>
|
||||
</view>
|
||||
<view class="action-item" @click.stop="deleteAddress(item.id)">
|
||||
<text class="action-icon">锟斤笍</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="footer-btn">
|
||||
<button class="add-btn" @click="addAddress">鏂板缓鏀惰揣鍦板潃</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
|
||||
type Address = {
|
||||
id: string
|
||||
name: string
|
||||
phone: string
|
||||
province: string
|
||||
city: string
|
||||
district: string
|
||||
detail: string
|
||||
isDefault: boolean
|
||||
label?: string
|
||||
}
|
||||
|
||||
const addresses = ref<Address[]>([])
|
||||
const selectionMode = ref<boolean>(false)
|
||||
let openerEventChannel: any | null = null
|
||||
|
||||
onShow(() => {
|
||||
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 storedAddresses = uni.getStorageSync('addresses')
|
||||
if (storedAddresses) {
|
||||
try {
|
||||
addresses.value = JSON.parse(storedAddresses as string) as Address[]
|
||||
} catch (e) {
|
||||
console.error('Failed to parse addresses', e)
|
||||
addresses.value = []
|
||||
}
|
||||
} else {
|
||||
// 鍒濆Mock鏁版嵁
|
||||
addresses.value = [
|
||||
{
|
||||
id: 'addr_001',
|
||||
name: '寮犱笁',
|
||||
phone: '13800138000',
|
||||
province: '鍖椾含甯?,
|
||||
city: '鍖椾含甯?,
|
||||
district: '鏈濋槼鍖?,
|
||||
detail: '涓夐噷灞疭OHO A搴?,
|
||||
isDefault: true,
|
||||
label: '鍏徃'
|
||||
}
|
||||
]
|
||||
uni.setStorageSync('addresses', JSON.stringify(addresses.value))
|
||||
}
|
||||
}
|
||||
|
||||
const getFullAddress = (item: Address): string => {
|
||||
return `${item.province}${item.city}${item.district} ${item.detail}`
|
||||
}
|
||||
|
||||
const addAddress = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/consumer/address-edit'
|
||||
})
|
||||
}
|
||||
|
||||
// 鍒犻櫎鍦板潃
|
||||
const deleteAddress = (id: string) => {
|
||||
uni.showModal({
|
||||
title: '鎻愮ず',
|
||||
content: '纭畾瑕佸垹闄よ鍦板潃鍚楋紵',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
const index = addresses.value.findIndex(addr => addr.id === id)
|
||||
if (index !== -1) {
|
||||
addresses.value.splice(index, 1)
|
||||
uni.setStorageSync('addresses', JSON.stringify(addresses.value))
|
||||
uni.showToast({
|
||||
title: '鍒犻櫎鎴愬姛',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const editAddress = (id: string) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/consumer/address-edit?id=${id}`
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.item-actions {
|
||||
padding: 10px;
|
||||
border-left: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
flex-direction: column; /* 绔栧悜鎺掑垪鍥炬爣 */
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.footer-btn {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: white;
|
||||
padding: 10px 15px;
|
||||
padding-bottom: calc(10px + env(safe-area-inset-bottom));
|
||||
box-shadow: 0 -2px 10px rgba(0,0,0,0.05);
|
||||
display: flex;
|
||||
justify-content: center; /* 灞呬腑鏄剧ず */
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
background-color: #ff5000;
|
||||
color: white;
|
||||
border-radius: 25px;
|
||||
font-size: 16px;
|
||||
height: 44px;
|
||||
line-height: 44px;
|
||||
border: none;
|
||||
width: 100%; /* 榛樿鍗犳弧 */
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* 鍝嶅簲寮忓竷灞€浼樺寲 */
|
||||
@media screen and (min-width: 768px) {
|
||||
.address-list {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.address-list-page {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.footer-btn {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
box-shadow: 0 -2px 10px rgba(0,0,0,0.05);
|
||||
border-radius: 12px 12px 0 0; /* 妗岄潰绔姞鐐瑰渾瑙掓洿缇庤 */
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
width: 300px; /* 妗岄潰绔檺鍒跺搴?*/
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
1561
doc_mall/consumer/backup_pages/cart copy.uvue
Normal file
1561
doc_mall/consumer/backup_pages/cart copy.uvue
Normal file
File diff suppressed because it is too large
Load Diff
25
doc_mall/consumer/backup_pages/cart_debug.txt
Normal file
25
doc_mall/consumer/backup_pages/cart_debug.txt
Normal file
@@ -0,0 +1,25 @@
|
||||
<!-- pages/mall/consumer/cart.uvue -->
|
||||
<template>
|
||||
<view class="cart-page">
|
||||
<!-- 鏅鸿兘椤堕儴瀵艰埅鏍?- 涓庢秷鎭〉淇濇寔涓€鑷?-->
|
||||
<view class="smart-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="nav-container">
|
||||
<text class="nav-title">璐墿杞?/text>
|
||||
<view class="nav-actions">
|
||||
<view class="action-btn" @click="toggleManageMode">
|
||||
<text class="action-icon">{{ isManageMode ? '鉁? : '鈿欙笍' }}</text>
|
||||
<text class="action-text">{{ isManageMode ? '瀹屾垚' : '绠$悊' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 瀵艰埅鏍忓崰浣嶇 - 闇€瑕佸寘鍚玸tatusBarHeight + 瀵艰埅鏍忛珮搴?4px -->
|
||||
<view class="navbar-placeholder" :style="{ height: (statusBarHeight + 44) + 'px' }"></view>
|
||||
|
||||
<!-- 璐墿杞﹀唴瀹?-->
|
||||
<scroll-view
|
||||
:scroll-y="true"
|
||||
class="cart-content"
|
||||
:show-scrollbar="false"
|
||||
:enhanced="true"
|
||||
1373
doc_mall/consumer/backup_pages/cart药品.uvue
Normal file
1373
doc_mall/consumer/backup_pages/cart药品.uvue
Normal file
File diff suppressed because it is too large
Load Diff
1271
doc_mall/consumer/backup_pages/category copy 2.uvue
Normal file
1271
doc_mall/consumer/backup_pages/category copy 2.uvue
Normal file
File diff suppressed because it is too large
Load Diff
1467
doc_mall/consumer/backup_pages/category copy 3.uvue
Normal file
1467
doc_mall/consumer/backup_pages/category copy 3.uvue
Normal file
File diff suppressed because it is too large
Load Diff
1248
doc_mall/consumer/backup_pages/category copy.uvue
Normal file
1248
doc_mall/consumer/backup_pages/category copy.uvue
Normal file
File diff suppressed because it is too large
Load Diff
1132
doc_mall/consumer/backup_pages/category药品.uvue
Normal file
1132
doc_mall/consumer/backup_pages/category药品.uvue
Normal file
File diff suppressed because it is too large
Load Diff
794
doc_mall/consumer/backup_pages/chat copy.uvue
Normal file
794
doc_mall/consumer/backup_pages/chat copy.uvue
Normal file
@@ -0,0 +1,794 @@
|
||||
<!-- pages/mall/consumer/chat.uvue -->
|
||||
<template>
|
||||
<view class="chat-page">
|
||||
<!-- 聊天头部 -->
|
||||
<view class="chat-header" :style="{ paddingTop: navPaddingTop }">
|
||||
<view class="header-back" @click="goBack">
|
||||
<text class="back-icon">🔙</text>
|
||||
</view>
|
||||
<view class="header-info">
|
||||
<text class="chat-title">{{ headerTitle }}</text>
|
||||
<text class="chat-status">在线</text>
|
||||
</view>
|
||||
<view class="header-actions">
|
||||
<text class="action-icon" @click="showMoreActions">⋯</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 聊天内容 -->
|
||||
<scroll-view
|
||||
scroll-y="true"
|
||||
class="chat-content"
|
||||
:scroll-into-view="scrollToView"
|
||||
:scroll-with-animation="false"
|
||||
:show-scrollbar="false"
|
||||
upper-threshold="100"
|
||||
@scrolltoupper="onScrollToUpper"
|
||||
>
|
||||
<!-- 聊天消息列表 -->
|
||||
<view class="chat-messages">
|
||||
<!-- 系统消息 -->
|
||||
<view class="message-item system">
|
||||
<text class="system-text">客服 小美 已接入,请描述您的问题</text>
|
||||
</view>
|
||||
|
||||
<!-- 时间分割线 -->
|
||||
<view class="time-divider">
|
||||
<text class="time-text">今天 14:30</text>
|
||||
</view>
|
||||
|
||||
<!-- 消息项 -->
|
||||
<view
|
||||
v-for="message in messages"
|
||||
:key="message.id"
|
||||
:class="['message-item', message.type]"
|
||||
:id="message.viewId"
|
||||
>
|
||||
<!-- 对方消息 -->
|
||||
<view v-if="message.type === 'received'" class="message-wrapper">
|
||||
<image
|
||||
class="avatar"
|
||||
:src="merchantAvatar"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="message-content-wrapper">
|
||||
<text class="sender-name">{{ headerTitle }}</text>
|
||||
<view class="message-bubble">
|
||||
<text class="message-text">{{ message.content }}</text>
|
||||
<text class="message-time">{{ message.time }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 我的消息 -->
|
||||
<view v-else class="message-wrapper me">
|
||||
<view class="message-content-wrapper">
|
||||
<view class="message-bubble me">
|
||||
<text class="message-text">{{ message.content }}</text>
|
||||
<text class="message-time">{{ message.time }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<image
|
||||
class="avatar me"
|
||||
src="/static/default-avatar.png"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 聊天输入框 -->
|
||||
<view class="chat-input">
|
||||
<view class="input-tools">
|
||||
<text class="tool-icon" @click="showEmojiPicker">😊</text>
|
||||
<text class="tool-icon" @click="showImagePicker">📷</text>
|
||||
<text class="tool-icon" @click="showMoreTools">➕</text>
|
||||
</view>
|
||||
|
||||
<view class="input-wrapper">
|
||||
<input
|
||||
class="message-input"
|
||||
v-model="inputMessage"
|
||||
placeholder="请输入消息..."
|
||||
:focus="inputFocus"
|
||||
@confirm="sendMessage"
|
||||
confirm-type="send"
|
||||
/>
|
||||
<button
|
||||
class="send-button"
|
||||
:class="{ active: inputMessage.trim() }"
|
||||
@click="sendMessage"
|
||||
>
|
||||
发送
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 表情选择器 -->
|
||||
<scroll-view v-if="showEmoji" class="emoji-picker" direction="vertical">
|
||||
<view class="emoji-category">
|
||||
<text
|
||||
v-for="emoji in emojiList"
|
||||
:key="emoji"
|
||||
class="emoji-item"
|
||||
@click="insertEmoji(emoji)"
|
||||
>
|
||||
{{ emoji }}
|
||||
</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { supabaseService, type ChatMessage } from '@/utils/supabaseService.uts'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
import type { AkSupaRealtimeChannel } from '@/components/supadb/aksupa.uts'
|
||||
import { getCurrentUser } from '@/utils/store.uts'
|
||||
|
||||
type UiChatMessage = {
|
||||
id: string
|
||||
viewId: string
|
||||
type: string
|
||||
content: string
|
||||
time: string
|
||||
}
|
||||
|
||||
// 响应式数据
|
||||
const messages = ref<UiChatMessage[]>([])
|
||||
const inputMessage = ref<string>('')
|
||||
const inputFocus = ref<boolean>(false)
|
||||
const showEmoji = ref<boolean>(false)
|
||||
const scrollToView = ref<string>('')
|
||||
const currentUserId = ref<string>('')
|
||||
const merchantId = ref<string>('') // 商家ID
|
||||
const headerTitle = ref<string>('在线客服')
|
||||
const merchantAvatar = ref<string>('/static/default-shop.png') // 商家头像
|
||||
const navPaddingTop = ref<string>('30px') // 默认值,包含状态栏高度+原有内边距
|
||||
const isInitialLoading = ref<boolean>(true)
|
||||
let realtimeChannel: AkSupaRealtimeChannel | null = null
|
||||
|
||||
// 模拟表情列表
|
||||
const emojiList = ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '']
|
||||
|
||||
function scrollToBottom() : void {
|
||||
if (messages.value.length === 0) return
|
||||
|
||||
// 获取最后一条消息的 ID
|
||||
const lastMsg = messages.value[messages.value.length - 1]
|
||||
const targetId = 'msg-' + lastMsg.id
|
||||
|
||||
// 关键点:在 UVue 安卓端,直接连续赋值可能被合并。
|
||||
// 我们先清空 ID,然后在下一帧赋值,确保 scroll-view 监听到变化。
|
||||
scrollToView.value = ''
|
||||
|
||||
// 增加多次尝试,确保在 DOM 彻底完成渲染(包含由于高度计算引起的多次排版)后定位。
|
||||
setTimeout(() => {
|
||||
scrollToView.value = targetId
|
||||
console.log('[scrollToBottom] 发起第一次滚动定位', targetId)
|
||||
|
||||
// 二次校准:针对长消息或图片导致的高度变化
|
||||
setTimeout(() => {
|
||||
scrollToView.value = ''
|
||||
setTimeout(() => {
|
||||
scrollToView.value = targetId
|
||||
console.log('[scrollToBottom] 二次校准完成:', targetId)
|
||||
}, 50)
|
||||
}, 100)
|
||||
}, 300)
|
||||
}
|
||||
|
||||
function getCurrentTime(): string {
|
||||
const now = new Date()
|
||||
const hours = now.getHours().toString().padStart(2, '0')
|
||||
const minutes = now.getMinutes().toString().padStart(2, '0')
|
||||
return ${hours}:
|
||||
}
|
||||
|
||||
function setupRealtimeSubscription(): void {
|
||||
console.log('开始建立聊天实时订阅...')
|
||||
console.log('当前用户ID:', currentUserId.value, '商家ID:', merchantId.value)
|
||||
|
||||
realtimeChannel = supa.channel('chat-messages-' + Date.now().toString())
|
||||
.on('postgres_changes', {
|
||||
event: 'INSERT',
|
||||
schema: 'public',
|
||||
table: 'ml_chat_messages'
|
||||
}, (payload: any) => {
|
||||
console.log('=== 收到实时订阅回调 ===')
|
||||
const payloadObj = (payload instanceof UTSJSONObject) ? (payload as UTSJSONObject) : (JSON.parse(JSON.stringify(payload ?? {})) as UTSJSONObject)
|
||||
const newMsgAny = payloadObj.get('new')
|
||||
if (newMsgAny == null) {
|
||||
console.log('newMsgAny 为空,跳过')
|
||||
return
|
||||
}
|
||||
const newMsg = (newMsgAny instanceof UTSJSONObject) ? (newMsgAny as UTSJSONObject) : (JSON.parse(JSON.stringify(newMsgAny)) as UTSJSONObject)
|
||||
console.log('收到新消息', newMsg)
|
||||
|
||||
const senderId = newMsg.getString('sender_id') ?? ''
|
||||
const receiverId = newMsg.getString('receiver_id') ?? ''
|
||||
const msgId = newMsg.getString('id') ?? ''
|
||||
const content = newMsg.getString('content') ?? ''
|
||||
|
||||
console.log('=== 消息详情 ===')
|
||||
console.log('消息ID:', msgId)
|
||||
console.log('发送者ID:', senderId)
|
||||
console.log('接收者ID:', receiverId)
|
||||
console.log('当前用户ID:', currentUserId.value)
|
||||
console.log('商家ID:', merchantId.value)
|
||||
console.log('消息内容:', content)
|
||||
|
||||
// 检查消息是否已经在列表中(避免重复)
|
||||
for (let i = 0; i < messages.value.length; i++) {
|
||||
if (messages.value[i].id == msgId) {
|
||||
console.log('消息已存在,跳过')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 判断消息类型
|
||||
const isMyMessage = (senderId == currentUserId.value)
|
||||
const isForMe = (receiverId == currentUserId.value)
|
||||
const isRelatedToCurrentChat = (senderId == merchantId.value || receiverId == merchantId.value)
|
||||
|
||||
console.log('=== 条件判断 ===')
|
||||
console.log('isMyMessage:', isMyMessage)
|
||||
console.log('isForMe:', isForMe)
|
||||
console.log('isRelatedToCurrentChat:', isRelatedToCurrentChat)
|
||||
|
||||
// 如果消息与当前聊天无关,跳过
|
||||
if (!isRelatedToCurrentChat) {
|
||||
console.log('消息与当前聊天无关,跳过')
|
||||
return
|
||||
}
|
||||
|
||||
// 如果是自己发送的消息,或者是发给自己的消息,都显示
|
||||
if (isMyMessage || isForMe) {
|
||||
const createdAt = newMsg.getString('created_at') ?? new Date().toISOString()
|
||||
const date = new Date(createdAt)
|
||||
const timeStr = ${date.getHours().toString().padStart(2, '0')}:
|
||||
|
||||
// 生成安全从 viewId
|
||||
const safeViewId = 'msg_' + msgId.replace(/[^a-zA-Z0-9]/g, '_')
|
||||
|
||||
const incomingMsg: UiChatMessage = {
|
||||
id: msgId,
|
||||
viewId: safeViewId,
|
||||
type: isMyMessage ? 'sent' : 'received',
|
||||
content: content,
|
||||
time: timeStr
|
||||
}
|
||||
|
||||
console.log('=== 添加新消息到列表 ===')
|
||||
console.log('消息类型:', incomingMsg.type)
|
||||
console.log('消息内容:', incomingMsg.content)
|
||||
messages.value.push(incomingMsg)
|
||||
scrollToBottom()
|
||||
} else {
|
||||
console.log('条件不满足,不添加消息')
|
||||
}
|
||||
})
|
||||
.subscribe((status: string, err: any | null) => {
|
||||
console.log('订阅状态', status)
|
||||
if (err != null) {
|
||||
console.log('订阅错误:', err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function loadChatHistory(): Promise<void> {
|
||||
let rawMsgs : ChatMessage[] = []
|
||||
|
||||
if (merchantId.value != '') {
|
||||
rawMsgs = await supabaseService.getChatMessages(merchantId.value)
|
||||
} else {
|
||||
console.warn("No merchant ID provided for chat")
|
||||
return
|
||||
}
|
||||
|
||||
// 确保时间顺序是升序(旧的在前,新的在后)
|
||||
// Supabase 返回的消息如果是降序,我们需要 reverse 过来显示
|
||||
const sortedRawMsgs = rawMsgs.sort((a, b) => {
|
||||
const timeA = new Date(a.created_at ?? '').getTime()
|
||||
const timeB = new Date(b.created_at ?? '').getTime()
|
||||
return timeA - timeB
|
||||
})
|
||||
|
||||
const uiMessages : UiChatMessage[] = []
|
||||
for (let i = 0; i < sortedRawMsgs.length; i++) {
|
||||
const m = sortedRawMsgs[i]
|
||||
const date = new Date(m.created_at ?? new Date().toISOString())
|
||||
const timeStr = ${date.getHours().toString().padStart(2, '0')}:
|
||||
|
||||
const sender = m.sender_id ?? ''
|
||||
const msgType = (currentUserId.value != '' && sender == currentUserId.value) ? 'sent' : 'received'
|
||||
const rawId = (m.id ?? '').toString()
|
||||
const msgId = rawId != '' ? rawId : Date.now().toString() + i.toString()
|
||||
const safeViewId = 'msg_' + msgId.replace(/[^a-zA-Z0-9]/g, '_')
|
||||
|
||||
const uiMsg : UiChatMessage = {
|
||||
id: msgId,
|
||||
viewId: safeViewId,
|
||||
type: msgType,
|
||||
content: m.content ?? '',
|
||||
time: timeStr
|
||||
}
|
||||
uiMessages.push(uiMsg)
|
||||
}
|
||||
messages.value = uiMessages
|
||||
|
||||
if (isInitialLoading.value) {
|
||||
// 增加一点初始化延迟,等待 scroll-view 渲染就绪
|
||||
setTimeout(() => {
|
||||
scrollToBottom()
|
||||
isInitialLoading.value = false
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
|
||||
function onScrollToUpper(e: any): void {
|
||||
console.log('[onScrollToUpper] 触发加载历史记录')
|
||||
}
|
||||
|
||||
async function loadMerchantInfo(): Promise<void> {
|
||||
if (merchantId.value == '') return
|
||||
|
||||
try {
|
||||
const response = await supa
|
||||
.from('ml_shops')
|
||||
.select('shop_logo, shop_name')
|
||||
.eq('merchant_id', merchantId.value)
|
||||
.limit(1)
|
||||
.execute()
|
||||
|
||||
if (response.error != null) {
|
||||
console.error('[loadMerchantInfo] 获取商家信息失败:', response.error)
|
||||
return
|
||||
}
|
||||
|
||||
const rawData = response.data
|
||||
if (rawData == null) return
|
||||
|
||||
const rawList = rawData as any[]
|
||||
if (rawList.length == 0) return
|
||||
|
||||
const shopData = rawList[0]
|
||||
const shopObj = JSON.parse(JSON.stringify(shopData)) as UTSJSONObject
|
||||
|
||||
const logo = shopObj.getString('shop_logo')
|
||||
if (logo != null && logo != '') {
|
||||
merchantAvatar.value = logo
|
||||
}
|
||||
|
||||
const name = shopObj.getString('shop_name')
|
||||
if (name != null && name != '' && headerTitle.value == '在线客服') {
|
||||
headerTitle.value = name
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[loadMerchantInfo] 获取商家信息异常:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onLoad((options: any) => {
|
||||
// 动态获取状态栏高度
|
||||
const sysInfo = uni.getSystemInfoSync()
|
||||
const statusBarH = sysInfo.statusBarHeight
|
||||
// 状态栏高度 + 10px 原有顶部内边距
|
||||
navPaddingTop.value = (statusBarH + 10) + 'px'
|
||||
|
||||
const optObj = (options instanceof UTSJSONObject) ? (options as UTSJSONObject) : (JSON.parse(JSON.stringify(options ?? {})) as UTSJSONObject)
|
||||
const mid = optObj.getString('merchantId') ?? ''
|
||||
if (mid !== '') {
|
||||
merchantId.value = mid
|
||||
}
|
||||
const mname = optObj.getString('merchantName') ?? ''
|
||||
if (mname !== '') {
|
||||
headerTitle.value = mname
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
supabaseService.ensureSession().then((uid) => {
|
||||
if (uid != null) {
|
||||
currentUserId.value = uid
|
||||
} else {
|
||||
getCurrentUser().then((user) => {
|
||||
if (user != null) {
|
||||
currentUserId.value = user.id ?? ''
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
loadMerchantInfo()
|
||||
loadChatHistory()
|
||||
setupRealtimeSubscription()
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (realtimeChannel != null) {
|
||||
supa.removeChannel(realtimeChannel!!)
|
||||
}
|
||||
})
|
||||
|
||||
const sendMessage = async () => {
|
||||
const content = inputMessage.value.trim()
|
||||
if (content == '') return
|
||||
|
||||
// 清空输入框
|
||||
inputMessage.value = ''
|
||||
|
||||
// 发送到 Supabase
|
||||
if (merchantId.value != '') {
|
||||
console.log('[sendMessage] 开始发送消息到:', merchantId.value)
|
||||
const success = await supabaseService.sendMessage(merchantId.value, content)
|
||||
console.log('[sendMessage] 发送结果', success)
|
||||
if (!success) {
|
||||
uni.showToast({
|
||||
title: '发送失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
// 不需要手动添加消息,等待实时订阅推送
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟客服回复 (已禁用,改用 Realtime)
|
||||
/*
|
||||
const simulateCustomerReply = async () => {
|
||||
// ...
|
||||
}
|
||||
*/
|
||||
|
||||
/* 移除不再使用的 simulateCustomerReply 和 addReceivedMessage */
|
||||
|
||||
// 插入表情
|
||||
function insertEmoji(emoji: string): void {
|
||||
inputMessage.value += emoji
|
||||
inputFocus.value = true
|
||||
}
|
||||
|
||||
// 显示表情选择器
|
||||
function showEmojiPicker(): void {
|
||||
showEmoji.value = !showEmoji.value
|
||||
}
|
||||
|
||||
// 显示图片选择器
|
||||
function showImagePicker(): void {
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
success: (res) => {
|
||||
console.log('选择图片:', res.tempFilePaths)
|
||||
// 这里可以处理图片上传
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 显示更多工具
|
||||
function showMoreTools(): void {
|
||||
uni.showActionSheet({
|
||||
itemList: ['发送位置', '发送文件', '发送语音'],
|
||||
success: (res) => {
|
||||
console.log('选择工具:', res.tapIndex)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 显示更多操作
|
||||
function showMoreActions(): void {
|
||||
uni.showActionSheet({
|
||||
itemList: ['投诉客服', '结束对话', '清除记录'],
|
||||
success: (res) => {
|
||||
switch (res.tapIndex) {
|
||||
case 0:
|
||||
uni.navigateTo({ url: '/pages/mall/consumer/complaint' })
|
||||
break
|
||||
case 1:
|
||||
uni.showModal({
|
||||
title: '确认结束',
|
||||
content: '确定要结束本次对话吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.navigateBack()
|
||||
}
|
||||
}
|
||||
})
|
||||
break
|
||||
case 2:
|
||||
uni.showModal({
|
||||
title: '确认清除',
|
||||
content: '确定要清除聊天记录吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
messages.value = []
|
||||
}
|
||||
}
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 返回
|
||||
const goBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.chat-page {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 聊天头部 */
|
||||
.chat-header {
|
||||
background-color: white;
|
||||
padding: 10px 15px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid #eee;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-back {
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.back-icon {
|
||||
font-size: 24px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.header-info {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.chat-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.chat-status {
|
||||
font-size: 12px;
|
||||
color: #34c759;
|
||||
}
|
||||
|
||||
.header-actions .action-icon {
|
||||
font-size: 20px;
|
||||
color: #333;
|
||||
width: 40px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* 聊天内容区 */
|
||||
.chat-content {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
padding: 10px;
|
||||
padding-bottom: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
/* 系统消息 */
|
||||
.message-item.system {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.system-text {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
background-color: #f0f0f0;
|
||||
padding: 5px 15px;
|
||||
border-radius: 15px;
|
||||
|
||||
}
|
||||
|
||||
/* 时间分割线 */
|
||||
.time-divider {
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.time-text {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
background-color: #f0f0f0;
|
||||
padding: 3px 10px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* 消息项 */
|
||||
.message-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.message-wrapper.me {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 20px;
|
||||
margin-right: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.avatar.me {
|
||||
margin-right: 0;
|
||||
margin-left: 10px;
|
||||
/* order: 2; removed for uni-app-x */
|
||||
}
|
||||
|
||||
.message-content-wrapper {
|
||||
max-width: 70%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.message-bubble {
|
||||
background-color: white;
|
||||
padding: 10px 15px;
|
||||
border-radius: 12px;
|
||||
position: relative;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
align-self: flex-start; /* 关键:根据内容宽度自适应,不撑满 */
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.message-bubble.me {
|
||||
background-color: #95ec69;
|
||||
align-self: flex-end; /* 关键:靠右对齐且宽度自适应 */
|
||||
border-top-right-radius: 2px;
|
||||
}
|
||||
|
||||
.message-bubble:not(.me) {
|
||||
border-top-left-radius: 2px;
|
||||
}
|
||||
|
||||
.sender-name {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
margin-bottom: 2px;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.message-text {
|
||||
font-size: 15px;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 5px;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.message-text {
|
||||
font-size: 15px;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 5px;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* 聊天输入框 */
|
||||
.chat-input {
|
||||
background-color: white;
|
||||
border-top: 1px solid #eee;
|
||||
padding: 10px 15px;
|
||||
padding-bottom: 20px;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.input-tools {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.tool-icon {
|
||||
font-size: 20px;
|
||||
margin-right: 15px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.message-input {
|
||||
flex: 1;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 20px;
|
||||
padding: 10px 15px;
|
||||
font-size: 15px;
|
||||
margin-right: 10px;
|
||||
min-height: 40px;
|
||||
max-height: 100px;
|
||||
}
|
||||
|
||||
.send-button {
|
||||
background-color: #ccc;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
padding: 8px 20px;
|
||||
font-size: 14px;
|
||||
min-width: 60px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.send-button.active {
|
||||
background-color: #ff5000;
|
||||
}
|
||||
|
||||
/* 表情选择器 */
|
||||
.emoji-picker {
|
||||
background-color: white;
|
||||
border-top: 1px solid #eee;
|
||||
padding: 10px;
|
||||
height: 200px;
|
||||
position: fixed;
|
||||
bottom: 80px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.emoji-category {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.emoji-item {
|
||||
font-size: 24px;
|
||||
padding: 8px;
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 响应式适配 removed for strict uv-app-x compliance */
|
||||
</style>
|
||||
|
||||
2672
doc_mall/consumer/backup_pages/index copy 2.uvue
Normal file
2672
doc_mall/consumer/backup_pages/index copy 2.uvue
Normal file
File diff suppressed because it is too large
Load Diff
2674
doc_mall/consumer/backup_pages/index copy.uvue
Normal file
2674
doc_mall/consumer/backup_pages/index copy.uvue
Normal file
File diff suppressed because it is too large
Load Diff
2193
doc_mall/consumer/backup_pages/index医药.uvue
Normal file
2193
doc_mall/consumer/backup_pages/index医药.uvue
Normal file
File diff suppressed because it is too large
Load Diff
1373
doc_mall/consumer/backup_pages/messages copy.uvue
Normal file
1373
doc_mall/consumer/backup_pages/messages copy.uvue
Normal file
File diff suppressed because it is too large
Load Diff
1279
doc_mall/consumer/backup_pages/messages医药.uvue
Normal file
1279
doc_mall/consumer/backup_pages/messages医药.uvue
Normal file
File diff suppressed because it is too large
Load Diff
1418
doc_mall/consumer/backup_pages/orders copy 2.uvue
Normal file
1418
doc_mall/consumer/backup_pages/orders copy 2.uvue
Normal file
File diff suppressed because it is too large
Load Diff
1360
doc_mall/consumer/backup_pages/orders copy.uvue
Normal file
1360
doc_mall/consumer/backup_pages/orders copy.uvue
Normal file
File diff suppressed because it is too large
Load Diff
1815
doc_mall/consumer/backup_pages/product-detail copy 2.uvue
Normal file
1815
doc_mall/consumer/backup_pages/product-detail copy 2.uvue
Normal file
File diff suppressed because it is too large
Load Diff
1584
doc_mall/consumer/backup_pages/product-detail copy.uvue
Normal file
1584
doc_mall/consumer/backup_pages/product-detail copy.uvue
Normal file
File diff suppressed because it is too large
Load Diff
1786
doc_mall/consumer/backup_pages/profile copy 2.uvue
Normal file
1786
doc_mall/consumer/backup_pages/profile copy 2.uvue
Normal file
File diff suppressed because it is too large
Load Diff
1336
doc_mall/consumer/backup_pages/profile copy.uvue
Normal file
1336
doc_mall/consumer/backup_pages/profile copy.uvue
Normal file
File diff suppressed because it is too large
Load Diff
951
doc_mall/consumer/backup_pages/wallet - 副本.uvue
Normal file
951
doc_mall/consumer/backup_pages/wallet - 副本.uvue
Normal file
@@ -0,0 +1,951 @@
|
||||
<!-- 閽卞寘椤甸潰 -->
|
||||
<template>
|
||||
<view class="wallet-page">
|
||||
<!-- 椤堕儴鏍?-->
|
||||
<view class="wallet-header">
|
||||
<text class="back-btn" @click="goBack">鈥?/text>
|
||||
<text class="header-title">鎴戠殑閽卞寘</text>
|
||||
<text class="more-btn" @click="showMoreActions">路路路</text>
|
||||
</view>
|
||||
|
||||
<scroll-view class="wallet-content" scroll-y>
|
||||
<!-- 浣欓姒傝 -->
|
||||
<view class="balance-overview">
|
||||
<text class="balance-label">璐︽埛浣欓</text>
|
||||
<text class="balance-value">楼{{ balance.toFixed(2) }}</text>
|
||||
<view class="balance-actions">
|
||||
<button class="action-btn recharge" @click="recharge">鍏呭€?/button>
|
||||
<button class="action-btn withdraw" @click="withdraw">鎻愮幇</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 璧勪骇缁熻 -->
|
||||
<view class="assets-stats">
|
||||
<view class="stat-item">
|
||||
<text class="stat-label">绱鍏呭€?/text>
|
||||
<text class="stat-value">楼{{ stats.totalRecharge.toFixed(2) }}</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-label">绱娑堣垂</text>
|
||||
<text class="stat-value">楼{{ stats.totalConsume.toFixed(2) }}</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-label">绱鎻愮幇</text>
|
||||
<text class="stat-value">楼{{ stats.totalWithdraw.toFixed(2) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 蹇嵎鍔熻兘 -->
|
||||
<view class="quick-actions">
|
||||
<view class="action-grid">
|
||||
<view class="action-item" @click="goToCoupons">
|
||||
<text class="action-icon">馃帿</text>
|
||||
<text class="action-text">浼樻儬鍒?/text>
|
||||
</view>
|
||||
<view class="action-item" @click="goToRedPackets">
|
||||
<text class="action-icon">馃Ё</text>
|
||||
<text class="action-text">绾㈠寘</text>
|
||||
</view>
|
||||
<view class="action-item" @click="goToPoints">
|
||||
<text class="action-icon">猸?/text>
|
||||
<text class="action-text">绉垎</text>
|
||||
</view>
|
||||
<view class="action-item" @click="goToBankCards">
|
||||
<text class="action-icon">馃挸</text>
|
||||
<text class="action-text">閾惰鍗?/text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 浜ゆ槗璁板綍 -->
|
||||
<view class="transactions-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">浜ゆ槗璁板綍</text>
|
||||
<view class="filter-tabs">
|
||||
<text :class="['filter-tab', { active: activeFilter === 'all' }]"
|
||||
@click="changeFilter('all')">鍏ㄩ儴</text>
|
||||
<text :class="['filter-tab', { active: activeFilter === 'income' }]"
|
||||
@click="changeFilter('income')">鏀跺叆</text>
|
||||
<text :class="['filter-tab', { active: activeFilter === 'expense' }]"
|
||||
@click="changeFilter('expense')">鏀嚭</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 绌虹姸鎬?-->
|
||||
<view v-if="transactions.length === 0 && !isLoading" class="empty-transactions">
|
||||
<text class="empty-icon">馃挵</text>
|
||||
<text class="empty-text">鏆傛棤浜ゆ槗璁板綍</text>
|
||||
<text class="empty-subtext">蹇幓浣跨敤閽卞寘鍔熻兘鍚?/text>
|
||||
</view>
|
||||
|
||||
<!-- 浜ゆ槗鍒楄〃 -->
|
||||
<view class="transactions-list">
|
||||
<view v-for="transaction in transactions"
|
||||
:key="transaction.id"
|
||||
class="transaction-item">
|
||||
<view class="transaction-left">
|
||||
<text class="transaction-icon">{{ getTransactionIcon(transaction.type) }}</text>
|
||||
<view class="transaction-info">
|
||||
<text class="transaction-title">{{ getTransactionTitle(transaction.type) }}</text>
|
||||
<text class="transaction-time">{{ formatTime(transaction.created_at) }}</text>
|
||||
<text v-if="transaction.remark" class="transaction-remark">{{ transaction.remark }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="transaction-right">
|
||||
<text :class="['transaction-amount',
|
||||
{ income: transaction.amount > 0, expense: transaction.amount < 0 }]">
|
||||
{{ transaction.amount > 0 ? '+' : '' }}楼{{ Math.abs(transaction.amount).toFixed(2) }}
|
||||
</text>
|
||||
<text class="transaction-balance">浣欓: 楼{{ transaction.current_balance.toFixed(2) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 鍔犺浇鏇村 -->
|
||||
<view v-if="isLoading" class="loading-more">
|
||||
<text class="loading-text">鍔犺浇涓?..</text>
|
||||
</view>
|
||||
<view v-if="!hasMore && transactions.length > 0" class="no-more">
|
||||
<text class="no-more-text">娌℃湁鏇村璁板綍浜?/text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 瀹夊叏鎻愮ず -->
|
||||
<view class="security-tips">
|
||||
<text class="tip-title">瀹夊叏鎻愮ず</text>
|
||||
<text class="tip-item">1. 璇峰Ε鍠勪繚绠℃偍鐨勬敮浠樺瘑鐮?/text>
|
||||
<text class="tip-item">2. 涓嶈鍚戜粬浜洪€忛湶鎮ㄧ殑璐︽埛淇℃伅</text>
|
||||
<text class="tip-item">3. 瀹氭湡淇敼瀵嗙爜浠ョ‘淇濊处鎴峰畨鍏?/text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 鍏呭€煎脊绐?-->
|
||||
<view v-if="showRechargePopup" class="recharge-popup">
|
||||
<view class="popup-mask" @click="closeRechargePopup"></view>
|
||||
<view class="popup-content">
|
||||
<view class="popup-header">
|
||||
<text class="popup-title">鍏呭€?/text>
|
||||
<text class="popup-close" @click="closeRechargePopup">脳</text>
|
||||
</view>
|
||||
<view class="popup-body">
|
||||
<text class="amount-label">鍏呭€奸噾棰?/text>
|
||||
<view class="amount-input">
|
||||
<text class="currency-symbol">楼</text>
|
||||
<input class="amount-field"
|
||||
v-model="rechargeAmount"
|
||||
type="number"
|
||||
placeholder="璇疯緭鍏ュ厖鍊奸噾棰?
|
||||
focus />
|
||||
</view>
|
||||
<view class="quick-amounts">
|
||||
<text v-for="amount in quickAmounts"
|
||||
:key="amount"
|
||||
:class="['quick-amount', { active: rechargeAmount === amount.toString() }]"
|
||||
@click="selectQuickAmount(amount)">
|
||||
楼{{ amount }}
|
||||
</text>
|
||||
</view>
|
||||
<text class="recharge-tip">鍗曠瑪鍏呭€兼渶浣?0鍏冿紝鏈€楂?000鍏?/text>
|
||||
</view>
|
||||
<view class="popup-footer">
|
||||
<button class="cancel-btn" @click="closeRechargePopup">鍙栨秷</button>
|
||||
<button class="confirm-btn"
|
||||
:class="{ disabled: !canRecharge }"
|
||||
@click="confirmRecharge">
|
||||
纭鍏呭€?
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted, computed, watch } from 'vue'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
type WalletType = {
|
||||
id: string
|
||||
user_id: string
|
||||
balance: number
|
||||
total_recharge: number
|
||||
total_consume: number
|
||||
total_withdraw: number
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
type TransactionType = {
|
||||
id: string
|
||||
user_id: string
|
||||
change_amount: number
|
||||
current_balance: number
|
||||
change_type: string // 'recharge' | 'consume' | 'withdraw' | 'refund' | 'reward'
|
||||
related_id: string | null
|
||||
remark: string | null
|
||||
created_at: string
|
||||
}
|
||||
|
||||
type StatsType = {
|
||||
totalRecharge: number
|
||||
totalConsume: number
|
||||
totalWithdraw: number
|
||||
}
|
||||
|
||||
const balance = ref<number>(0)
|
||||
const stats = ref<StatsType>({
|
||||
totalRecharge: 0,
|
||||
totalConsume: 0,
|
||||
totalWithdraw: 0
|
||||
})
|
||||
const transactions = ref<Array<TransactionType>>([])
|
||||
const activeFilter = ref<string>('all')
|
||||
const isLoading = ref<boolean>(false)
|
||||
const currentPage = ref<number>(1)
|
||||
const pageSize = ref<number>(20)
|
||||
const hasMore = ref<boolean>(true)
|
||||
const showRechargePopup = ref<boolean>(false)
|
||||
const rechargeAmount = ref<string>('')
|
||||
const quickAmounts = [50, 100, 200, 500, 1000]
|
||||
|
||||
// 璁$畻灞炴€?
|
||||
const canRecharge = computed(() => {
|
||||
const amount = parseFloat(rechargeAmount.value)
|
||||
return !isNaN(amount) && amount >= 10 && amount <= 5000
|
||||
})
|
||||
|
||||
// 鐩戝惉杩囨护鍣ㄥ彉鍖?
|
||||
watch(activeFilter, () => {
|
||||
resetTransactions()
|
||||
loadTransactions()
|
||||
})
|
||||
|
||||
// 鐢熷懡鍛ㄦ湡
|
||||
onMounted(() => {
|
||||
loadWalletData()
|
||||
})
|
||||
|
||||
// 閲嶇疆浜ゆ槗璁板綍
|
||||
const resetTransactions = () => {
|
||||
transactions.value = []
|
||||
currentPage.value = 1
|
||||
hasMore.value = true
|
||||
}
|
||||
|
||||
// 鍔犺浇閽卞寘鏁版嵁
|
||||
const loadWalletData = async () => {
|
||||
const userId = getCurrentUserId()
|
||||
if (!userId) {
|
||||
uni.navigateTo({
|
||||
url: '/pages/user/login'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
loadBalance(),
|
||||
loadTransactions()
|
||||
])
|
||||
}
|
||||
|
||||
// 鍔犺浇浣欓淇℃伅
|
||||
const loadBalance = async () => {
|
||||
const userId = getCurrentUserId()
|
||||
if (!userId) return
|
||||
|
||||
try {
|
||||
const { data, error } = await supa
|
||||
.from('user_wallets')
|
||||
.select('*')
|
||||
.eq('user_id', userId)
|
||||
.single()
|
||||
|
||||
if (error !== null) {
|
||||
console.error('鍔犺浇閽卞寘澶辫触:', error)
|
||||
return
|
||||
}
|
||||
|
||||
if (data) {
|
||||
balance.value = data.balance || 0
|
||||
stats.value = {
|
||||
totalRecharge: data.total_recharge || 0,
|
||||
totalConsume: data.total_consume || 0,
|
||||
totalWithdraw: data.total_withdraw || 0
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('鍔犺浇閽卞寘寮傚父:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 鍔犺浇浜ゆ槗璁板綍
|
||||
const loadTransactions = async (loadMore: boolean = false) => {
|
||||
if (isLoading.value || (!hasMore.value && loadMore)) {
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
const userId = getCurrentUserId()
|
||||
if (!userId) return
|
||||
|
||||
const page = loadMore ? currentPage.value + 1 : 1
|
||||
|
||||
let query = supa
|
||||
.from('balance_records')
|
||||
.select('*')
|
||||
.eq('user_id', userId)
|
||||
.order('created_at', { ascending: false })
|
||||
|
||||
// 鏍规嵁杩囨护鍣ㄧ瓫閫?
|
||||
if (activeFilter.value === 'income') {
|
||||
query = query.gt('change_amount', 0)
|
||||
} else if (activeFilter.value === 'expense') {
|
||||
query = query.lt('change_amount', 0)
|
||||
}
|
||||
|
||||
// 鍒嗛〉
|
||||
query = query.range((page - 1) * pageSize.value, page * pageSize.value - 1)
|
||||
|
||||
const { data, error } = await query
|
||||
|
||||
if (error !== null) {
|
||||
console.error('鍔犺浇浜ゆ槗璁板綍澶辫触:', error)
|
||||
return
|
||||
}
|
||||
|
||||
const newTransactions = data || []
|
||||
|
||||
if (loadMore) {
|
||||
transactions.value.push(...newTransactions)
|
||||
currentPage.value = page
|
||||
} else {
|
||||
transactions.value = newTransactions
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
hasMore.value = newTransactions.length === pageSize.value
|
||||
} catch (err) {
|
||||
console.error('鍔犺浇浜ゆ槗璁板綍寮傚父:', err)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 鑾峰彇褰撳墠鐢ㄦ埛ID
|
||||
const getCurrentUserId = (): string => {
|
||||
const userStore = uni.getStorageSync('userInfo')
|
||||
return userStore?.id || ''
|
||||
}
|
||||
|
||||
// 鑾峰彇浜ゆ槗鍥炬爣
|
||||
const getTransactionIcon = (type: string): string => {
|
||||
const icons: Record<string, string> = {
|
||||
recharge: '馃挸',
|
||||
consume: '馃洅',
|
||||
withdraw: '馃彟',
|
||||
refund: '馃攧',
|
||||
reward: '馃巵',
|
||||
income: '馃挵',
|
||||
expense: '馃摛'
|
||||
}
|
||||
return icons[type] || '馃挵'
|
||||
}
|
||||
|
||||
// 鑾峰彇浜ゆ槗鏍囬
|
||||
const getTransactionTitle = (type: string): string => {
|
||||
const titles: Record<string, string> = {
|
||||
recharge: '璐︽埛鍏呭€?,
|
||||
consume: '鍟嗗搧娑堣垂',
|
||||
withdraw: '浣欓鎻愮幇',
|
||||
refund: '璁㈠崟閫€娆?,
|
||||
reward: '娲诲姩濂栧姳',
|
||||
income: '鏀跺叆',
|
||||
expense: '鏀嚭'
|
||||
}
|
||||
return titles[type] || '浜ゆ槗'
|
||||
}
|
||||
|
||||
// 鏍煎紡鍖栨椂闂?
|
||||
const formatTime = (timeStr: string): string => {
|
||||
const date = new Date(timeStr)
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
||||
const day = date.getDate().toString().padStart(2, '0')
|
||||
const hours = date.getHours().toString().padStart(2, '0')
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0')
|
||||
return `${month}-${day} ${hours}:${minutes}`
|
||||
}
|
||||
|
||||
// 鏄剧ず鏇村鎿嶄綔
|
||||
const showMoreActions = () => {
|
||||
uni.showActionSheet({
|
||||
itemList: ['浜ゆ槗璁板綍', '瀹夊叏璁剧疆', '甯姪涓績'],
|
||||
success: (res) => {
|
||||
switch (res.tapIndex) {
|
||||
case 0:
|
||||
// 浜ゆ槗璁板綍宸茬粡鍦ㄥ綋鍓嶉〉
|
||||
break
|
||||
case 1:
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/consumer/settings'
|
||||
})
|
||||
break
|
||||
case 2:
|
||||
uni.navigateTo({
|
||||
url: '/pages/info/help'
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 鍏呭€?
|
||||
const recharge = () => {
|
||||
showRechargePopup.value = true
|
||||
rechargeAmount.value = ''
|
||||
}
|
||||
|
||||
// 鎻愮幇
|
||||
const withdraw = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/consumer/withdraw'
|
||||
})
|
||||
}
|
||||
|
||||
// 璺宠浆鍒颁紭鎯犲埜
|
||||
const goToCoupons = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/consumer/coupons'
|
||||
})
|
||||
}
|
||||
|
||||
// 璺宠浆鍒扮孩鍖?
|
||||
const goToRedPackets = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/consumer/red-packets'
|
||||
})
|
||||
}
|
||||
|
||||
// 璺宠浆鍒扮Н鍒?
|
||||
const goToPoints = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/consumer/points'
|
||||
})
|
||||
}
|
||||
|
||||
// 璺宠浆鍒伴摱琛屽崱
|
||||
const goToBankCards = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/consumer/bank-cards'
|
||||
})
|
||||
}
|
||||
|
||||
// 鍒囨崲杩囨护鍣?
|
||||
const changeFilter = (filter: string) => {
|
||||
activeFilter.value = filter
|
||||
}
|
||||
|
||||
// 鍔犺浇鏇村
|
||||
const loadMore = () => {
|
||||
if (hasMore.value && !isLoading.value) {
|
||||
loadTransactions(true)
|
||||
}
|
||||
}
|
||||
|
||||
// 閫夋嫨蹇嵎閲戦
|
||||
const selectQuickAmount = (amount: number) => {
|
||||
rechargeAmount.value = amount.toString()
|
||||
}
|
||||
|
||||
// 纭鍏呭€?
|
||||
const confirmRecharge = async () => {
|
||||
if (!canRecharge.value) return
|
||||
|
||||
const amount = parseFloat(rechargeAmount.value)
|
||||
if (isNaN(amount)) return
|
||||
|
||||
// 杩欓噷搴旇璺宠浆鍒版敮浠橀〉闈㈣繘琛屽厖鍊?
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/consumer/payment?type=recharge&amount=${amount}`
|
||||
})
|
||||
|
||||
closeRechargePopup()
|
||||
}
|
||||
|
||||
// 鍏抽棴鍏呭€煎脊绐?
|
||||
const closeRechargePopup = () => {
|
||||
showRechargePopup.value = false
|
||||
rechargeAmount.value = ''
|
||||
}
|
||||
|
||||
// 杩斿洖
|
||||
const goBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wallet-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.wallet-header {
|
||||
background-color: #ffffff;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
font-size: 24px;
|
||||
color: #333333;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.more-btn {
|
||||
color: #333333;
|
||||
font-size: 20px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.wallet-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.balance-overview {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 30px 20px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.balance-label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
opacity: 0.9;
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.balance-value {
|
||||
display: block;
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.balance-actions {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
height: 40px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.action-btn.recharge {
|
||||
background-color: #ffffff;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.action-btn.withdraw {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
color: #ffffff;
|
||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.assets-stats {
|
||||
background-color: #ffffff;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #666666;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
background-color: #ffffff;
|
||||
margin-top: 10px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.action-grid {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.action-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
font-size: 28px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: 12px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.transactions-section {
|
||||
background-color: #ffffff;
|
||||
margin-top: 10px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.filter-tabs {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.filter-tab {
|
||||
font-size: 14px;
|
||||
color: #666666;
|
||||
padding: 5px 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.filter-tab.active {
|
||||
color: #007aff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.filter-tab.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background-color: #007aff;
|
||||
}
|
||||
|
||||
.empty-transactions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 60px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 16px;
|
||||
color: #666666;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.empty-subtext {
|
||||
font-size: 14px;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.transactions-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.transaction-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
padding: 15px 0;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
}
|
||||
|
||||
.transaction-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.transaction-left {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.transaction-icon {
|
||||
font-size: 24px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.transaction-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.transaction-title {
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.transaction-time {
|
||||
font-size: 12px;
|
||||
color: #999999;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.transaction-remark {
|
||||
font-size: 12px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.transaction-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.transaction-amount {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.transaction-amount.income {
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.transaction-amount.expense {
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.transaction-balance {
|
||||
font-size: 12px;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.loading-more,
|
||||
.no-more {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading-text,
|
||||
.no-more-text {
|
||||
color: #999999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.security-tips {
|
||||
background-color: #ffffff;
|
||||
margin-top: 10px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.tip-title {
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.tip-item {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #666666;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.tip-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.recharge-popup {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.popup-mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.popup-content {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: #ffffff;
|
||||
border-top-left-radius: 15px;
|
||||
border-top-right-radius: 15px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.popup-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.popup-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.popup-close {
|
||||
font-size: 24px;
|
||||
color: #999999;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.popup-body {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.amount-label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.amount-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding: 10px;
|
||||
border: 1px solid #e5e5e5;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.currency-symbol {
|
||||
font-size: 20px;
|
||||
color: #333333;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.amount-field {
|
||||
flex: 1;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.quick-amounts {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.quick-amount {
|
||||
padding: 8px 15px;
|
||||
border: 1px solid #e5e5e5;
|
||||
border-radius: 15px;
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.quick-amount.active {
|
||||
background-color: #007aff;
|
||||
color: #ffffff;
|
||||
border-color: #007aff;
|
||||
}
|
||||
|
||||
.recharge-tip {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.popup-footer {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.cancel-btn,
|
||||
.confirm-btn {
|
||||
flex: 1;
|
||||
height: 45px;
|
||||
border-radius: 22.5px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background-color: #f5f5f5;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
background-color: #007aff;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.confirm-btn.disabled {
|
||||
background-color: #cccccc;
|
||||
opacity: 0.6;
|
||||
}
|
||||
</style>
|
||||
|
||||
985
doc_mall/consumer/backup_pages/wallett.uvue
Normal file
985
doc_mall/consumer/backup_pages/wallett.uvue
Normal file
@@ -0,0 +1,985 @@
|
||||
<!-- 閽卞寘椤甸潰 -->
|
||||
<template>
|
||||
<view class="wallet-page">
|
||||
<!-- 椤堕儴鏍?-->
|
||||
<!--<view class="wallet-header">
|
||||
<text class="back-btn" @click="goBack">鈥?/text>
|
||||
</view>-->
|
||||
|
||||
<scroll-view class="wallet-content" scroll-y>
|
||||
<!-- 浣欓姒傝 -->
|
||||
<view class="balance-overview">
|
||||
<text class="balance-label">璐︽埛浣欓</text>
|
||||
<text class="balance-value">楼{{ balance.toFixed(2) }}</text>
|
||||
<view class="balance-actions">
|
||||
<button class="action-btn recharge" @click="recharge">鍏呭€?/button>
|
||||
<button class="action-btn withdraw" @click="withdraw">鎻愮幇</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 璧勪骇缁熻 -->
|
||||
<view class="assets-stats">
|
||||
<view class="stat-item">
|
||||
<text class="stat-label">绱鍏呭€?/text>
|
||||
<text class="stat-value">楼{{ stats.totalRecharge.toFixed(2) }}</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-label">绱娑堣垂</text>
|
||||
<text class="stat-value">楼{{ stats.totalConsume.toFixed(2) }}</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-label">绱鎻愮幇</text>
|
||||
<text class="stat-value">楼{{ stats.totalWithdraw.toFixed(2) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 蹇嵎鍔熻兘 -->
|
||||
<view class="quick-actions">
|
||||
<view class="action-grid">
|
||||
<view class="action-item" @click="goToCoupons">
|
||||
<text class="action-icon">馃帿</text>
|
||||
<text class="action-text">浼樻儬鍒?/text>
|
||||
</view>
|
||||
<view class="action-item" @click="goToRedPackets">
|
||||
<text class="action-icon">馃Ё</text>
|
||||
<text class="action-text">绾㈠寘</text>
|
||||
</view>
|
||||
<view class="action-item" @click="goToPoints">
|
||||
<text class="action-icon">猸?/text>
|
||||
<text class="action-text">绉垎</text>
|
||||
</view>
|
||||
<view class="action-item" @click="goToBankCards">
|
||||
<text class="action-icon">馃挸</text>
|
||||
<text class="action-text">閾惰鍗?/text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 浜ゆ槗璁板綍 -->
|
||||
<view class="transactions-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">浜ゆ槗璁板綍</text>
|
||||
<view class="filter-tabs">
|
||||
<text :class="['filter-tab', { active: activeFilter === 'all' }]"
|
||||
@click="changeFilter('all')">鍏ㄩ儴</text>
|
||||
<text :class="['filter-tab', { active: activeFilter === 'income' }]"
|
||||
@click="changeFilter('income')">鏀跺叆</text>
|
||||
<text :class="['filter-tab', { active: activeFilter === 'expense' }]"
|
||||
@click="changeFilter('expense')">鏀嚭</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 绌虹姸鎬?-->
|
||||
<view v-if="transactions.length === 0 && !isLoading" class="empty-transactions">
|
||||
<text class="empty-icon">馃挵</text>
|
||||
<text class="empty-text">鏆傛棤浜ゆ槗璁板綍</text>
|
||||
<text class="empty-subtext">蹇幓浣跨敤閽卞寘鍔熻兘鍚?/text>
|
||||
</view>
|
||||
|
||||
<!-- 浜ゆ槗鍒楄〃 -->
|
||||
<view class="transactions-list">
|
||||
<view v-for="transaction in transactions"
|
||||
:key="transaction.id"
|
||||
class="transaction-item">
|
||||
<view class="transaction-left">
|
||||
<text class="transaction-icon">{{ getTransactionIcon(transaction.type) }}</text>
|
||||
<view class="transaction-info">
|
||||
<text class="transaction-title">{{ getTransactionTitle(transaction.type) }}</text>
|
||||
<text class="transaction-time">{{ formatTime(transaction.created_at) }}</text>
|
||||
<text v-if="transaction.remark" class="transaction-remark">{{ transaction.remark }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="transaction-right">
|
||||
<text :class="['transaction-amount',
|
||||
{ income: transaction.amount > 0, expense: transaction.amount < 0 }]">
|
||||
{{ transaction.amount > 0 ? '+' : '' }}楼{{ Math.abs(transaction.amount).toFixed(2) }}
|
||||
</text>
|
||||
<text class="transaction-balance">浣欓: 楼{{ transaction.current_balance.toFixed(2) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 鍔犺浇鏇村 -->
|
||||
<view v-if="isLoading" class="loading-more">
|
||||
<text class="loading-text">鍔犺浇涓?..</text>
|
||||
</view>
|
||||
<view v-if="!hasMore && transactions.length > 0" class="no-more">
|
||||
<text class="no-more-text">娌℃湁鏇村璁板綍浜?/text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 瀹夊叏鎻愮ず -->
|
||||
<view class="security-tips">
|
||||
<text class="tip-title">瀹夊叏鎻愮ず</text>
|
||||
<text class="tip-item">1. 璇峰Ε鍠勪繚绠℃偍鐨勬敮浠樺瘑鐮?/text>
|
||||
<text class="tip-item">2. 涓嶈鍚戜粬浜洪€忛湶鎮ㄧ殑璐︽埛淇℃伅</text>
|
||||
<text class="tip-item">3. 瀹氭湡淇敼瀵嗙爜浠ョ‘淇濊处鎴峰畨鍏?/text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 鍏呭€煎脊绐?-->
|
||||
<view v-if="showRechargePopup" class="recharge-popup">
|
||||
<view class="popup-mask" @click="closeRechargePopup"></view>
|
||||
<view class="popup-content">
|
||||
<view class="popup-header">
|
||||
<text class="popup-title">鍏呭€?/text>
|
||||
<text class="popup-close" @click="closeRechargePopup">脳</text>
|
||||
</view>
|
||||
<view class="popup-body">
|
||||
<text class="amount-label">鍏呭€奸噾棰?/text>
|
||||
<view class="amount-input">
|
||||
<text class="currency-symbol">楼</text>
|
||||
<input class="amount-field"
|
||||
v-model="rechargeAmount"
|
||||
type="number"
|
||||
placeholder="璇疯緭鍏ュ厖鍊奸噾棰?
|
||||
focus />
|
||||
</view>
|
||||
<view class="quick-amounts">
|
||||
<text v-for="amount in quickAmounts"
|
||||
:key="amount"
|
||||
:class="['quick-amount', { active: rechargeAmount === amount.toString() }]"
|
||||
@click="selectQuickAmount(amount)">
|
||||
楼{{ amount }}
|
||||
</text>
|
||||
</view>
|
||||
<text class="recharge-tip">鍗曠瑪鍏呭€兼渶浣?0鍏冿紝鏈€楂?000鍏?/text>
|
||||
</view>
|
||||
<view class="popup-footer">
|
||||
<button class="cancel-btn" @click="closeRechargePopup">鍙栨秷</button>
|
||||
<button class="confirm-btn"
|
||||
:class="{ disabled: !canRecharge }"
|
||||
@click="confirmRecharge">
|
||||
纭鍏呭€?
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted, computed, watch } from 'vue'
|
||||
//import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
type WalletType = {
|
||||
id: string
|
||||
user_id: string
|
||||
balance: number
|
||||
total_recharge: number
|
||||
total_consume: number
|
||||
total_withdraw: number
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
type TransactionType = {
|
||||
id: string
|
||||
user_id: string
|
||||
change_amount: number
|
||||
current_balance: number
|
||||
change_type: string // 'recharge' | 'consume' | 'withdraw' | 'refund' | 'reward'
|
||||
related_id: string | null
|
||||
remark: string | null
|
||||
created_at: string
|
||||
}
|
||||
|
||||
type StatsType = {
|
||||
totalRecharge: number
|
||||
totalConsume: number
|
||||
totalWithdraw: number
|
||||
}
|
||||
|
||||
const balance = ref<number>(0)
|
||||
const stats = ref<StatsType>({
|
||||
totalRecharge: 0,
|
||||
totalConsume: 0,
|
||||
totalWithdraw: 0
|
||||
})
|
||||
const transactions = ref<Array<TransactionType>>([])
|
||||
const activeFilter = ref<string>('all')
|
||||
const isLoading = ref<boolean>(false)
|
||||
const currentPage = ref<number>(1)
|
||||
const pageSize = ref<number>(20)
|
||||
const hasMore = ref<boolean>(true)
|
||||
const showRechargePopup = ref<boolean>(false)
|
||||
const rechargeAmount = ref<string>('')
|
||||
const quickAmounts = [50, 100, 200, 500, 1000]
|
||||
|
||||
// 璁$畻灞炴€?
|
||||
const canRecharge = computed(() => {
|
||||
const amount = parseFloat(rechargeAmount.value)
|
||||
return !isNaN(amount) && amount >= 10 && amount <= 5000
|
||||
})
|
||||
|
||||
// 鐩戝惉杩囨护鍣ㄥ彉鍖?
|
||||
watch(activeFilter, () => {
|
||||
resetTransactions()
|
||||
loadTransactions()
|
||||
})
|
||||
|
||||
// 鐢熷懡鍛ㄦ湡
|
||||
onMounted(() => {
|
||||
loadWalletData()
|
||||
})
|
||||
|
||||
// 閲嶇疆浜ゆ槗璁板綍
|
||||
const resetTransactions = () => {
|
||||
transactions.value = []
|
||||
currentPage.value = 1
|
||||
hasMore.value = true
|
||||
}
|
||||
|
||||
// 鍔犺浇閽卞寘鏁版嵁
|
||||
const loadWalletData = async () => {
|
||||
const userId = getCurrentUserId()
|
||||
if (!userId) {
|
||||
uni.navigateTo({
|
||||
url: '/pages/user/login'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
loadBalance(),
|
||||
loadTransactions()
|
||||
])
|
||||
}
|
||||
|
||||
// 鍔犺浇浣欓淇℃伅
|
||||
const loadBalance = async () => {
|
||||
const userId = getCurrentUserId()
|
||||
if (!userId) return
|
||||
|
||||
try {
|
||||
const { data, error } = await supa
|
||||
.from('user_wallets')
|
||||
.select('*')
|
||||
.eq('user_id', userId)
|
||||
.single()
|
||||
|
||||
if (error !== null) {
|
||||
console.error('鍔犺浇閽卞寘澶辫触:', error)
|
||||
return
|
||||
}
|
||||
|
||||
if (data) {
|
||||
balance.value = data.balance || 0
|
||||
stats.value = {
|
||||
totalRecharge: data.total_recharge || 0,
|
||||
totalConsume: data.total_consume || 0,
|
||||
totalWithdraw: data.total_withdraw || 0
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('鍔犺浇閽卞寘寮傚父:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 鍔犺浇浜ゆ槗璁板綍
|
||||
const loadTransactions = async (loadMore: boolean = false) => {
|
||||
if (isLoading.value || (!hasMore.value && loadMore)) {
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
const userId = getCurrentUserId()
|
||||
if (!userId) return
|
||||
|
||||
const page = loadMore ? currentPage.value + 1 : 1
|
||||
|
||||
let query = supa
|
||||
.from('balance_records')
|
||||
.select('*')
|
||||
.eq('user_id', userId)
|
||||
.order('created_at', { ascending: false })
|
||||
|
||||
// 鏍规嵁杩囨护鍣ㄧ瓫閫?
|
||||
if (activeFilter.value === 'income') {
|
||||
query = query.gt('change_amount', 0)
|
||||
} else if (activeFilter.value === 'expense') {
|
||||
query = query.lt('change_amount', 0)
|
||||
}
|
||||
|
||||
// 鍒嗛〉
|
||||
query = query.range((page - 1) * pageSize.value, page * pageSize.value - 1)
|
||||
|
||||
const { data, error } = await query
|
||||
|
||||
if (error !== null) {
|
||||
console.error('鍔犺浇浜ゆ槗璁板綍澶辫触:', error)
|
||||
return
|
||||
}
|
||||
|
||||
const newTransactions = data || []
|
||||
|
||||
if (loadMore) {
|
||||
transactions.value.push(...newTransactions)
|
||||
currentPage.value = page
|
||||
} else {
|
||||
transactions.value = newTransactions
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
hasMore.value = newTransactions.length === pageSize.value
|
||||
} catch (err) {
|
||||
console.error('鍔犺浇浜ゆ槗璁板綍寮傚父:', err)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 鑾峰彇褰撳墠鐢ㄦ埛ID
|
||||
const getCurrentUserId = (): string => {
|
||||
const userStore = uni.getStorageSync('userInfo')
|
||||
return userStore?.id || ''
|
||||
}
|
||||
|
||||
// 鑾峰彇浜ゆ槗鍥炬爣
|
||||
const getTransactionIcon = (type: string): string => {
|
||||
const icons: Record<string, string> = {
|
||||
recharge: '馃挸',
|
||||
consume: '馃洅',
|
||||
withdraw: '馃彟',
|
||||
refund: '馃攧',
|
||||
reward: '馃巵',
|
||||
income: '馃挵',
|
||||
expense: '馃摛'
|
||||
}
|
||||
return icons[type] || '馃挵'
|
||||
}
|
||||
|
||||
// 鑾峰彇浜ゆ槗鏍囬
|
||||
const getTransactionTitle = (type: string): string => {
|
||||
const titles: Record<string, string> = {
|
||||
recharge: '璐︽埛鍏呭€?,
|
||||
consume: '鍟嗗搧娑堣垂',
|
||||
withdraw: '浣欓鎻愮幇',
|
||||
refund: '璁㈠崟閫€娆?,
|
||||
reward: '娲诲姩濂栧姳',
|
||||
income: '鏀跺叆',
|
||||
expense: '鏀嚭'
|
||||
}
|
||||
return titles[type] || '浜ゆ槗'
|
||||
}
|
||||
|
||||
// 鏍煎紡鍖栨椂闂?
|
||||
const formatTime = (timeStr: string): string => {
|
||||
const date = new Date(timeStr)
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
||||
const day = date.getDate().toString().padStart(2, '0')
|
||||
const hours = date.getHours().toString().padStart(2, '0')
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0')
|
||||
return `${month}-${day} ${hours}:${minutes}`
|
||||
}
|
||||
|
||||
// 鏄剧ず鏇村鎿嶄綔
|
||||
const showMoreActions = () => {
|
||||
uni.showActionSheet({
|
||||
itemList: ['浜ゆ槗璁板綍', '瀹夊叏璁剧疆', '甯姪涓績'],
|
||||
success: (res) => {
|
||||
switch (res.tapIndex) {
|
||||
case 0:
|
||||
// 浜ゆ槗璁板綍宸茬粡鍦ㄥ綋鍓嶉〉
|
||||
break
|
||||
case 1:
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/consumer/settings'
|
||||
})
|
||||
break
|
||||
case 2:
|
||||
uni.navigateTo({
|
||||
url: '/pages/info/help'
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 鍏呭€?
|
||||
const recharge = () => {
|
||||
showRechargePopup.value = true
|
||||
rechargeAmount.value = ''
|
||||
}
|
||||
|
||||
// 鎻愮幇
|
||||
const withdraw = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/consumer/withdraw'
|
||||
})
|
||||
}
|
||||
|
||||
// 璺宠浆鍒颁紭鎯犲埜
|
||||
const goToCoupons = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/consumer/coupons'
|
||||
})
|
||||
}
|
||||
|
||||
// 璺宠浆鍒扮孩鍖?
|
||||
const goToRedPackets = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/consumer/red-packets'
|
||||
})
|
||||
}
|
||||
|
||||
// 璺宠浆鍒扮Н鍒?
|
||||
const goToPoints = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/consumer/points'
|
||||
})
|
||||
}
|
||||
|
||||
// 璺宠浆鍒伴摱琛屽崱
|
||||
const goToBankCards = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/consumer/bank-cards'
|
||||
})
|
||||
}
|
||||
|
||||
// 鍒囨崲杩囨护鍣?
|
||||
const changeFilter = (filter: string) => {
|
||||
activeFilter.value = filter
|
||||
}
|
||||
|
||||
// 鍔犺浇鏇村
|
||||
const loadMore = () => {
|
||||
if (hasMore.value && !isLoading.value) {
|
||||
loadTransactions(true)
|
||||
}
|
||||
}
|
||||
|
||||
// 閫夋嫨蹇嵎閲戦
|
||||
const selectQuickAmount = (amount: number) => {
|
||||
rechargeAmount.value = amount.toString()
|
||||
}
|
||||
|
||||
// 纭鍏呭€?
|
||||
const confirmRecharge = async () => {
|
||||
if (!canRecharge.value) return
|
||||
|
||||
const amount = parseFloat(rechargeAmount.value)
|
||||
if (isNaN(amount)) return
|
||||
|
||||
// 杩欓噷搴旇璺宠浆鍒版敮浠橀〉闈㈣繘琛屽厖鍊?
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/consumer/payment?type=recharge&amount=${amount}`
|
||||
})
|
||||
|
||||
closeRechargePopup()
|
||||
}
|
||||
|
||||
// 鍏抽棴鍏呭€煎脊绐?
|
||||
const closeRechargePopup = () => {
|
||||
showRechargePopup.value = false
|
||||
rechargeAmount.value = ''
|
||||
}
|
||||
|
||||
// 杩斿洖
|
||||
const goBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 鍝嶅簲寮忓竷灞€浼樺寲 */
|
||||
@media screen and (min-width: 768px) {
|
||||
.wallet-content {
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.balance-overview {
|
||||
border-radius: 12px;
|
||||
margin-bottom: 20px;
|
||||
max-width: 800px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.assets-stats, .quick-actions, .transactions-section, .security-tips {
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
max-width: 800px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.popup-content {
|
||||
width: 400px;
|
||||
left: 50%;
|
||||
bottom: 50%;
|
||||
transform: translate(-50%, 50%);
|
||||
border-radius: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1024px) {
|
||||
.wallet-page {
|
||||
flex-direction: row; /* 澶у睆涓嬫敼涓烘í鍚戝竷灞€ */
|
||||
}
|
||||
|
||||
.wallet-header {
|
||||
display: none; /* 澶у睆涓嬮殣钘忛《閮ㄦ爮 */
|
||||
}
|
||||
|
||||
.wallet-content {
|
||||
width: 100%;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.wallet-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.wallet-header {
|
||||
background-color: #ffffff;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
font-size: 24px;
|
||||
color: #333333;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.wallet-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.balance-overview {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 30px 20px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.balance-label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
opacity: 0.9;
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.balance-value {
|
||||
display: block;
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.balance-actions {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
height: 40px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.action-btn.recharge {
|
||||
background-color: #ffffff;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.action-btn.withdraw {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
color: #ffffff;
|
||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.assets-stats {
|
||||
background-color: #ffffff;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #666666;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
background-color: #ffffff;
|
||||
margin-top: 10px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.action-grid {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.action-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
font-size: 28px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: 12px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.transactions-section {
|
||||
background-color: #ffffff;
|
||||
margin-top: 10px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.filter-tabs {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.filter-tab {
|
||||
font-size: 14px;
|
||||
color: #666666;
|
||||
padding: 5px 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.filter-tab.active {
|
||||
color: #007aff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.filter-tab.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background-color: #007aff;
|
||||
}
|
||||
|
||||
.empty-transactions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 60px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 16px;
|
||||
color: #666666;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.empty-subtext {
|
||||
font-size: 14px;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.transactions-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.transaction-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
padding: 15px 0;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
}
|
||||
|
||||
.transaction-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.transaction-left {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.transaction-icon {
|
||||
font-size: 24px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.transaction-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.transaction-title {
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.transaction-time {
|
||||
font-size: 12px;
|
||||
color: #999999;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.transaction-remark {
|
||||
font-size: 12px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.transaction-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.transaction-amount {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.transaction-amount.income {
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.transaction-amount.expense {
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.transaction-balance {
|
||||
font-size: 12px;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.loading-more,
|
||||
.no-more {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading-text,
|
||||
.no-more-text {
|
||||
color: #999999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.security-tips {
|
||||
background-color: #ffffff;
|
||||
margin-top: 10px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.tip-title {
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.tip-item {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #666666;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.tip-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.recharge-popup {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.popup-mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.popup-content {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: #ffffff;
|
||||
border-top-left-radius: 15px;
|
||||
border-top-right-radius: 15px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.popup-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.popup-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.popup-close {
|
||||
font-size: 24px;
|
||||
color: #999999;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.popup-body {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.amount-label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.amount-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding: 10px;
|
||||
border: 1px solid #e5e5e5;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.currency-symbol {
|
||||
font-size: 20px;
|
||||
color: #333333;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.amount-field {
|
||||
flex: 1;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.quick-amounts {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.quick-amount {
|
||||
padding: 8px 15px;
|
||||
border: 1px solid #e5e5e5;
|
||||
border-radius: 15px;
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.quick-amount.active {
|
||||
background-color: #007aff;
|
||||
color: #ffffff;
|
||||
border-color: #007aff;
|
||||
}
|
||||
|
||||
.recharge-tip {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.popup-footer {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.cancel-btn,
|
||||
.confirm-btn {
|
||||
flex: 1;
|
||||
height: 45px;
|
||||
border-radius: 22.5px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background-color: #f5f5f5;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
background-color: #007aff;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.confirm-btn.disabled {
|
||||
background-color: #cccccc;
|
||||
opacity: 0.6;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user