consumer模块完成度85%,测试连接supabase
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
// Supabase 配置
|
// Supabase 配置
|
||||||
// 内网环境 - 本地部署的 Supabase
|
// 内网环境 - 本地部署的 Supabase
|
||||||
// IP: 192.168.1.63
|
// IP: 192.168.1.61 (Ubuntu服务器)
|
||||||
// Kong HTTP Port: 8000
|
// Kong HTTP Port: 8000
|
||||||
export const SUPA_URL: string = 'http://192.168.1.63:8000'
|
export const SUPA_URL: string = 'http://192.168.1.61:8000'
|
||||||
export const SUPA_KEY: string = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzY4ODMwNjI0LCJleHAiOjE5MjY1MTA2MjR9.mDVl-kIOdRK9v6VTxo0TDF8r7X7xk3PZXazaavHyVvg'
|
export const SUPA_KEY: string = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzY4ODMwNjI0LCJleHAiOjE5MjY1MTA2MjR9.mDVl-kIOdRK9v6VTxo0TDF8r7X7xk3PZXazaavHyVvg'
|
||||||
|
|
||||||
// WebSocket 实时连接(内网使用 ws:// 而非 wss://)
|
// WebSocket 实时连接(内网使用 ws:// 而非 wss://)
|
||||||
export const WS_URL: string = 'ws://192.168.1.63:8000/realtime/v1/websocket'
|
export const WS_URL: string = 'ws://192.168.1.61:8000/realtime/v1/websocket'
|
||||||
|
|
||||||
// 备用配置(已注释,如需切换可取消注释)
|
// 备用配置(已注释,如需切换可取消注释)
|
||||||
// 开发环境 - 其他内网地址
|
// 开发环境 - 其他内网地址
|
||||||
|
|||||||
247
doc_mall/SUPABASE_DATA_MIGRATION_GUIDE.md
Normal file
247
doc_mall/SUPABASE_DATA_MIGRATION_GUIDE.md
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
# Supabase 数据迁移指南
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
本指南将帮助您将当前使用模拟数据的 uni-app 项目迁移到使用 Supabase 数据库。您的项目已经配置了连接到 Ubuntu 服务器上的 Supabase(IP: 192.168.1.61),现在需要创建数据库表并插入测试数据,然后修改前端代码以使用真实数据。
|
||||||
|
|
||||||
|
## 第一步:在 Supabase 中创建数据库表
|
||||||
|
|
||||||
|
### 方法一:通过 Supabase Dashboard 执行 SQL
|
||||||
|
|
||||||
|
1. **打开 Supabase Dashboard**
|
||||||
|
- 访问:http://192.168.1.61:3000
|
||||||
|
- 使用 Dashboard 用户名和密码登录(位于 `supabase_pro/.env` 中的 `DASHBOARD_USERNAME` 和 `DASHBOARD_PASSWORD`)
|
||||||
|
|
||||||
|
2. **进入 SQL Editor**
|
||||||
|
- 在左侧菜单中点击 "SQL Editor"
|
||||||
|
- 点击 "New query" 创建新查询
|
||||||
|
|
||||||
|
3. **执行建表脚本**
|
||||||
|
- 复制 `sql/001_create_tables.sql` 文件中的全部内容
|
||||||
|
- 粘贴到 SQL Editor 中
|
||||||
|
- 点击 "Run" 执行
|
||||||
|
|
||||||
|
4. **执行插入数据脚本**
|
||||||
|
- 复制 `sql/002_insert_test_data.sql` 文件中的全部内容
|
||||||
|
- 粘贴到 SQL Editor 中
|
||||||
|
- 点击 "Run" 执行
|
||||||
|
|
||||||
|
### 方法二:通过命令行执行(如果 Supabase 运行在 Ubuntu 服务器上)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 登录到 Ubuntu 服务器
|
||||||
|
ssh hfkj@192.168.1.61
|
||||||
|
|
||||||
|
# 进入 Supabase 项目目录(假设 Supabase 安装在默认位置)
|
||||||
|
cd ~/supabase
|
||||||
|
|
||||||
|
# 使用 psql 连接到数据库执行 SQL 脚本
|
||||||
|
# 注意:需要知道数据库密码(位于 supabase_pro/.env 中的 POSTGRES_PASSWORD)
|
||||||
|
PGPASSWORD=yxyHINygZMLSq9jLddrZQBB-CoyGHSF5DwlwWmbrYXc psql -h localhost -U postgres -d postgres -f /path/to/001_create_tables.sql
|
||||||
|
PGPASSWORD=yxyHINygZMLSq9jLddrZQBB-CoyGHSF5DwlwWmbrYXc psql -h localhost -U postgres -d postgres -f /path/to/002_insert_test_data.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
## 第二步:验证数据表创建成功
|
||||||
|
|
||||||
|
### 在 Supabase Dashboard 中验证
|
||||||
|
|
||||||
|
1. **查看 Tables**
|
||||||
|
- 在左侧菜单中点击 "Table Editor"
|
||||||
|
- 应该能看到 `categories` 和 `products` 表
|
||||||
|
|
||||||
|
2. **查看数据**
|
||||||
|
- 点击 `categories` 表,应该能看到 10 条分类数据
|
||||||
|
- 点击 `products` 表,应该能看到 18 条商品数据
|
||||||
|
|
||||||
|
### 通过 API 验证
|
||||||
|
|
||||||
|
1. **测试分类 API**
|
||||||
|
```
|
||||||
|
GET http://192.168.1.61:8000/rest/v1/categories
|
||||||
|
Headers:
|
||||||
|
apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzY4ODMwNjI0LCJleHAiOjE5MjY1MTA2MjR9.mDVl-kIOdRK9v6VTxo0TDF8r7X7xk3PZXazaavHyVvg
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **测试商品 API**
|
||||||
|
```
|
||||||
|
GET http://192.168.1.61:8000/rest/v1/products?category_id=eq.cold
|
||||||
|
Headers:
|
||||||
|
apikey: [同上]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 第三步:修改前端代码使用真实数据
|
||||||
|
|
||||||
|
### 1. 已创建的服务文件
|
||||||
|
|
||||||
|
我已经创建了 `utils/supabaseService.uts` 文件,提供了以下功能:
|
||||||
|
- `getCategories()` - 获取所有分类
|
||||||
|
- `getProductsByCategory()` - 获取指定分类的商品
|
||||||
|
- `searchProducts()` - 搜索商品
|
||||||
|
- `getProductById()` - 获取单个商品详情
|
||||||
|
- `getHotProducts()` - 获取热销商品
|
||||||
|
- `getRecommendedProducts()` - 获取推荐商品
|
||||||
|
|
||||||
|
### 2. 修改分类页面 (`pages/mall/consumer/category.uvue`)
|
||||||
|
|
||||||
|
需要将硬编码的模拟数据替换为从 Supabase 获取的数据:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 在 script 部分添加导入
|
||||||
|
import supabaseService from '@/utils/supabaseService.uts'
|
||||||
|
import type { Category, Product } from '@/utils/supabaseService.uts'
|
||||||
|
|
||||||
|
// 替换 medicineCategories 的初始化
|
||||||
|
// 删除原有的 medicineCategories 数组定义
|
||||||
|
|
||||||
|
// 修改 onMounted 或创建新的生命周期函数
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadCategories()
|
||||||
|
await loadProducts()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 添加加载分类的方法
|
||||||
|
const loadCategories = async () => {
|
||||||
|
const categories = await supabaseService.getCategories()
|
||||||
|
if (categories.length > 0) {
|
||||||
|
primaryCategories.value = categories
|
||||||
|
// 设置默认选中第一个分类
|
||||||
|
if (!activePrimary.value && categories[0]) {
|
||||||
|
activePrimary.value = categories[0].id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改 selectPrimaryCategory 方法
|
||||||
|
const selectPrimaryCategory = async (categoryId: string) => {
|
||||||
|
activePrimary.value = categoryId
|
||||||
|
|
||||||
|
// 更新当前分类信息
|
||||||
|
const category = primaryCategories.value.find(cat => cat.id === categoryId)
|
||||||
|
if (category) {
|
||||||
|
currentCategoryName.value = category.name
|
||||||
|
currentCategoryDesc.value = category.description
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载对应商品
|
||||||
|
const response = await supabaseService.getProductsByCategory(categoryId)
|
||||||
|
productList.value = response.data
|
||||||
|
hasMore.value = response.hasmore
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 修改主页 (`pages/mall/consumer/index.uvue`)
|
||||||
|
|
||||||
|
如果主页显示商品,也需要修改为从 Supabase 获取:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import supabaseService from '@/utils/supabaseService.uts'
|
||||||
|
|
||||||
|
// 获取热销商品
|
||||||
|
const loadHotProducts = async () => {
|
||||||
|
hotProducts.value = await supabaseService.getHotProducts(6)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取推荐商品
|
||||||
|
const loadRecommendedProducts = async () => {
|
||||||
|
recommendedProducts.value = await supabaseService.getRecommendedProducts(6)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 第四步:测试连接和数据
|
||||||
|
|
||||||
|
### 1. 测试 Supabase 连接
|
||||||
|
|
||||||
|
创建一个测试页面或使用现有的页面测试连接:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 测试代码示例
|
||||||
|
const testConnection = async () => {
|
||||||
|
try {
|
||||||
|
const categories = await supabaseService.getCategories()
|
||||||
|
console.log('连接成功,获取到分类数:', categories.length)
|
||||||
|
uni.showToast({
|
||||||
|
title: `连接成功,获取到 ${categories.length} 个分类`,
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('连接失败:', error)
|
||||||
|
uni.showToast({
|
||||||
|
title: '连接失败,请检查配置',
|
||||||
|
icon: 'error'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 测试数据加载
|
||||||
|
|
||||||
|
在分类页面测试:
|
||||||
|
1. 打开分类页面
|
||||||
|
2. 检查分类列表是否显示
|
||||||
|
3. 点击不同分类,检查商品列表是否更新
|
||||||
|
4. 检查商品图片、价格等信息是否正确显示
|
||||||
|
|
||||||
|
### 3. 测试搜索功能
|
||||||
|
|
||||||
|
如果项目有搜索页面,测试搜索功能:
|
||||||
|
1. 输入关键字搜索
|
||||||
|
2. 检查返回的商品是否相关
|
||||||
|
|
||||||
|
## 第五步:处理图片 URL
|
||||||
|
|
||||||
|
### 当前情况
|
||||||
|
- 数据库中的 `image` 字段目前为空或使用本地路径
|
||||||
|
- 实际项目中,图片应该存储在 Supabase Storage 或 CDN
|
||||||
|
|
||||||
|
### 临时解决方案
|
||||||
|
在显示图片时,如果数据库中没有图片 URL,使用默认图片:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const getProductImage = (product: Product) => {
|
||||||
|
if (product.image && product.image.startsWith('http')) {
|
||||||
|
return product.image
|
||||||
|
}
|
||||||
|
return '/static/images/default-product.png'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 长期解决方案
|
||||||
|
1. 将图片上传到 Supabase Storage
|
||||||
|
2. 更新数据库中的 `image` 字段为完整 URL
|
||||||
|
|
||||||
|
## 常见问题解决
|
||||||
|
|
||||||
|
### 1. 连接超时或失败
|
||||||
|
- 检查 Ubuntu 服务器上的 Supabase 是否正常运行
|
||||||
|
- 检查防火墙设置,确保 8000 和 3000 端口可访问
|
||||||
|
- 检查 `ak/config.uts` 中的 IP 地址是否正确
|
||||||
|
|
||||||
|
### 2. 401 未授权错误
|
||||||
|
- 检查 `SUPA_KEY` 是否正确(与 `supabase_pro/.env` 中的 `ANON_KEY` 一致)
|
||||||
|
- 检查 Supabase 是否已启用匿名访问
|
||||||
|
|
||||||
|
### 3. 表不存在错误
|
||||||
|
- 确认已执行 SQL 脚本创建表
|
||||||
|
- 检查表名是否拼写正确(区分大小写)
|
||||||
|
|
||||||
|
### 4. 数据不显示
|
||||||
|
- 检查浏览器控制台是否有错误
|
||||||
|
- 检查网络请求是否成功
|
||||||
|
- 确认数据库中有数据
|
||||||
|
|
||||||
|
## 下一步优化建议
|
||||||
|
|
||||||
|
1. **添加加载状态**:在数据加载时显示加载动画
|
||||||
|
2. **错误处理**:添加更完善的错误处理和重试机制
|
||||||
|
3. **数据缓存**:使用本地存储缓存常用数据,减少网络请求
|
||||||
|
4. **分页加载**:实现滚动加载更多商品
|
||||||
|
5. **图片优化**:使用图片懒加载和压缩
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
通过以上步骤,您的项目将从使用模拟数据过渡到使用 Supabase 数据库数据。主要工作包括:
|
||||||
|
1. 在 Supabase 中创建表和插入测试数据
|
||||||
|
2. 修改前端代码使用新的服务层
|
||||||
|
3. 测试连接和数据加载
|
||||||
|
|
||||||
|
完成后,您的应用将具备完整的后端数据支持,为后续添加用户管理、购物车、订单等功能打下基础。
|
||||||
53
pages.json
53
pages.json
@@ -1,11 +1,12 @@
|
|||||||
{
|
{
|
||||||
"pages": [
|
"pages": [
|
||||||
{
|
{
|
||||||
"path": "pages/user/login",
|
"path": "pages/mall/consumer/index",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "用户登录",
|
"navigationBarTitleText": "首页",
|
||||||
"navigationStyle": "custom"
|
"navigationStyle": "custom",
|
||||||
}
|
"enablePullDownRefresh": true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/user/boot",
|
"path": "pages/user/boot",
|
||||||
@@ -44,11 +45,28 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/mall/consumer/index",
|
"path": "pages/user/login",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "首页",
|
"navigationBarTitleText": "用户登录",
|
||||||
"navigationStyle": "custom",
|
"navigationStyle": "custom"
|
||||||
"enablePullDownRefresh": true
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/user/change-password",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "修改密码"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/user/bind-phone",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "绑定手机"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/user/bind-email",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "绑定邮箱"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -75,9 +93,7 @@
|
|||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "我的"
|
"navigationBarTitleText": "我的"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
],
|
|
||||||
"subPackages": [
|
|
||||||
{
|
{
|
||||||
"path": "pages/mall/consumer/search",
|
"path": "pages/mall/consumer/search",
|
||||||
"style": {
|
"style": {
|
||||||
@@ -195,8 +211,21 @@
|
|||||||
"navigationBarTitleText": "客服聊天",
|
"navigationBarTitleText": "客服聊天",
|
||||||
"navigationStyle": "custom"
|
"navigationStyle": "custom"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/mall/consumer/settings",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "设置"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/mall/consumer/wallet",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "我的钱包"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"subPackages": [],
|
||||||
"tabBar": {
|
"tabBar": {
|
||||||
"color": "#999999",
|
"color": "#999999",
|
||||||
"selectedColor": "#ff5000",
|
"selectedColor": "#ff5000",
|
||||||
@@ -241,4 +270,4 @@
|
|||||||
"navigationBarBackgroundColor": "#FFFFFF",
|
"navigationBarBackgroundColor": "#FFFFFF",
|
||||||
"backgroundColor": "#F8F8F8"
|
"backgroundColor": "#F8F8F8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -482,8 +482,18 @@ const goShopping = () => {
|
|||||||
const navigateToProduct = (product: any) => {
|
const navigateToProduct = (product: any) => {
|
||||||
// 使用productId(如果存在)作为跳转的商品ID,否则使用id
|
// 使用productId(如果存在)作为跳转的商品ID,否则使用id
|
||||||
const productId = product.productId || product.id
|
const productId = product.productId || product.id
|
||||||
|
// 传递完整的参数,确保商品详情页能正确加载
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.append('id', productId)
|
||||||
|
params.append('productId', productId)
|
||||||
|
params.append('price', product.price?.toString() || '0')
|
||||||
|
// 商品详情页期望的参数名是originalPrice
|
||||||
|
params.append('originalPrice', (product.original_price || product.originalPrice || (product.price * 1.2).toFixed(2))?.toString())
|
||||||
|
params.append('name', encodeURIComponent(product.name || ''))
|
||||||
|
params.append('image', encodeURIComponent(product.image || '/static/product1.jpg'))
|
||||||
|
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: `/pages/mall/consumer/product-detail?id=${productId}&name=${encodeURIComponent(product.name)}&price=${product.price}&image=${encodeURIComponent(product.image)}`
|
url: `/pages/mall/consumer/product-detail?${params.toString()}`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -346,6 +346,7 @@
|
|||||||
|
|
||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { onShow } from '@dcloudio/uni-app'
|
||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
const statusBarHeight = ref(0)
|
const statusBarHeight = ref(0)
|
||||||
@@ -787,8 +788,20 @@ const addToCart = (product: any) => {
|
|||||||
const navigateToSearch = () => uni.navigateTo({ url: '/pages/mall/consumer/search' })
|
const navigateToSearch = () => uni.navigateTo({ url: '/pages/mall/consumer/search' })
|
||||||
const navigateToNews = () => uni.navigateTo({ url: '/pages/news/list' })
|
const navigateToNews = () => uni.navigateTo({ url: '/pages/news/list' })
|
||||||
const navigateToProduct = (product: any) => {
|
const navigateToProduct = (product: any) => {
|
||||||
uni.navigateTo({
|
// 使用productId(如果存在)作为跳转的商品ID,否则使用id
|
||||||
url: `/pages/mall/consumer/product-detail?productId=${product.id}&price=${product.price}&originalPrice=${product.originalPrice || ''}`
|
const productId = product.productId || product.id
|
||||||
|
// 传递完整的参数,确保商品详情页能正确加载
|
||||||
|
// 移除 URLSearchParams 内部的 encodeURIComponent,因为 append 会自动编码
|
||||||
|
// 或者直接构建 URL 字符串以确保兼容性
|
||||||
|
|
||||||
|
const name = product.name || ''
|
||||||
|
const image = product.image || '/static/product1.jpg'
|
||||||
|
const price = product.price?.toString() || '0'
|
||||||
|
const originalPrice = (product.original_price || product.originalPrice || (product.price * 1.2).toFixed(2))?.toString()
|
||||||
|
|
||||||
|
// 手动构建URL,避免双重编码问题
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/mall/consumer/product-detail?id=${productId}&productId=${productId}&price=${price}&originalPrice=${originalPrice}&name=${encodeURIComponent(name)}&image=${encodeURIComponent(image)}`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const navigateToCategory = (item: any) => {
|
const navigateToCategory = (item: any) => {
|
||||||
|
|||||||
@@ -156,7 +156,6 @@
|
|||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import { ref, reactive, onMounted, computed } from 'vue'
|
import { ref, reactive, onMounted, computed } from 'vue'
|
||||||
import { onShow, onLoad } from '@dcloudio/uni-app'
|
import { onShow, onLoad } from '@dcloudio/uni-app'
|
||||||
// // import supa from '@/components/supadb/aksupainstance.uts'
|
|
||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
const orders = ref<any[]>([])
|
const orders = ref<any[]>([])
|
||||||
@@ -365,6 +364,13 @@ const loadOrders = async () => {
|
|||||||
if (ordersStr) {
|
if (ordersStr) {
|
||||||
localOrders = JSON.parse(ordersStr as string) as any[]
|
localOrders = JSON.parse(ordersStr as string) as any[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果本地存储为空,使用 Mock 数据
|
||||||
|
if (localOrders.length === 0) {
|
||||||
|
localOrders = mockOrders
|
||||||
|
// 可选:将 Mock 数据写入本地存储,以便后续操作生效
|
||||||
|
// uni.setStorageSync('orders', JSON.stringify(mockOrders))
|
||||||
|
}
|
||||||
|
|
||||||
// 过滤当前用户的订单
|
// 过滤当前用户的订单
|
||||||
// const userOrders = localOrders.filter((o: any) => o.user_id === userId)
|
// const userOrders = localOrders.filter((o: any) => o.user_id === userId)
|
||||||
|
|||||||
@@ -153,9 +153,27 @@ export default {
|
|||||||
onLoad(options: any) {
|
onLoad(options: any) {
|
||||||
const productId = options.productId as string || options.id as string
|
const productId = options.productId as string || options.id as string
|
||||||
const productPrice = options.price ? parseFloat(options.price) : null
|
const productPrice = options.price ? parseFloat(options.price) : null
|
||||||
const productOriginalPrice = options.original_price ? parseFloat(options.original_price) : null
|
const productOriginalPrice = options.originalPrice ? parseFloat(options.originalPrice) : null
|
||||||
const productName = options.name as string
|
|
||||||
const productImage = options.image as string
|
// 处理商品名称:如果是编码的则解码,否则直接使用
|
||||||
|
let productName = options.name as string
|
||||||
|
if (productName) {
|
||||||
|
try {
|
||||||
|
// 尝试解码,如果失败(不是有效的URI组件)则使用原值
|
||||||
|
productName = decodeURIComponent(productName)
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('ProductName decode failed, using original:', productName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let productImage = options.image as string
|
||||||
|
if (productImage) {
|
||||||
|
try {
|
||||||
|
productImage = decodeURIComponent(productImage)
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('ProductImage decode failed, using original:', productImage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (productId) {
|
if (productId) {
|
||||||
this.loadProductDetail(productId, {
|
this.loadProductDetail(productId, {
|
||||||
@@ -166,6 +184,13 @@ export default {
|
|||||||
})
|
})
|
||||||
this.checkFavoriteStatus(productId)
|
this.checkFavoriteStatus(productId)
|
||||||
this.saveFootprint(productId)
|
this.saveFootprint(productId)
|
||||||
|
|
||||||
|
// 设置导航栏标题为商品名称
|
||||||
|
if (productName) {
|
||||||
|
uni.setNavigationBarTitle({
|
||||||
|
title: productName
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -210,7 +235,6 @@ export default {
|
|||||||
if (footprints.length > 50) {
|
if (footprints.length > 50) {
|
||||||
footprints = footprints.slice(0, 50)
|
footprints = footprints.slice(0, 50)
|
||||||
}
|
}
|
||||||
|
|
||||||
uni.setStorageSync('footprints', JSON.stringify(footprints))
|
uni.setStorageSync('footprints', JSON.stringify(footprints))
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="settings-page">
|
<view class="settings-page">
|
||||||
<!-- 顶部栏 -->
|
<!-- 顶部栏 -->
|
||||||
<!--<view class="settings-header">
|
<!--<view class="settings-header" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||||
<text class="back-btn" @click="goBack">‹</text>
|
<text class="back-btn" @click="goBack">‹</text>
|
||||||
<text class="header-title">设置</text>
|
<text class="header-title">设置</text>
|
||||||
</view>-->
|
</view>-->
|
||||||
@@ -192,8 +192,20 @@
|
|||||||
|
|
||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { onBackPress } from '@dcloudio/uni-app'
|
||||||
// import supa from '@/components/supadb/aksupainstance.uts'
|
// import supa from '@/components/supadb/aksupainstance.uts'
|
||||||
|
|
||||||
|
// 拦截返回事件,强制跳转到个人中心页
|
||||||
|
onBackPress((options) => {
|
||||||
|
// 无论是什么触发的返回(系统返回键或导航栏返回按钮),都跳转到profile
|
||||||
|
// 注意:onBackPress 只能在 page 中使用,component 中无效
|
||||||
|
uni.switchTab({
|
||||||
|
url: '/pages/mall/consumer/profile'
|
||||||
|
})
|
||||||
|
// 返回 true 表示阻止默认返回行为
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
type UserType = {
|
type UserType = {
|
||||||
id: string
|
id: string
|
||||||
phone: string | null
|
phone: string | null
|
||||||
@@ -236,8 +248,12 @@ const currentLanguage = ref<string>('简体中文')
|
|||||||
const currentTheme = ref<string>('自动')
|
const currentTheme = ref<string>('自动')
|
||||||
const appVersion = ref<string>('1.0.0')
|
const appVersion = ref<string>('1.0.0')
|
||||||
|
|
||||||
|
const statusBarHeight = ref(0)
|
||||||
|
|
||||||
// 生命周期
|
// 生命周期
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
const systemInfo = uni.getSystemInfoSync()
|
||||||
|
statusBarHeight.value = systemInfo.statusBarHeight || 0
|
||||||
loadUserInfo()
|
loadUserInfo()
|
||||||
loadSettings()
|
loadSettings()
|
||||||
})
|
})
|
||||||
@@ -564,11 +580,6 @@ const deleteAccount = () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 返回
|
|
||||||
const goBack = () => {
|
|
||||||
uni.navigateBack()
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -7,113 +7,121 @@
|
|||||||
</view>-->
|
</view>-->
|
||||||
|
|
||||||
<scroll-view class="wallet-content" scroll-y>
|
<scroll-view class="wallet-content" scroll-y>
|
||||||
<!-- 余额概览 -->
|
<view class="dashboard-container">
|
||||||
<view class="balance-overview">
|
<!-- 左侧/顶部区域:资产信息 -->
|
||||||
<text class="balance-label">账户余额</text>
|
<view class="dashboard-main">
|
||||||
<text class="balance-value">¥{{ balance.toFixed(2) }}</text>
|
<!-- 余额概览 -->
|
||||||
<view class="balance-actions">
|
<view class="balance-overview">
|
||||||
<button class="action-btn recharge" @click="recharge">充值</button>
|
<text class="balance-label">账户余额</text>
|
||||||
<button class="action-btn withdraw" @click="withdraw">提现</button>
|
<text class="balance-value">¥{{ balance.toFixed(2) }}</text>
|
||||||
</view>
|
<view class="balance-actions">
|
||||||
</view>
|
<button class="action-btn recharge" @click="recharge">充值</button>
|
||||||
|
<button class="action-btn withdraw" @click="withdraw">提现</button>
|
||||||
<!-- 资产统计 -->
|
</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>
|
||||||
<view class="action-item" @click="goToRedPackets">
|
|
||||||
<text class="action-icon">🧧</text>
|
<!-- 资产统计 -->
|
||||||
<text class="action-text">红包</text>
|
<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>
|
||||||
<view class="action-item" @click="goToPoints">
|
|
||||||
<text class="action-icon">⭐</text>
|
<!-- 快捷功能 -->
|
||||||
<text class="action-text">积分</text>
|
<view class="quick-actions">
|
||||||
</view>
|
<view class="action-grid">
|
||||||
<view class="action-item" @click="goToBankCards">
|
<view class="action-item" @click="goToCoupons">
|
||||||
<text class="action-icon">💳</text>
|
<text class="action-icon">🎫</text>
|
||||||
<text class="action-text">银行卡</text>
|
<text class="action-text">优惠券</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
<view class="action-item" @click="goToRedPackets">
|
||||||
</view>
|
<text class="action-icon">🧧</text>
|
||||||
|
<text class="action-text">红包</text>
|
||||||
<!-- 交易记录 -->
|
</view>
|
||||||
<view class="transactions-section">
|
<view class="action-item" @click="goToPoints">
|
||||||
<view class="section-header">
|
<text class="action-icon">⭐</text>
|
||||||
<text class="section-title">交易记录</text>
|
<text class="action-text">积分</text>
|
||||||
<view class="filter-tabs">
|
</view>
|
||||||
<text :class="['filter-tab', { active: activeFilter === 'all' }]"
|
<view class="action-item" @click="goToBankCards">
|
||||||
@click="changeFilter('all')">全部</text>
|
<text class="action-icon">💳</text>
|
||||||
<text :class="['filter-tab', { active: activeFilter === 'income' }]"
|
<text class="action-text">银行卡</text>
|
||||||
@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>
|
</view>
|
||||||
<view class="transaction-right">
|
</view>
|
||||||
<text :class="['transaction-amount',
|
|
||||||
{ income: transaction.amount > 0, expense: transaction.amount < 0 }]">
|
<!-- 安全提示 (移动端在底部,PC端在左侧底部) -->
|
||||||
{{ transaction.amount > 0 ? '+' : '' }}¥{{ Math.abs(transaction.amount).toFixed(2) }}
|
<view class="security-tips">
|
||||||
</text>
|
<text class="tip-title">安全提示</text>
|
||||||
<text class="transaction-balance">余额: ¥{{ transaction.current_balance.toFixed(2) }}</text>
|
<text class="tip-item">1. 请妥善保管您的支付密码</text>
|
||||||
|
<text class="tip-item">2. 不要向他人透露您的账户信息</text>
|
||||||
|
<text class="tip-item">3. 定期修改密码以确保账户安全</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 右侧/底部区域:交易记录 -->
|
||||||
|
<view class="dashboard-side">
|
||||||
|
<!-- 交易记录 -->
|
||||||
|
<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>
|
</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>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
|
|
||||||
@@ -251,23 +259,26 @@ const loadBalance = async () => {
|
|||||||
if (!userId) return
|
if (!userId) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data, error } = await supa
|
// 使用本地模拟数据
|
||||||
.from('user_wallets')
|
const mockBalance = {
|
||||||
.select('*')
|
balance: 12580.00,
|
||||||
.eq('user_id', userId)
|
total_recharge: 20000.00,
|
||||||
.single()
|
total_consume: 7420.00,
|
||||||
|
total_withdraw: 0.00
|
||||||
if (error !== null) {
|
|
||||||
console.error('加载钱包失败:', error)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 尝试从本地存储获取
|
||||||
|
const storedWallet = uni.getStorageSync(`wallet_${userId}`)
|
||||||
|
const data = storedWallet ? JSON.parse(storedWallet as string) : mockBalance
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
balance.value = data.balance || 0
|
// 类型断言,处理 any 类型
|
||||||
|
const walletData = data as any
|
||||||
|
balance.value = Number(walletData.balance || 0)
|
||||||
stats.value = {
|
stats.value = {
|
||||||
totalRecharge: data.total_recharge || 0,
|
totalRecharge: Number(walletData.total_recharge || 0),
|
||||||
totalConsume: data.total_consume || 0,
|
totalConsume: Number(walletData.total_consume || 0),
|
||||||
totalWithdraw: data.total_withdraw || 0
|
totalWithdraw: Number(walletData.total_withdraw || 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -289,30 +300,49 @@ const loadTransactions = async (loadMore: boolean = false) => {
|
|||||||
|
|
||||||
const page = loadMore ? currentPage.value + 1 : 1
|
const page = loadMore ? currentPage.value + 1 : 1
|
||||||
|
|
||||||
let query = supa
|
// 模拟交易记录数据
|
||||||
.from('balance_records')
|
const mockTransactions: TransactionType[] = [
|
||||||
.select('*')
|
{
|
||||||
.eq('user_id', userId)
|
id: 't1',
|
||||||
.order('created_at', { ascending: false })
|
user_id: userId,
|
||||||
|
change_amount: -128.00,
|
||||||
|
current_balance: 12580.00,
|
||||||
|
change_type: 'consume',
|
||||||
|
related_id: 'ord_001',
|
||||||
|
remark: '购买药品',
|
||||||
|
created_at: new Date().toISOString()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 't2',
|
||||||
|
user_id: userId,
|
||||||
|
change_amount: 500.00,
|
||||||
|
current_balance: 12708.00,
|
||||||
|
change_type: 'recharge',
|
||||||
|
related_id: 'rec_001',
|
||||||
|
remark: '账户充值',
|
||||||
|
created_at: new Date(Date.now() - 86400000).toISOString()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 't3',
|
||||||
|
user_id: userId,
|
||||||
|
change_amount: -58.50,
|
||||||
|
current_balance: 12208.00,
|
||||||
|
change_type: 'consume',
|
||||||
|
related_id: 'ord_002',
|
||||||
|
remark: '购买保健品',
|
||||||
|
created_at: new Date(Date.now() - 172800000).toISOString()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
// 根据过滤器筛选
|
// 简单模拟分页和筛选
|
||||||
|
let filtered = mockTransactions
|
||||||
if (activeFilter.value === 'income') {
|
if (activeFilter.value === 'income') {
|
||||||
query = query.gt('change_amount', 0)
|
filtered = filtered.filter(t => t.change_amount > 0)
|
||||||
} else if (activeFilter.value === 'expense') {
|
} else if (activeFilter.value === 'expense') {
|
||||||
query = query.lt('change_amount', 0)
|
filtered = filtered.filter(t => t.change_amount < 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 分页
|
const newTransactions = filtered
|
||||||
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) {
|
if (loadMore) {
|
||||||
transactions.value.push(...newTransactions)
|
transactions.value.push(...newTransactions)
|
||||||
@@ -322,7 +352,8 @@ const loadTransactions = async (loadMore: boolean = false) => {
|
|||||||
currentPage.value = 1
|
currentPage.value = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
hasMore.value = newTransactions.length === pageSize.value
|
// 模拟没有更多数据
|
||||||
|
hasMore.value = false
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('加载交易记录异常:', err)
|
console.error('加载交易记录异常:', err)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -484,6 +515,34 @@ const goBack = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
/* 基础样式 */
|
||||||
|
.wallet-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-side {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
/* 响应式布局优化 */
|
/* 响应式布局优化 */
|
||||||
@media screen and (min-width: 768px) {
|
@media screen and (min-width: 768px) {
|
||||||
.wallet-content {
|
.wallet-content {
|
||||||
@@ -491,22 +550,16 @@ const goBack = () => {
|
|||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.balance-overview {
|
.dashboard-container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance-overview, .assets-stats, .quick-actions, .transactions-section, .security-tips {
|
||||||
border-radius: 12px;
|
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 {
|
.popup-content {
|
||||||
width: 400px;
|
width: 400px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@@ -518,46 +571,48 @@ const goBack = () => {
|
|||||||
|
|
||||||
@media screen and (min-width: 1024px) {
|
@media screen and (min-width: 1024px) {
|
||||||
.wallet-page {
|
.wallet-page {
|
||||||
flex-direction: row; /* 大屏下改为横向布局 */
|
flex-direction: column; /* 保持纵向,内容区内部处理横向 */
|
||||||
}
|
|
||||||
|
|
||||||
.wallet-header {
|
|
||||||
display: none; /* 大屏下隐藏顶部栏 */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.wallet-content {
|
.wallet-content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 1000px;
|
max-width: 1200px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dashboard-container {
|
||||||
|
flex-direction: row; /* 横向排列 */
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 20px;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-main {
|
||||||
|
width: 400px; /* 左侧固定宽度 */
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-side {
|
||||||
|
flex: 1; /* 右侧自适应 */
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 调整各模块间距 */
|
||||||
|
.balance-overview,
|
||||||
|
.assets-stats,
|
||||||
|
.quick-actions,
|
||||||
|
.security-tips {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transactions-section {
|
||||||
|
margin-top: 0; /* 移除顶部间距,与左侧对齐 */
|
||||||
|
height: 100%;
|
||||||
|
min-height: 600px; /* 保证右侧高度 */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.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 {
|
.balance-overview {
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
padding: 30px 20px;
|
padding: 30px 20px;
|
||||||
|
|||||||
984
pages/mall/consumer/wallett.uvue
Normal file
984
pages/mall/consumer/wallett.uvue
Normal file
@@ -0,0 +1,984 @@
|
|||||||
|
<!-- 钱包页面 -->
|
||||||
|
<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">单笔充值最低10元,最高5000元</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>
|
||||||
@@ -1,544 +1,135 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="bind-email-page">
|
<view class="page-container">
|
||||||
<view class="header">
|
<view class="form-group">
|
||||||
<view class="back-btn" @click="goBack">
|
<view class="input-item">
|
||||||
<text class="back-icon">‹</text>
|
<text class="label">邮箱</text>
|
||||||
</view>
|
<input class="input" type="text" placeholder="请输入邮箱地址" v-model="email" />
|
||||||
<text class="header-title">绑定邮箱</text>
|
</view>
|
||||||
</view>
|
<view class="input-item">
|
||||||
|
<text class="label">验证码</text>
|
||||||
<view class="content">
|
<input class="input" type="number" placeholder="请输入验证码" v-model="code" maxlength="6" />
|
||||||
<view v-if="userInfo.email" class="already-bound">
|
<text class="code-btn" @click="sendCode">{{ counting ? `${count}s` : '获取验证码' }}</text>
|
||||||
<text class="bound-icon">✓</text>
|
</view>
|
||||||
<text class="bound-title">已绑定邮箱</text>
|
</view>
|
||||||
<text class="bound-email">{{ userInfo.email }}</text>
|
|
||||||
<text class="bound-time">绑定时间:{{ formatTime(userInfo.emailBoundTime) }}</text>
|
<button class="submit-btn" @click="handleSubmit">确认绑定</button>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<form @submit="onSubmit" v-if="!userInfo.email || isChanging">
|
|
||||||
<!-- 邮箱输入 -->
|
|
||||||
<view class="form-item">
|
|
||||||
<text class="label">邮箱地址</text>
|
|
||||||
<input
|
|
||||||
class="input"
|
|
||||||
type="email"
|
|
||||||
placeholder="请输入邮箱地址"
|
|
||||||
v-model="form.email"
|
|
||||||
:disabled="loading"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 验证码 -->
|
|
||||||
<view class="form-item">
|
|
||||||
<text class="label">验证码</text>
|
|
||||||
<view class="code-input-wrapper">
|
|
||||||
<input
|
|
||||||
class="code-input"
|
|
||||||
type="number"
|
|
||||||
placeholder="请输入验证码"
|
|
||||||
v-model="form.code"
|
|
||||||
:disabled="loading"
|
|
||||||
maxlength="6"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
class="get-code-btn"
|
|
||||||
@click="getCode"
|
|
||||||
:disabled="!canGetCode || countdown > 0"
|
|
||||||
>
|
|
||||||
{{ countdown > 0 ? `${countdown}s后重新获取` : '获取验证码' }}
|
|
||||||
</button>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 提交按钮 -->
|
|
||||||
<button
|
|
||||||
class="submit-btn"
|
|
||||||
form-type="submit"
|
|
||||||
:disabled="loading || !isFormValid"
|
|
||||||
:loading="loading"
|
|
||||||
>
|
|
||||||
{{ loading ? '处理中...' : userInfo.email ? '更换邮箱' : '绑定邮箱' }}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<view v-if="userInfo.email && !isChanging" class="action-buttons">
|
|
||||||
<button class="change-btn" @click="startChange">更换邮箱</button>
|
|
||||||
<button class="unbind-btn" @click="unbindEmail">解绑邮箱</button>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 成功提示 -->
|
|
||||||
<view v-if="showSuccess" class="success-modal" @click="hideSuccess">
|
|
||||||
<view class="success-content" @click.stop>
|
|
||||||
<text class="success-icon">✓</text>
|
|
||||||
<text class="success-title">{{ successTitle }}</text>
|
|
||||||
<text class="success-text">{{ successMessage }}</text>
|
|
||||||
<button class="success-btn" @click="hideSuccess">确定</button>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref } from 'vue'
|
||||||
//import supa from '@/components/supadb/aksupainstance.uts'
|
|
||||||
|
|
||||||
// 用户信息
|
const email = ref('')
|
||||||
const userInfo = ref({
|
const code = ref('')
|
||||||
email: '',
|
const counting = ref(false)
|
||||||
emailBoundTime: null as string | null
|
const count = ref(60)
|
||||||
})
|
|
||||||
|
|
||||||
const form = ref({
|
const sendCode = () => {
|
||||||
email: '',
|
if (counting.value) return
|
||||||
code: ''
|
if (!email.value || !email.value.includes('@')) {
|
||||||
})
|
uni.showToast({
|
||||||
|
title: '请输入正确的邮箱',
|
||||||
const loading = ref<boolean>(false)
|
icon: 'none'
|
||||||
const countdown = ref<number>(0)
|
})
|
||||||
const isChanging = ref<boolean>(false)
|
return
|
||||||
const showSuccess = ref<boolean>(false)
|
}
|
||||||
const successTitle = ref<string>('')
|
|
||||||
const successMessage = ref<string>('')
|
counting.value = true
|
||||||
|
count.value = 60
|
||||||
// 表单验证
|
|
||||||
const isFormValid = computed((): boolean => {
|
const timer = setInterval(() => {
|
||||||
const { email, code } = form.value
|
count.value--
|
||||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
if (count.value <= 0) {
|
||||||
return emailRegex.test(email) && /^\d{6}$/.test(code)
|
clearInterval(timer)
|
||||||
})
|
counting.value = false
|
||||||
|
}
|
||||||
// 是否可以获取验证码
|
}, 1000)
|
||||||
const canGetCode = computed((): boolean => {
|
|
||||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
uni.showToast({
|
||||||
return emailRegex.test(form.value.email)
|
title: '验证码已发送',
|
||||||
})
|
icon: 'none'
|
||||||
|
})
|
||||||
// 加载用户信息
|
|
||||||
const loadUserInfo = () => {
|
|
||||||
const storedUserInfo = uni.getStorageSync('userInfo')
|
|
||||||
if (storedUserInfo) {
|
|
||||||
try {
|
|
||||||
const info = JSON.parse(storedUserInfo as string)
|
|
||||||
userInfo.value.email = info.email || ''
|
|
||||||
userInfo.value.emailBoundTime = info.emailBoundTime || null
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to parse user info', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取验证码
|
const handleSubmit = () => {
|
||||||
const getCode = () => {
|
if (!email.value || !code.value) {
|
||||||
if (!canGetCode.value) {
|
uni.showToast({
|
||||||
uni.showToast({
|
title: '请填写完整信息',
|
||||||
title: '请输入正确的邮箱地址',
|
icon: 'none'
|
||||||
icon: 'none'
|
})
|
||||||
})
|
return
|
||||||
return
|
}
|
||||||
}
|
|
||||||
|
// TODO: Call API to bind email
|
||||||
// 开始倒计时
|
uni.showLoading({ title: '提交中...' })
|
||||||
countdown.value = 60
|
setTimeout(() => {
|
||||||
const timer = setInterval(() => {
|
uni.hideLoading()
|
||||||
countdown.value--
|
uni.showToast({
|
||||||
if (countdown.value <= 0) {
|
title: '绑定成功',
|
||||||
clearInterval(timer)
|
icon: 'success'
|
||||||
}
|
})
|
||||||
}, 1000)
|
|
||||||
|
// 更新本地存储的用户信息
|
||||||
// 模拟发送验证码
|
const userInfo = uni.getStorageSync('userInfo')
|
||||||
uni.showToast({
|
if (userInfo) {
|
||||||
title: '验证码已发送到邮箱',
|
// @ts-ignore
|
||||||
icon: 'success'
|
userInfo.email = email.value
|
||||||
})
|
uni.setStorageSync('userInfo', userInfo)
|
||||||
|
}
|
||||||
// 实际项目中这里应该调用发送邮件的API
|
|
||||||
// const response = await sendEmailCode(form.value.email)
|
setTimeout(() => {
|
||||||
|
uni.navigateBack()
|
||||||
|
}, 1500)
|
||||||
|
}, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提交表单
|
|
||||||
const onSubmit = async () => {
|
|
||||||
if (!isFormValid.value) {
|
|
||||||
uni.showToast({
|
|
||||||
title: '请填写完整且正确的信息',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
loading.value = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 模拟API调用
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
||||||
|
|
||||||
// 更新本地用户信息
|
|
||||||
const storedUserInfo = uni.getStorageSync('userInfo')
|
|
||||||
let userInfoData = storedUserInfo ? JSON.parse(storedUserInfo as string) : {}
|
|
||||||
|
|
||||||
userInfoData.email = form.value.email
|
|
||||||
userInfoData.emailBoundTime = new Date().toISOString()
|
|
||||||
|
|
||||||
uni.setStorageSync('userInfo', JSON.stringify(userInfoData))
|
|
||||||
userInfo.value.email = form.value.email
|
|
||||||
userInfo.value.emailBoundTime = userInfoData.emailBoundTime
|
|
||||||
|
|
||||||
// 显示成功消息
|
|
||||||
successTitle.value = isChanging.value ? '更换成功' : '绑定成功'
|
|
||||||
successMessage.value = `邮箱 ${form.value.email} 已成功${isChanging.value ? '更换' : '绑定'}`
|
|
||||||
showSuccess.value = true
|
|
||||||
|
|
||||||
// 重置表单
|
|
||||||
form.value = { email: '', code: '' }
|
|
||||||
isChanging.value = false
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('绑定邮箱失败:', error)
|
|
||||||
uni.showToast({
|
|
||||||
title: error.message || '操作失败',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 开始更换邮箱
|
|
||||||
const startChange = () => {
|
|
||||||
isChanging.value = true
|
|
||||||
form.value.email = userInfo.value.email
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解绑邮箱
|
|
||||||
const unbindEmail = () => {
|
|
||||||
uni.showModal({
|
|
||||||
title: '解绑邮箱',
|
|
||||||
content: '确定要解绑邮箱吗?解绑后可能影响账号安全',
|
|
||||||
success: async (res) => {
|
|
||||||
if (res.confirm) {
|
|
||||||
loading.value = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 模拟API调用
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
||||||
|
|
||||||
// 更新本地用户信息
|
|
||||||
const storedUserInfo = uni.getStorageSync('userInfo')
|
|
||||||
let userInfoData = storedUserInfo ? JSON.parse(storedUserInfo as string) : {}
|
|
||||||
|
|
||||||
userInfoData.email = ''
|
|
||||||
userInfoData.emailBoundTime = null
|
|
||||||
|
|
||||||
uni.setStorageSync('userInfo', JSON.stringify(userInfoData))
|
|
||||||
userInfo.value.email = ''
|
|
||||||
userInfo.value.emailBoundTime = null
|
|
||||||
|
|
||||||
uni.showToast({
|
|
||||||
title: '解绑成功',
|
|
||||||
icon: 'success'
|
|
||||||
})
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('解绑失败:', error)
|
|
||||||
uni.showToast({
|
|
||||||
title: error.message || '解绑失败',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化时间显示
|
|
||||||
const formatTime = (time: string | null): string => {
|
|
||||||
if (!time) return '未知'
|
|
||||||
const date = new Date(time)
|
|
||||||
return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导航函数
|
|
||||||
const goBack = () => {
|
|
||||||
uni.navigateBack()
|
|
||||||
}
|
|
||||||
|
|
||||||
const hideSuccess = () => {
|
|
||||||
showSuccess.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生命周期
|
|
||||||
onMounted(() => {
|
|
||||||
loadUserInfo()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.bind-email-page {
|
.page-container {
|
||||||
min-height: 100vh;
|
padding: 20px;
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.form-group {
|
||||||
background-color: #ffffff;
|
background-color: #fff;
|
||||||
padding: 15px;
|
border-radius: 8px;
|
||||||
display: flex;
|
padding: 0 15px;
|
||||||
align-items: center;
|
margin-bottom: 30px;
|
||||||
border-bottom: 1px solid #e5e5e5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.back-btn {
|
.input-item {
|
||||||
width: 40px;
|
display: flex;
|
||||||
height: 40px;
|
align-items: center;
|
||||||
display: flex;
|
height: 50px;
|
||||||
align-items: center;
|
border-bottom: 1px solid #eee;
|
||||||
justify-content: center;
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.back-icon {
|
.input-item:last-child {
|
||||||
font-size: 24px;
|
border-bottom: none;
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-title {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #333;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.already-bound {
|
|
||||||
background-color: #ffffff;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 20px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bound-icon {
|
|
||||||
display: block;
|
|
||||||
width: 60px;
|
|
||||||
height: 60px;
|
|
||||||
line-height: 60px;
|
|
||||||
background-color: #4cd964;
|
|
||||||
color: #ffffff;
|
|
||||||
font-size: 30px;
|
|
||||||
border-radius: 50%;
|
|
||||||
margin: 0 auto 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bound-title {
|
|
||||||
display: block;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #333;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bound-email {
|
|
||||||
display: block;
|
|
||||||
font-size: 16px;
|
|
||||||
color: #007aff;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
font-weight: bold;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bound-time {
|
|
||||||
display: block;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-item {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
background-color: #ffffff;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 15px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
display: block;
|
width: 70px;
|
||||||
font-size: 16px;
|
font-size: 14px;
|
||||||
color: #333;
|
color: #333;
|
||||||
margin-bottom: 10px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
width: 100%;
|
flex: 1;
|
||||||
height: 44px;
|
font-size: 14px;
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 0 15px;
|
|
||||||
font-size: 16px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.input:focus {
|
.code-btn {
|
||||||
border-color: #007aff;
|
color: #007aff;
|
||||||
outline: none;
|
font-size: 14px;
|
||||||
}
|
padding: 5px 10px;
|
||||||
|
|
||||||
.input:disabled {
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.code-input-wrapper {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.code-input {
|
|
||||||
flex: 1;
|
|
||||||
height: 44px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 0 15px;
|
|
||||||
font-size: 16px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.get-code-btn {
|
|
||||||
width: 120px;
|
|
||||||
height: 44px;
|
|
||||||
background-color: #007aff;
|
|
||||||
color: #ffffff;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.get-code-btn:disabled {
|
|
||||||
background-color: #cccccc;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.submit-btn {
|
.submit-btn {
|
||||||
width: 100%;
|
background-color: #007aff;
|
||||||
height: 50px;
|
color: #fff;
|
||||||
background-color: #007aff;
|
border-radius: 25px;
|
||||||
color: #ffffff;
|
font-size: 16px;
|
||||||
border-radius: 25px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
border: none;
|
|
||||||
margin-top: 30px;
|
|
||||||
}
|
}
|
||||||
|
</style>
|
||||||
.submit-btn:disabled {
|
|
||||||
background-color: #cccccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-buttons {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 15px;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.change-btn {
|
|
||||||
width: 100%;
|
|
||||||
height: 50px;
|
|
||||||
background-color: #ffffff;
|
|
||||||
color: #007aff;
|
|
||||||
border: 2px solid #007aff;
|
|
||||||
border-radius: 25px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.unbind-btn {
|
|
||||||
width: 100%;
|
|
||||||
height: 50px;
|
|
||||||
background-color: #ffffff;
|
|
||||||
color: #ff3b30;
|
|
||||||
border: 2px solid #ff3b30;
|
|
||||||
border-radius: 25px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 成功提示模态框 */
|
|
||||||
.success-modal {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.success-content {
|
|
||||||
width: 280px;
|
|
||||||
background-color: #ffffff;
|
|
||||||
border-radius: 15px;
|
|
||||||
padding: 30px 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.success-icon {
|
|
||||||
display: block;
|
|
||||||
width: 60px;
|
|
||||||
height: 60px;
|
|
||||||
line-height: 60px;
|
|
||||||
background-color: #4cd964;
|
|
||||||
color: #ffffff;
|
|
||||||
font-size: 30px;
|
|
||||||
border-radius: 50%;
|
|
||||||
margin: 0 auto 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.success-title {
|
|
||||||
display: block;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #333;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.success-text {
|
|
||||||
display: block;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #666;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.success-btn {
|
|
||||||
width: 100%;
|
|
||||||
height: 44px;
|
|
||||||
background-color: #007aff;
|
|
||||||
color: #ffffff;
|
|
||||||
border-radius: 22px;
|
|
||||||
font-size: 16px;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 响应式优化 */
|
|
||||||
@media screen and (min-width: 768px) {
|
|
||||||
.bind-email-page {
|
|
||||||
max-width: 500px;
|
|
||||||
margin: 0 auto;
|
|
||||||
border-left: 1px solid #e5e5e5;
|
|
||||||
border-right: 1px solid #e5e5e5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
padding: 40px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,548 +1,135 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="bind-phone-page">
|
<view class="page-container">
|
||||||
<view class="header">
|
<view class="form-group">
|
||||||
<view class="back-btn" @click="goBack">
|
<view class="input-item">
|
||||||
<text class="back-icon">‹</text>
|
<text class="label">手机号</text>
|
||||||
</view>
|
<input class="input" type="number" placeholder="请输入新手机号" v-model="phone" maxlength="11" />
|
||||||
<text class="header-title">绑定手机</text>
|
</view>
|
||||||
</view>
|
<view class="input-item">
|
||||||
|
<text class="label">验证码</text>
|
||||||
<view class="content">
|
<input class="input" type="number" placeholder="请输入验证码" v-model="code" maxlength="6" />
|
||||||
<view v-if="userInfo.phone" class="already-bound">
|
<text class="code-btn" @click="sendCode">{{ counting ? `${count}s` : '获取验证码' }}</text>
|
||||||
<text class="bound-icon">✓</text>
|
</view>
|
||||||
<text class="bound-title">已绑定手机</text>
|
</view>
|
||||||
<text class="bound-phone">{{ formatPhone(userInfo.phone) }}</text>
|
|
||||||
<text class="bound-time">绑定时间:{{ formatTime(userInfo.phoneBoundTime) }}</text>
|
<button class="submit-btn" @click="handleSubmit">确认绑定</button>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<form @submit="onSubmit" v-if="!userInfo.phone || isChanging">
|
|
||||||
<!-- 手机号输入 -->
|
|
||||||
<view class="form-item">
|
|
||||||
<text class="label">手机号</text>
|
|
||||||
<input
|
|
||||||
class="input"
|
|
||||||
type="number"
|
|
||||||
placeholder="请输入手机号"
|
|
||||||
v-model="form.phone"
|
|
||||||
:disabled="loading || countdown > 0"
|
|
||||||
maxlength="11"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 验证码 -->
|
|
||||||
<view class="form-item">
|
|
||||||
<text class="label">验证码</text>
|
|
||||||
<view class="code-input-wrapper">
|
|
||||||
<input
|
|
||||||
class="code-input"
|
|
||||||
type="number"
|
|
||||||
placeholder="请输入验证码"
|
|
||||||
v-model="form.code"
|
|
||||||
:disabled="loading"
|
|
||||||
maxlength="6"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
class="get-code-btn"
|
|
||||||
@click="getCode"
|
|
||||||
:disabled="!canGetCode || countdown > 0"
|
|
||||||
>
|
|
||||||
{{ countdown > 0 ? `${countdown}s后重新获取` : '获取验证码' }}
|
|
||||||
</button>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 提交按钮 -->
|
|
||||||
<button
|
|
||||||
class="submit-btn"
|
|
||||||
form-type="submit"
|
|
||||||
:disabled="loading || !isFormValid"
|
|
||||||
:loading="loading"
|
|
||||||
>
|
|
||||||
{{ loading ? '处理中...' : userInfo.phone ? '更换手机号' : '绑定手机号' }}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<view v-if="userInfo.phone && !isChanging" class="action-buttons">
|
|
||||||
<button class="change-btn" @click="startChange">更换手机号</button>
|
|
||||||
<button class="unbind-btn" @click="unbindPhone">解绑手机</button>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 成功提示 -->
|
|
||||||
<view v-if="showSuccess" class="success-modal" @click="hideSuccess">
|
|
||||||
<view class="success-content" @click.stop>
|
|
||||||
<text class="success-icon">✓</text>
|
|
||||||
<text class="success-title">{{ successTitle }}</text>
|
|
||||||
<text class="success-text">{{ successMessage }}</text>
|
|
||||||
<button class="success-btn" @click="hideSuccess">确定</button>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref } from 'vue'
|
||||||
//import supa from '@/components/supadb/aksupainstance.uts'
|
|
||||||
|
|
||||||
// 用户信息
|
const phone = ref('')
|
||||||
const userInfo = ref({
|
const code = ref('')
|
||||||
phone: '',
|
const counting = ref(false)
|
||||||
phoneBoundTime: null as string | null
|
const count = ref(60)
|
||||||
})
|
|
||||||
|
|
||||||
const form = ref({
|
const sendCode = () => {
|
||||||
phone: '',
|
if (counting.value) return
|
||||||
code: ''
|
if (!phone.value || phone.value.length !== 11) {
|
||||||
})
|
uni.showToast({
|
||||||
|
title: '请输入正确的手机号',
|
||||||
const loading = ref<boolean>(false)
|
icon: 'none'
|
||||||
const countdown = ref<number>(0)
|
})
|
||||||
const isChanging = ref<boolean>(false)
|
return
|
||||||
const showSuccess = ref<boolean>(false)
|
}
|
||||||
const successTitle = ref<string>('')
|
|
||||||
const successMessage = ref<string>('')
|
counting.value = true
|
||||||
|
count.value = 60
|
||||||
// 表单验证
|
|
||||||
const isFormValid = computed((): boolean => {
|
const timer = setInterval(() => {
|
||||||
const { phone, code } = form.value
|
count.value--
|
||||||
return /^1[3-9]\d{9}$/.test(phone) && /^\d{6}$/.test(code)
|
if (count.value <= 0) {
|
||||||
})
|
clearInterval(timer)
|
||||||
|
counting.value = false
|
||||||
// 是否可以获取验证码
|
}
|
||||||
const canGetCode = computed((): boolean => {
|
}, 1000)
|
||||||
return /^1[3-9]\d{9}$/.test(form.value.phone)
|
|
||||||
})
|
uni.showToast({
|
||||||
|
title: '验证码已发送',
|
||||||
// 加载用户信息
|
icon: 'none'
|
||||||
const loadUserInfo = () => {
|
})
|
||||||
const storedUserInfo = uni.getStorageSync('userInfo')
|
|
||||||
if (storedUserInfo) {
|
|
||||||
try {
|
|
||||||
const info = JSON.parse(storedUserInfo as string)
|
|
||||||
userInfo.value.phone = info.phone || ''
|
|
||||||
userInfo.value.phoneBoundTime = info.phoneBoundTime || null
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to parse user info', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取验证码
|
const handleSubmit = () => {
|
||||||
const getCode = () => {
|
if (!phone.value || !code.value) {
|
||||||
if (!canGetCode.value) {
|
uni.showToast({
|
||||||
uni.showToast({
|
title: '请填写完整信息',
|
||||||
title: '请输入正确的手机号',
|
icon: 'none'
|
||||||
icon: 'none'
|
})
|
||||||
})
|
return
|
||||||
return
|
}
|
||||||
}
|
|
||||||
|
// TODO: Call API to bind phone
|
||||||
// 开始倒计时
|
uni.showLoading({ title: '提交中...' })
|
||||||
countdown.value = 60
|
setTimeout(() => {
|
||||||
const timer = setInterval(() => {
|
uni.hideLoading()
|
||||||
countdown.value--
|
uni.showToast({
|
||||||
if (countdown.value <= 0) {
|
title: '绑定成功',
|
||||||
clearInterval(timer)
|
icon: 'success'
|
||||||
}
|
})
|
||||||
}, 1000)
|
|
||||||
|
// 更新本地存储的用户信息
|
||||||
// 模拟发送验证码
|
const userInfo = uni.getStorageSync('userInfo')
|
||||||
uni.showToast({
|
if (userInfo) {
|
||||||
title: '验证码已发送',
|
// @ts-ignore
|
||||||
icon: 'success'
|
userInfo.phone = phone.value
|
||||||
})
|
uni.setStorageSync('userInfo', userInfo)
|
||||||
|
}
|
||||||
// 实际项目中这里应该调用发送短信的API
|
|
||||||
// const response = await sendSmsCode(form.value.phone)
|
setTimeout(() => {
|
||||||
|
uni.navigateBack()
|
||||||
|
}, 1500)
|
||||||
|
}, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提交表单
|
|
||||||
const onSubmit = async () => {
|
|
||||||
if (!isFormValid.value) {
|
|
||||||
uni.showToast({
|
|
||||||
title: '请填写完整且正确的信息',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
loading.value = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 模拟API调用
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
||||||
|
|
||||||
// 更新本地用户信息
|
|
||||||
const storedUserInfo = uni.getStorageSync('userInfo')
|
|
||||||
let userInfoData = storedUserInfo ? JSON.parse(storedUserInfo as string) : {}
|
|
||||||
|
|
||||||
userInfoData.phone = form.value.phone
|
|
||||||
userInfoData.phoneBoundTime = new Date().toISOString()
|
|
||||||
|
|
||||||
uni.setStorageSync('userInfo', JSON.stringify(userInfoData))
|
|
||||||
userInfo.value.phone = form.value.phone
|
|
||||||
userInfo.value.phoneBoundTime = userInfoData.phoneBoundTime
|
|
||||||
|
|
||||||
// 显示成功消息
|
|
||||||
successTitle.value = isChanging.value ? '更换成功' : '绑定成功'
|
|
||||||
successMessage.value = `手机号 ${formatPhone(form.value.phone)} 已成功${isChanging.value ? '更换' : '绑定'}`
|
|
||||||
showSuccess.value = true
|
|
||||||
|
|
||||||
// 重置表单
|
|
||||||
form.value = { phone: '', code: '' }
|
|
||||||
isChanging.value = false
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('绑定手机失败:', error)
|
|
||||||
uni.showToast({
|
|
||||||
title: error.message || '操作失败',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 开始更换手机号
|
|
||||||
const startChange = () => {
|
|
||||||
isChanging.value = true
|
|
||||||
form.value.phone = userInfo.value.phone
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解绑手机
|
|
||||||
const unbindPhone = () => {
|
|
||||||
uni.showModal({
|
|
||||||
title: '解绑手机',
|
|
||||||
content: '确定要解绑手机吗?解绑后可能影响账号安全',
|
|
||||||
success: async (res) => {
|
|
||||||
if (res.confirm) {
|
|
||||||
loading.value = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 模拟API调用
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
||||||
|
|
||||||
// 更新本地用户信息
|
|
||||||
const storedUserInfo = uni.getStorageSync('userInfo')
|
|
||||||
let userInfoData = storedUserInfo ? JSON.parse(storedUserInfo as string) : {}
|
|
||||||
|
|
||||||
userInfoData.phone = ''
|
|
||||||
userInfoData.phoneBoundTime = null
|
|
||||||
|
|
||||||
uni.setStorageSync('userInfo', JSON.stringify(userInfoData))
|
|
||||||
userInfo.value.phone = ''
|
|
||||||
userInfo.value.phoneBoundTime = null
|
|
||||||
|
|
||||||
uni.showToast({
|
|
||||||
title: '解绑成功',
|
|
||||||
icon: 'success'
|
|
||||||
})
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('解绑失败:', error)
|
|
||||||
uni.showToast({
|
|
||||||
title: error.message || '解绑失败',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化手机号显示
|
|
||||||
const formatPhone = (phone: string): string => {
|
|
||||||
if (!phone) return ''
|
|
||||||
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化时间显示
|
|
||||||
const formatTime = (time: string | null): string => {
|
|
||||||
if (!time) return '未知'
|
|
||||||
const date = new Date(time)
|
|
||||||
return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导航函数
|
|
||||||
const goBack = () => {
|
|
||||||
uni.navigateBack()
|
|
||||||
}
|
|
||||||
|
|
||||||
const hideSuccess = () => {
|
|
||||||
showSuccess.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生命周期
|
|
||||||
onMounted(() => {
|
|
||||||
loadUserInfo()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.bind-phone-page {
|
.page-container {
|
||||||
min-height: 100vh;
|
padding: 20px;
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.form-group {
|
||||||
background-color: #ffffff;
|
background-color: #fff;
|
||||||
padding: 15px;
|
border-radius: 8px;
|
||||||
display: flex;
|
padding: 0 15px;
|
||||||
align-items: center;
|
margin-bottom: 30px;
|
||||||
border-bottom: 1px solid #e5e5e5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.back-btn {
|
.input-item {
|
||||||
width: 40px;
|
display: flex;
|
||||||
height: 40px;
|
align-items: center;
|
||||||
display: flex;
|
height: 50px;
|
||||||
align-items: center;
|
border-bottom: 1px solid #eee;
|
||||||
justify-content: center;
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.back-icon {
|
.input-item:last-child {
|
||||||
font-size: 24px;
|
border-bottom: none;
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-title {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #333;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.already-bound {
|
|
||||||
background-color: #ffffff;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 20px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bound-icon {
|
|
||||||
display: block;
|
|
||||||
width: 60px;
|
|
||||||
height: 60px;
|
|
||||||
line-height: 60px;
|
|
||||||
background-color: #4cd964;
|
|
||||||
color: #ffffff;
|
|
||||||
font-size: 30px;
|
|
||||||
border-radius: 50%;
|
|
||||||
margin: 0 auto 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bound-title {
|
|
||||||
display: block;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #333;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bound-phone {
|
|
||||||
display: block;
|
|
||||||
font-size: 24px;
|
|
||||||
color: #007aff;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bound-time {
|
|
||||||
display: block;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-item {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
background-color: #ffffff;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 15px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
display: block;
|
width: 70px;
|
||||||
font-size: 16px;
|
font-size: 14px;
|
||||||
color: #333;
|
color: #333;
|
||||||
margin-bottom: 10px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
width: 100%;
|
flex: 1;
|
||||||
height: 44px;
|
font-size: 14px;
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 0 15px;
|
|
||||||
font-size: 16px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.input:focus {
|
.code-btn {
|
||||||
border-color: #007aff;
|
color: #007aff;
|
||||||
outline: none;
|
font-size: 14px;
|
||||||
}
|
padding: 5px 10px;
|
||||||
|
|
||||||
.input:disabled {
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.code-input-wrapper {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.code-input {
|
|
||||||
flex: 1;
|
|
||||||
height: 44px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 0 15px;
|
|
||||||
font-size: 16px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.get-code-btn {
|
|
||||||
width: 120px;
|
|
||||||
height: 44px;
|
|
||||||
background-color: #007aff;
|
|
||||||
color: #ffffff;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.get-code-btn:disabled {
|
|
||||||
background-color: #cccccc;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.submit-btn {
|
.submit-btn {
|
||||||
width: 100%;
|
background-color: #007aff;
|
||||||
height: 50px;
|
color: #fff;
|
||||||
background-color: #007aff;
|
border-radius: 25px;
|
||||||
color: #ffffff;
|
font-size: 16px;
|
||||||
border-radius: 25px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
border: none;
|
|
||||||
margin-top: 30px;
|
|
||||||
}
|
}
|
||||||
|
</style>
|
||||||
.submit-btn:disabled {
|
|
||||||
background-color: #cccccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-buttons {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 15px;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.change-btn {
|
|
||||||
width: 100%;
|
|
||||||
height: 50px;
|
|
||||||
background-color: #ffffff;
|
|
||||||
color: #007aff;
|
|
||||||
border: 2px solid #007aff;
|
|
||||||
border-radius: 25px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.unbind-btn {
|
|
||||||
width: 100%;
|
|
||||||
height: 50px;
|
|
||||||
background-color: #ffffff;
|
|
||||||
color: #ff3b30;
|
|
||||||
border: 2px solid #ff3b30;
|
|
||||||
border-radius: 25px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 成功提示模态框 */
|
|
||||||
.success-modal {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.success-content {
|
|
||||||
width: 280px;
|
|
||||||
background-color: #ffffff;
|
|
||||||
border-radius: 15px;
|
|
||||||
padding: 30px 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.success-icon {
|
|
||||||
display: block;
|
|
||||||
width: 60px;
|
|
||||||
height: 60px;
|
|
||||||
line-height: 60px;
|
|
||||||
background-color: #4cd964;
|
|
||||||
color: #ffffff;
|
|
||||||
font-size: 30px;
|
|
||||||
border-radius: 50%;
|
|
||||||
margin: 0 auto 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.success-title {
|
|
||||||
display: block;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #333;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.success-text {
|
|
||||||
display: block;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #666;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.success-btn {
|
|
||||||
width: 100%;
|
|
||||||
height: 44px;
|
|
||||||
background-color: #007aff;
|
|
||||||
color: #ffffff;
|
|
||||||
border-radius: 22px;
|
|
||||||
font-size: 16px;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 响应式优化 */
|
|
||||||
@media screen and (min-width: 768px) {
|
|
||||||
.bind-phone-page {
|
|
||||||
max-width: 500px;
|
|
||||||
margin: 0 auto;
|
|
||||||
border-left: 1px solid #e5e5e5;
|
|
||||||
border-right: 1px solid #e5e5e5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
padding: 40px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,353 +1,103 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="change-password-page">
|
<view class="page-container">
|
||||||
<view class="header">
|
<view class="form-group">
|
||||||
<view class="back-btn" @click="goBack">
|
<view class="input-item">
|
||||||
<text class="back-icon">‹</text>
|
<text class="label">旧密码</text>
|
||||||
</view>
|
<input class="input" type="password" placeholder="请输入旧密码" v-model="oldPassword" />
|
||||||
<text class="header-title">修改密码</text>
|
</view>
|
||||||
</view>
|
<view class="input-item">
|
||||||
|
<text class="label">新密码</text>
|
||||||
<view class="content">
|
<input class="input" type="password" placeholder="请输入新密码" v-model="newPassword" />
|
||||||
<form @submit="onSubmit">
|
</view>
|
||||||
<!-- 当前密码 -->
|
<view class="input-item">
|
||||||
<view class="form-item">
|
<text class="label">确认密码</text>
|
||||||
<text class="label">当前密码</text>
|
<input class="input" type="password" placeholder="请再次输入新密码" v-model="confirmPassword" />
|
||||||
<input
|
</view>
|
||||||
class="input"
|
</view>
|
||||||
type="password"
|
|
||||||
placeholder="请输入当前密码"
|
<button class="submit-btn" @click="handleSubmit">确认修改</button>
|
||||||
v-model="form.currentPassword"
|
</view>
|
||||||
:disabled="loading"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 新密码 -->
|
|
||||||
<view class="form-item">
|
|
||||||
<text class="label">新密码</text>
|
|
||||||
<input
|
|
||||||
class="input"
|
|
||||||
type="password"
|
|
||||||
placeholder="请输入新密码(6-20位字符)"
|
|
||||||
v-model="form.newPassword"
|
|
||||||
:disabled="loading"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<text class="hint">6-20位字符,建议包含字母、数字和特殊符号</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 确认新密码 -->
|
|
||||||
<view class="form-item">
|
|
||||||
<text class="label">确认新密码</text>
|
|
||||||
<input
|
|
||||||
class="input"
|
|
||||||
type="password"
|
|
||||||
placeholder="请再次输入新密码"
|
|
||||||
v-model="form.confirmPassword"
|
|
||||||
:disabled="loading"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 提交按钮 -->
|
|
||||||
<button
|
|
||||||
class="submit-btn"
|
|
||||||
form-type="submit"
|
|
||||||
:disabled="loading || !isFormValid"
|
|
||||||
:loading="loading"
|
|
||||||
>
|
|
||||||
{{ loading ? '处理中...' : '确认修改' }}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<!-- 忘记密码提示 -->
|
|
||||||
<view class="forgot-password">
|
|
||||||
<text class="forgot-text" @click="goToForgotPassword">忘记密码?</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 成功提示 -->
|
|
||||||
<view v-if="showSuccess" class="success-modal" @click="hideSuccess">
|
|
||||||
<view class="success-content" @click.stop>
|
|
||||||
<text class="success-icon">✓</text>
|
|
||||||
<text class="success-title">修改成功</text>
|
|
||||||
<text class="success-text">密码已成功修改,请使用新密码登录</text>
|
|
||||||
<button class="success-btn" @click="hideSuccess">确定</button>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref } from 'vue'
|
||||||
import supa from '@/components/supadb/aksupainstance.uts'
|
|
||||||
|
|
||||||
// 表单数据
|
const oldPassword = ref('')
|
||||||
const form = ref({
|
const newPassword = ref('')
|
||||||
currentPassword: '',
|
const confirmPassword = ref('')
|
||||||
newPassword: '',
|
|
||||||
confirmPassword: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const loading = ref<boolean>(false)
|
const handleSubmit = () => {
|
||||||
const showSuccess = ref<boolean>(false)
|
if (!oldPassword.value || !newPassword.value || !confirmPassword.value) {
|
||||||
|
uni.showToast({
|
||||||
// 表单验证
|
title: '请填写完整信息',
|
||||||
const isFormValid = computed((): boolean => {
|
icon: 'none'
|
||||||
const { currentPassword, newPassword, confirmPassword } = form.value
|
})
|
||||||
return currentPassword.length >= 6 &&
|
return
|
||||||
newPassword.length >= 6 &&
|
}
|
||||||
newPassword.length <= 20 &&
|
|
||||||
newPassword === confirmPassword
|
if (newPassword.value !== confirmPassword.value) {
|
||||||
})
|
uni.showToast({
|
||||||
|
title: '两次输入的密码不一致',
|
||||||
// 提交表单
|
icon: 'none'
|
||||||
const onSubmit = async () => {
|
})
|
||||||
if (!isFormValid.value) {
|
return
|
||||||
uni.showToast({
|
}
|
||||||
title: '请填写完整且正确的密码信息',
|
|
||||||
icon: 'none'
|
// TODO: Call API to change password
|
||||||
})
|
uni.showLoading({ title: '提交中...' })
|
||||||
return
|
setTimeout(() => {
|
||||||
}
|
uni.hideLoading()
|
||||||
|
uni.showToast({
|
||||||
loading.value = true
|
title: '修改成功',
|
||||||
|
icon: 'success'
|
||||||
try {
|
})
|
||||||
// 调用 Supabase 更新密码
|
setTimeout(() => {
|
||||||
const { error } = await supa.auth.updateUser({
|
uni.navigateBack()
|
||||||
password: form.value.newPassword
|
}, 1500)
|
||||||
})
|
}, 1000)
|
||||||
|
|
||||||
if (error !== null) {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
// 成功
|
|
||||||
showSuccess.value = true
|
|
||||||
|
|
||||||
// 清除表单
|
|
||||||
form.value = {
|
|
||||||
currentPassword: '',
|
|
||||||
newPassword: '',
|
|
||||||
confirmPassword: ''
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('修改密码失败:', error)
|
|
||||||
|
|
||||||
let errorMessage = '修改密码失败'
|
|
||||||
if (error.message?.includes('invalid_credentials')) {
|
|
||||||
errorMessage = '当前密码错误'
|
|
||||||
} else if (error.message?.includes('password')) {
|
|
||||||
errorMessage = '新密码不符合要求'
|
|
||||||
}
|
|
||||||
|
|
||||||
uni.showToast({
|
|
||||||
title: errorMessage,
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导航函数
|
|
||||||
const goBack = () => {
|
|
||||||
uni.navigateBack()
|
|
||||||
}
|
|
||||||
|
|
||||||
const goToForgotPassword = () => {
|
|
||||||
uni.navigateTo({
|
|
||||||
url: '/pages/user/forgot-password'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const hideSuccess = () => {
|
|
||||||
showSuccess.value = false
|
|
||||||
goBack()
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.change-password-page {
|
.page-container {
|
||||||
min-height: 100vh;
|
padding: 20px;
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.form-group {
|
||||||
background-color: #ffffff;
|
background-color: #fff;
|
||||||
padding: 15px;
|
border-radius: 8px;
|
||||||
display: flex;
|
padding: 0 15px;
|
||||||
align-items: center;
|
margin-bottom: 30px;
|
||||||
border-bottom: 1px solid #e5e5e5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.back-btn {
|
.input-item {
|
||||||
width: 40px;
|
display: flex;
|
||||||
height: 40px;
|
align-items: center;
|
||||||
display: flex;
|
height: 50px;
|
||||||
align-items: center;
|
border-bottom: 1px solid #eee;
|
||||||
justify-content: center;
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.back-icon {
|
.input-item:last-child {
|
||||||
font-size: 24px;
|
border-bottom: none;
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-title {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #333;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-item {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
background-color: #ffffff;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 15px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
display: block;
|
width: 80px;
|
||||||
font-size: 16px;
|
font-size: 14px;
|
||||||
color: #333;
|
color: #333;
|
||||||
margin-bottom: 10px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
width: 100%;
|
flex: 1;
|
||||||
height: 44px;
|
font-size: 14px;
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 0 15px;
|
|
||||||
font-size: 16px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input:focus {
|
|
||||||
border-color: #007aff;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input:disabled {
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hint {
|
|
||||||
display: block;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #999;
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.submit-btn {
|
.submit-btn {
|
||||||
width: 100%;
|
background-color: #007aff;
|
||||||
height: 50px;
|
color: #fff;
|
||||||
background-color: #007aff;
|
border-radius: 25px;
|
||||||
color: #ffffff;
|
font-size: 16px;
|
||||||
border-radius: 25px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
border: none;
|
|
||||||
margin-top: 30px;
|
|
||||||
}
|
}
|
||||||
|
</style>
|
||||||
.submit-btn:disabled {
|
|
||||||
background-color: #cccccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.forgot-password {
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.forgot-text {
|
|
||||||
color: #007aff;
|
|
||||||
font-size: 14px;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 成功提示模态框 */
|
|
||||||
.success-modal {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.success-content {
|
|
||||||
width: 280px;
|
|
||||||
background-color: #ffffff;
|
|
||||||
border-radius: 15px;
|
|
||||||
padding: 30px 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.success-icon {
|
|
||||||
display: block;
|
|
||||||
width: 60px;
|
|
||||||
height: 60px;
|
|
||||||
line-height: 60px;
|
|
||||||
background-color: #4cd964;
|
|
||||||
color: #ffffff;
|
|
||||||
font-size: 30px;
|
|
||||||
border-radius: 50%;
|
|
||||||
margin: 0 auto 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.success-title {
|
|
||||||
display: block;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #333;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.success-text {
|
|
||||||
display: block;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #666;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.success-btn {
|
|
||||||
width: 100%;
|
|
||||||
height: 44px;
|
|
||||||
background-color: #007aff;
|
|
||||||
color: #ffffff;
|
|
||||||
border-radius: 22px;
|
|
||||||
font-size: 16px;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 响应式优化 */
|
|
||||||
@media screen and (min-width: 768px) {
|
|
||||||
.change-password-page {
|
|
||||||
max-width: 500px;
|
|
||||||
margin: 0 auto;
|
|
||||||
border-left: 1px solid #e5e5e5;
|
|
||||||
border-right: 1px solid #e5e5e5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
padding: 40px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -41,7 +41,7 @@ docker-compose logs auth | grep -i error
|
|||||||
|
|
||||||
### 4. 验证配置是否生效
|
### 4. 验证配置是否生效
|
||||||
|
|
||||||
在 Supabase Dashboard (http://192.168.1.63:3000) 的 SQL Editor 中执行:
|
在 Supabase Dashboard (http://192.168.1.61:3000) 的 SQL Editor 中执行:
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
-- 检查当前配置(需要访问 GoTrue 配置)
|
-- 检查当前配置(需要访问 GoTrue 配置)
|
||||||
@@ -59,7 +59,7 @@ docker-compose logs auth | grep -i error
|
|||||||
确认 `ak/config.uts` 中的配置正确:
|
确认 `ak/config.uts` 中的配置正确:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export const SUPA_URL: string = 'http://192.168.1.63:8000'
|
export const SUPA_URL: string = 'http://192.168.1.61:8000'
|
||||||
export const SUPA_KEY: string = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
|
export const SUPA_KEY: string = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ grep ENABLE_EMAIL_AUTOCONFIRM supabase_pro/.env
|
|||||||
|
|
||||||
## 🔍 验证用户是否创建
|
## 🔍 验证用户是否创建
|
||||||
|
|
||||||
在 Supabase Dashboard (http://192.168.1.63:3000) 的 SQL Editor 中执行:
|
在 Supabase Dashboard (http://192.168.1.61:3000) 的 SQL Editor 中执行:
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
-- 检查最新注册的用户
|
-- 检查最新注册的用户
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
**执行步骤**:
|
**执行步骤**:
|
||||||
|
|
||||||
1. **在 Supabase Dashboard (http://192.168.1.63:3000) 中打开 SQL Editor**
|
1. **在 Supabase Dashboard (http://192.168.1.61:3000) 中打开 SQL Editor**
|
||||||
|
|
||||||
2. **执行 `USER_AUTH_SCHEMA.sql`**
|
2. **执行 `USER_AUTH_SCHEMA.sql`**
|
||||||
- 创建 `ak_users` 表和 RLS 策略
|
- 创建 `ak_users` 表和 RLS 策略
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ docker-compose restart auth
|
|||||||
|
|
||||||
### 方法一:在 Supabase Dashboard 中手动确认
|
### 方法一:在 Supabase Dashboard 中手动确认
|
||||||
|
|
||||||
1. 打开 Supabase Dashboard: http://192.168.1.63:3000
|
1. 打开 Supabase Dashboard: http://192.168.1.61:3000
|
||||||
2. 进入 **Authentication** → **Users**
|
2. 进入 **Authentication** → **Users**
|
||||||
3. 找到对应的用户
|
3. 找到对应的用户
|
||||||
4. 点击用户,在详情页中点击 **Confirm Email** 按钮
|
4. 点击用户,在详情页中点击 **Confirm Email** 按钮
|
||||||
|
|||||||
228
utils/supabaseService.uts
Normal file
228
utils/supabaseService.uts
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
import { createClient } from '@/components/supadb/aksupa.uts'
|
||||||
|
import { SUPA_URL, SUPA_KEY } from '@/ak/config.uts'
|
||||||
|
import type { AkReqResponse } from '@/uni_modules/ak-req/index.uts'
|
||||||
|
|
||||||
|
// 创建 Supabase 客户端
|
||||||
|
const supa = createClient(SUPA_URL, SUPA_KEY)
|
||||||
|
|
||||||
|
// 类型定义
|
||||||
|
export interface Category {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
icon: string
|
||||||
|
description: string
|
||||||
|
color: string
|
||||||
|
created_at?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Product {
|
||||||
|
id: string
|
||||||
|
category_id: string
|
||||||
|
name: string
|
||||||
|
specification: string
|
||||||
|
price: number
|
||||||
|
original_price?: number
|
||||||
|
image?: string
|
||||||
|
manufacturer: string
|
||||||
|
sales: number
|
||||||
|
badge?: string
|
||||||
|
shop_id?: string
|
||||||
|
shop_name?: string
|
||||||
|
created_at?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginatedResponse<T> {
|
||||||
|
data: T[]
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
limit: number
|
||||||
|
hasmore: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
class SupabaseService {
|
||||||
|
// 获取所有分类
|
||||||
|
async getCategories(): Promise<Category[]> {
|
||||||
|
try {
|
||||||
|
const response = await supa
|
||||||
|
.from('categories')
|
||||||
|
.select('*')
|
||||||
|
.order('name', { ascending: true })
|
||||||
|
.execute()
|
||||||
|
|
||||||
|
if (response.error) {
|
||||||
|
console.error('获取分类失败:', response.error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data as Category[]
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取分类异常:', error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取指定分类的商品
|
||||||
|
async getProductsByCategory(
|
||||||
|
categoryId: string,
|
||||||
|
page: number = 1,
|
||||||
|
limit: number = 20
|
||||||
|
): Promise<PaginatedResponse<Product>> {
|
||||||
|
try {
|
||||||
|
const response = await supa
|
||||||
|
.from('products')
|
||||||
|
.select('*', { count: 'exact' })
|
||||||
|
.eq('category_id', categoryId)
|
||||||
|
.order('sales', { ascending: false })
|
||||||
|
.page(page)
|
||||||
|
.limit(limit)
|
||||||
|
.execute()
|
||||||
|
|
||||||
|
if (response.error) {
|
||||||
|
console.error('获取商品失败:', response.error)
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
total: 0,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
hasmore: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: response.data as Product[],
|
||||||
|
total: response.total || 0,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
hasmore: response.hasmore || false
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取商品异常:', error)
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
total: 0,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
hasmore: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索商品
|
||||||
|
async searchProducts(
|
||||||
|
keyword: string,
|
||||||
|
page: number = 1,
|
||||||
|
limit: number = 20
|
||||||
|
): Promise<PaginatedResponse<Product>> {
|
||||||
|
try {
|
||||||
|
const response = await supa
|
||||||
|
.from('products')
|
||||||
|
.select('*', { count: 'exact' })
|
||||||
|
.or(`name.ilike.%${keyword}%,manufacturer.ilike.%${keyword}%,specification.ilike.%${keyword}%`)
|
||||||
|
.order('sales', { ascending: false })
|
||||||
|
.page(page)
|
||||||
|
.limit(limit)
|
||||||
|
.execute()
|
||||||
|
|
||||||
|
if (response.error) {
|
||||||
|
console.error('搜索商品失败:', response.error)
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
total: 0,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
hasmore: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: response.data as Product[],
|
||||||
|
total: response.total || 0,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
hasmore: response.hasmore || false
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('搜索商品异常:', error)
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
total: 0,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
hasmore: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取单个商品详情
|
||||||
|
async getProductById(productId: string): Promise<Product | null> {
|
||||||
|
try {
|
||||||
|
const response = await supa
|
||||||
|
.from('products')
|
||||||
|
.select('*')
|
||||||
|
.eq('id', productId)
|
||||||
|
.single()
|
||||||
|
.execute()
|
||||||
|
|
||||||
|
if (response.error) {
|
||||||
|
console.error('获取商品详情失败:', response.error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data as Product
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取商品详情异常:', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取热销商品
|
||||||
|
async getHotProducts(limit: number = 10): Promise<Product[]> {
|
||||||
|
try {
|
||||||
|
const response = await supa
|
||||||
|
.from('products')
|
||||||
|
.select('*')
|
||||||
|
.order('sales', { ascending: false })
|
||||||
|
.limit(limit)
|
||||||
|
.execute()
|
||||||
|
|
||||||
|
if (response.error) {
|
||||||
|
console.error('获取热销商品失败:', response.error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data as Product[]
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取热销商品异常:', error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取推荐商品(带badge的商品)
|
||||||
|
async getRecommendedProducts(limit: number = 10): Promise<Product[]> {
|
||||||
|
try {
|
||||||
|
const response = await supa
|
||||||
|
.from('products')
|
||||||
|
.select('*')
|
||||||
|
.not('badge', 'is', null)
|
||||||
|
.order('sales', { ascending: false })
|
||||||
|
.limit(limit)
|
||||||
|
.execute()
|
||||||
|
|
||||||
|
if (response.error) {
|
||||||
|
console.error('获取推荐商品失败:', response.error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data as Product[]
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取推荐商品异常:', error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出单例实例
|
||||||
|
export const supabaseService = new SupabaseService()
|
||||||
|
|
||||||
|
// 默认导出
|
||||||
|
export default supabaseService
|
||||||
Reference in New Issue
Block a user