Browse Source

feat: 新增金币兑换功能及相关页面

- 添加金币兑换功能,包括兑换页面和记录页面
- 实现用户登录、验证码验证流程
- 更新路由配置以支持新页面
- 修改请求拦截器添加认证头
- 优化充值页面添加登录状态检查
Shirley 2 weeks ago
parent
commit
08e3632563

+ 24 - 0
src/api/coin.js

@@ -0,0 +1,24 @@
+import request from "@/utils/request";
+
+// 金币兑换给其他用户
+export function integralExchangeGoldCoinToOther(accepterUserId, number) {
+  return request({
+    url: '/web/integralExchangeGoldCoinToOther',
+    method: 'POST',
+    data: {
+      accepterUserId,
+      number
+    }
+  })
+}
+
+// 获取金币兑换记录列表
+export function getIntegralExchangeGoldCoinStreamList(pageNo) {
+  return request({
+    url: '/web/getIntegralExchangeGoldCoinStreamList',
+    method: 'GET',
+    params: {
+      pageNo
+    }
+  })
+}

+ 19 - 1
src/api/user.js

@@ -7,4 +7,22 @@ export function getUserInfo(userId) {
     method: 'get',
     method: 'get',
     params: { userId }
     params: { userId }
   })
   })
-} 
+}
+
+// 获取登录短信验证码
+export function getLoginSmsCode(phoneNumber) {
+  return request({
+    url: '/web/getLoginSmsCode',
+    method: 'get',
+    params: { phoneNumber }
+  })
+}
+
+// 短信验证码登录
+export function smsCodeLoginAccount(phoneNumber, phoneCode) {
+  return request({
+    url: '/web/smsCodeLoginAccount',
+    method: 'post',
+    data: { phoneNumber, phoneCode }
+  })
+}

+ 43 - 18
src/router/index.js

@@ -1,28 +1,53 @@
-import Vue from 'vue'
-import VueRouter from 'vue-router'
+import Vue from "vue";
+import VueRouter from "vue-router";
 
 
-Vue.use(VueRouter)
+Vue.use(VueRouter);
 
 
 const routes = [
 const routes = [
-    {
-    path: '/pay',
-    name: 'Search',
-    component: () => import('../views/Search.vue')
+  {
+    path: "/pay",
+    name: "Search",
+    component: () => import("../views/Search.vue"),
+  },
+  {
+    path: "/",
+    name: "Search",
+    component: () => import("../views/Search.vue"),
+  },
+  {
+    path: "/recharge",
+    name: "Recharge",
+    component: () => import("../views/Recharge.vue"),
+  },
+  {
+    path: "/recharge",
+    name: "Recharge",
+    component: () => import("../views/Recharge.vue"),
+  },
+  {
+    path: "/login",
+    name: "Login",
+    component: () => import("../views/Login.vue"),
   },
   },
   {
   {
-    path: '/',
-    name: 'Search',
-    component: () => import('../views/Search.vue')
+    path: "/verify-code",
+    name: "VerifyCode",
+    component: () => import("../views/VerifyCode.vue"),
   },
   },
   {
   {
-    path: '/recharge',
-    name: 'Recharge',
-    component: () => import('../views/Recharge.vue')
-  }
-]
+    path: "/coin-exchange",
+    name: "CoinExchange",
+    component: () => import("../views/CoinExchange.vue"),
+  },
+  {
+    path: "/coin-exchange-record",
+    name: "CoinExchangeRecord",
+    component: () => import("../views/CoinExchangeRecord.vue"),
+  },
+];
 
 
 const router = new VueRouter({
 const router = new VueRouter({
-  routes
-})
+  routes,
+});
 
 
-export default router 
+export default router;

+ 1 - 1
src/utils/config.js

@@ -3,7 +3,7 @@ const isDev = process.env.NODE_ENV === 'development'
 
 
 // API基础URL配置
 // API基础URL配置
 export const API_BASE_URL = isDev 
 export const API_BASE_URL = isDev 
-  ? 'https://gbyy91.com/api'
+  ? 'https://gbyy91.asia/api'
   : 'https://gbyy91.com/api' // 生产环境 
   : 'https://gbyy91.com/api' // 生产环境 
 
 
 // 支付方式ID
 // 支付方式ID

+ 16 - 1
src/utils/request.js

@@ -12,6 +12,21 @@ const service = axios.create({
 service.interceptors.request.use(
 service.interceptors.request.use(
   config => {
   config => {
     // 在发送请求之前可以做一些处理
     // 在发送请求之前可以做一些处理
+    // 从localStorage获取token和userId
+    const token = localStorage.getItem('token')
+    const userId = localStorage.getItem('userId')
+    
+    // 如果存在token,添加到请求头
+    if (token) {
+      config.headers['Authorization'] = token // `Bearer ${token}`
+      config.headers['token'] = token
+    }
+    
+    // 如果存在userId,添加到请求头
+    if (userId) {
+      config.headers['userId'] = userId
+    }
+    
     return config
     return config
   },
   },
   error => {
   error => {
@@ -43,4 +58,4 @@ service.interceptors.response.use(
   }
   }
 )
 )
 
 
-export default service 
+export default service

+ 646 - 0
src/views/CoinExchange.vue

@@ -0,0 +1,646 @@
+<template>
+  <div class="coin-exchange-page">
+    <!-- 顶部标题 -->
+    <div class="page-header">
+      <div class="header-content">
+        <div class="back-btn" @click="goBack">
+          <van-icon name="arrow-left" size="20" />
+        </div>
+        <div class="header-title">金币兑换</div>
+        <div class="record-btn" @click="goToRecord">
+          兑换记录
+        </div>
+      </div>
+    </div>
+
+    <!-- 金币余额 -->
+    <div class="balance-section">
+      <div class="balance-card">
+        <div class="balance-label">我的金币余额</div>
+        <div class="balance-amount">{{ coinBalance }}</div>
+      </div>
+    </div>
+
+    <!-- 兑换表单 -->
+    <div class="exchange-form">
+      <div class="form-card">
+        <!-- 用户ID输入 -->
+        <div class="form-item">
+          <div class="form-label">输入要兑换的用户ID</div>
+          <van-field
+            v-model="userId"
+            type="tel"
+            placeholder="请输入6位用户ID"
+            maxlength="6"
+            class="user-id-input"
+            @input="onUserIdInput"
+          />
+        </div>
+
+        <!-- 用户信息显示 -->
+        <div v-if="userInfo" class="user-info-section">
+          <div class="user-info-card">
+            <div class="user-avatar">
+              <img v-if="userInfo.avatar" :src="userInfo.avatar" class="avatar" />
+              <div v-else class="avatar-placeholder">
+                <van-icon name="user-o" size="24" />
+              </div>
+            </div>
+            <div class="user-details">
+              <div class="user-name">{{ userInfo.nickname || '未知用户' }}</div>
+              <div class="user-id-display">ID: {{ userInfo.id }}</div>
+            </div>
+          </div>
+        </div>
+
+        <!-- 兑换数量输入 -->
+        <div class="form-item">
+          <div class="form-label">输入要兑换的金币数量</div>
+          <van-field
+            v-model="exchangeAmount"
+            type="number"
+            placeholder="请输入金币数量"
+            class="exchange-input"
+            @input="onExchangeAmountInput"
+          />
+        </div>
+
+        <!-- 兑换结果显示 -->
+        <div v-if="exchangeAmount && exchangeAmount > 0" class="exchange-result">
+          <div class="result-item">
+            <span class="result-label">兑换后获得钻石:</span>
+            <span class="result-value">{{ getDiamondAmount }} 钻石</span>
+          </div>
+        </div>
+
+        <!-- 确认兑换按钮 -->
+        <div class="confirm-btn-container">
+          <van-button 
+            type="primary" 
+            block 
+            round 
+            :disabled="!canExchange"
+            @click="showConfirmDialog"
+            class="confirm-btn"
+          >
+            确认兑换(-{{ exchangeAmount || 0 }}金币)
+          </van-button>
+        </div>
+      </div>
+    </div>
+
+    <!-- 二次确认弹窗 -->
+    <van-dialog
+      v-model="confirmDialogVisible"
+      title="是否确认ID兑换?"
+      show-cancel-button
+      confirm-button-text="确认兑换"
+      cancel-button-text="取消兑换"
+      @confirm="confirmExchange"
+      @cancel="confirmDialogVisible = false"
+      class="confirm-dialog"
+    >
+      <div class="confirm-content">
+        <div class="confirm-user-info">
+          <div class="confirm-avatar">
+            <img v-if="userInfo && userInfo.avatar" :src="userInfo.avatar" class="avatar" />
+            <div v-else class="avatar-placeholder">
+              <van-icon name="user-o" size="32" />
+            </div>
+          </div>
+          <div class="confirm-user-details">
+            <div class="confirm-user-name">{{ userInfo ? userInfo.nickname : '用户昵称' }}</div>
+            <div class="confirm-user-id">ID: {{ userInfo ? userInfo.id : userId }}</div>
+          </div>
+        </div>
+        
+        <div class="confirm-exchange-info">
+          <div class="confirm-item">
+            <span class="confirm-label">兑换的金币数量:</span>
+            <span class="confirm-value">{{ exchangeAmount }}</span>
+          </div>
+          <div class="confirm-item">
+            <span class="confirm-label">兑换后获得:</span>
+            <span class="confirm-value highlight">{{ getDiamondAmount }} <van-icon name="diamond-o" size="16" /></span>
+          </div>
+        </div>
+      </div>
+    </van-dialog>
+  </div>
+</template>
+
+<script>
+import { getUserInfo } from '@/api/user'
+import { integralExchangeGoldCoinToOther } from '@/api/coin'
+
+export default {
+  name: 'CoinExchangePage',
+  data() {
+    return {
+      coinBalance: 12345, // 金币余额,实际应从API获取
+      userId: '',
+      userInfo: null,
+      exchangeAmount: '',
+      confirmDialogVisible: false,
+      exchangeRate: 10 // 1金币 = 10钻石
+    }
+  },
+  computed: {
+    // 计算兑换后获得的钻石数量
+    getDiamondAmount() {
+      const amount = parseInt(this.exchangeAmount) || 0
+      return amount * this.exchangeRate
+    },
+    
+    // 是否可以兑换
+    canExchange() {
+      const amount = parseInt(this.exchangeAmount) || 0
+      return this.userInfo && 
+             amount > 0 && 
+             amount <= this.coinBalance &&
+             this.userId.length === 6
+    }
+  },
+  methods: {
+    // 返回上一页
+    goBack() {
+      this.$router.go(-1)
+    },
+    
+    // 跳转到兑换记录页面
+    goToRecord() {
+      this.$router.push('/coin-exchange-record')
+    },
+    
+    // 用户ID输入处理
+    async onUserIdInput() {
+      // 只允许数字
+      this.userId = this.userId.replace(/\D/g, '')
+      
+      // 当输入6位数字时自动搜索用户
+      if (this.userId.length === 6) {
+        await this.searchUser()
+      } else {
+        this.userInfo = null
+      }
+    },
+    
+    // 搜索用户信息
+    async searchUser() {
+      if (!this.userId || this.userId.length !== 6) {
+        return
+      }
+      
+      try {
+        this.$toast.loading({
+          message: '搜索用户中...',
+          forbidClick: true,
+          duration: 0
+        })
+        
+        // 调用API搜索用户信息
+        const res = await getUserInfo(this.userId)
+        this.$toast.clear()
+        
+        if (res.data && res.data.userId) {
+          this.userInfo = {
+            id: res.data.userId,
+            nickname: res.data.userName || '未知用户',
+            avatar: res.data.avatar || ''
+          }
+        } else {
+          this.userInfo = null
+          this.$toast('未找到该用户')
+        }
+      } catch (error) {
+        this.$toast.clear()
+        this.userInfo = null
+        this.$toast('搜索用户失败')
+        console.error('搜索用户失败:', error)
+      }
+    },
+    
+    // 兑换数量输入处理
+    onExchangeAmountInput() {
+      // 只允许正整数
+      this.exchangeAmount = this.exchangeAmount.replace(/\D/g, '')
+      
+      // 检查是否超过余额
+      const amount = parseInt(this.exchangeAmount) || 0
+      if (amount > this.coinBalance) {
+        this.$toast('兑换数量不能超过余额')
+        this.exchangeAmount = this.coinBalance.toString()
+      }
+    },
+    
+    // 显示确认对话框
+    showConfirmDialog() {
+      if (!this.canExchange) {
+        if (!this.userInfo) {
+          this.$toast('请先输入有效的用户ID')
+        } else if (!this.exchangeAmount || this.exchangeAmount <= 0) {
+          this.$toast('请输入有效的兑换数量')
+        } else if (parseInt(this.exchangeAmount) > this.coinBalance) {
+          this.$toast('兑换数量不能超过余额')
+        }
+        return
+      }
+      
+      this.confirmDialogVisible = true
+    },
+    
+    // 确认兑换
+    async confirmExchange() {
+      // 关闭弹窗
+      this.confirmDialogVisible = false
+      
+      try {
+        this.$toast.loading({
+          message: '兑换中...',
+          forbidClick: true,
+          duration: 0
+        })
+        
+        // 调用兑换API
+        await integralExchangeGoldCoinToOther(
+          this.userId,
+          parseInt(this.exchangeAmount)
+        )
+        
+        this.$toast.clear()
+        this.$toast.success('兑换成功')
+        
+        // 更新余额
+        this.coinBalance -= parseInt(this.exchangeAmount)
+        
+        // 重置表单
+        this.resetForm()
+        
+      } catch (error) {
+        this.$toast.clear()
+        this.$toast.fail('兑换失败,请重试')
+        console.error('兑换失败:', error)
+      }
+    },
+    
+    // 重置表单
+    resetForm() {
+      this.userId = ''
+      this.userInfo = null
+      this.exchangeAmount = ''
+      this.confirmDialogVisible = false
+    }
+  }
+}
+</script>
+
+<style scoped>
+.coin-exchange-page {
+  min-height: 100vh;
+  background-color: var(--background-color);
+}
+
+/* 页面头部 */
+.page-header {
+  background: var(--card-background);
+  padding: 12px 16px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+}
+
+.header-content {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.back-btn {
+  width: 40px;
+  height: 40px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+  color: var(--text-color);
+}
+
+.header-title {
+  font-size: 18px;
+  font-weight: 600;
+  color: var(--text-color);
+}
+
+.record-btn {
+  font-size: 14px;
+  color: var(--primary-color);
+  cursor: pointer;
+  padding: 8px 12px;
+}
+
+.record-btn:hover {
+  opacity: 0.8;
+}
+
+/* 余额部分 */
+.balance-section {
+  padding: 20px 16px;
+}
+
+.balance-card {
+  background: linear-gradient(135deg, var(--primary-color), #9B59FF);
+  border-radius: 16px;
+  padding: 24px;
+  text-align: center;
+  color: white;
+}
+
+.balance-label {
+  font-size: 14px;
+  opacity: 0.9;
+  margin-bottom: 8px;
+}
+
+.balance-amount {
+  font-size: 32px;
+  font-weight: 700;
+}
+
+/* 表单部分 */
+.exchange-form {
+  padding: 0 16px 20px;
+}
+
+.form-card {
+  background: var(--card-background);
+  border-radius: 16px;
+  padding: 24px;
+  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
+}
+
+.form-item {
+  margin-bottom: 24px;
+}
+
+.form-label {
+  font-size: 16px;
+  font-weight: 500;
+  color: var(--text-color);
+  margin-bottom: 12px;
+}
+
+.user-id-input,
+.exchange-input {
+  background: var(--input-background);
+  border-radius: 12px;
+}
+
+.user-id-input :deep(.van-field__control),
+.exchange-input :deep(.van-field__control) {
+  font-size: 16px;
+  color: var(--text-color);
+  padding: 16px;
+}
+
+/* 用户信息显示 */
+.user-info-section {
+  margin-bottom: 24px;
+}
+
+.user-info-card {
+  background: var(--secondary-color);
+  border-radius: 12px;
+  padding: 16px;
+  display: flex;
+  align-items: center;
+}
+
+.user-avatar {
+  width: 48px;
+  height: 48px;
+  border-radius: 50%;
+  overflow: hidden;
+  margin-right: 12px;
+  background: var(--card-background);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.avatar {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+}
+
+.avatar-placeholder {
+  color: var(--text-lighter-color);
+}
+
+.user-details {
+  flex: 1;
+}
+
+.user-name {
+  font-size: 16px;
+  font-weight: 500;
+  color: var(--text-color);
+  margin-bottom: 4px;
+}
+
+.user-id-display {
+  font-size: 14px;
+  color: var(--text-lighter-color);
+}
+
+/* 兑换结果 */
+.exchange-result {
+  background: var(--secondary-color);
+  border-radius: 12px;
+  padding: 16px;
+  margin-bottom: 24px;
+}
+
+.result-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.result-label {
+  font-size: 14px;
+  color: var(--text-color);
+}
+
+.result-value {
+  font-size: 16px;
+  font-weight: 600;
+  color: var(--primary-color);
+}
+
+/* 确认按钮 */
+.confirm-btn-container {
+  margin-top: 32px;
+}
+
+.confirm-btn {
+  height: 48px;
+  font-size: 16px;
+  font-weight: 500;
+  background: var(--primary-color);
+  border: none;
+}
+
+.confirm-btn:disabled {
+  background: var(--text-lighter-color);
+  opacity: 0.6;
+}
+
+/* 确认对话框 */
+.confirm-dialog :deep(.van-dialog) {
+  border-radius: 16px;
+  overflow: hidden;
+}
+
+.confirm-dialog :deep(.van-dialog__header) {
+  padding: 20px 24px 16px;
+  font-size: 16px;
+  font-weight: 500;
+  color: #333;
+}
+
+.confirm-dialog :deep(.van-dialog__content) {
+  padding: 0 24px 24px;
+}
+
+.confirm-dialog :deep(.van-dialog__footer) {
+  padding: 16px 24px 24px;
+  display: flex;
+  gap: 12px;
+}
+
+.confirm-dialog :deep(.van-dialog__cancel) {
+  flex: 1;
+  background: #f5f5f5;
+  color: #666;
+  border: none;
+  border-radius: 24px;
+  height: 48px;
+  font-size: 16px;
+}
+
+.confirm-dialog :deep(.van-dialog__confirm) {
+  flex: 1;
+  background: var(--primary-color);
+  color: white;
+  border: none;
+  border-radius: 24px;
+  height: 48px;
+  font-size: 16px;
+}
+
+.confirm-content {
+  text-align: center;
+}
+
+.confirm-user-info {
+  display: flex;
+  align-items: center;
+  justify-content: flex-start;
+  margin-bottom: 24px;
+  padding: 0;
+}
+
+.confirm-avatar {
+  width: 48px;
+  height: 48px;
+  border-radius: 50%;
+  overflow: hidden;
+  margin-right: 12px;
+  background: #f5f5f5;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.confirm-avatar .avatar {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+}
+
+.confirm-user-details {
+  text-align: left;
+  flex: 1;
+}
+
+.confirm-user-name {
+  font-size: 16px;
+  font-weight: 500;
+  color: #333;
+  margin-bottom: 4px;
+}
+
+.confirm-user-id {
+  font-size: 14px;
+  color: #999;
+}
+
+.confirm-exchange-info {
+  text-align: left;
+}
+
+.confirm-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 16px;
+  padding: 0;
+}
+
+.confirm-label {
+  font-size: 14px;
+  color: var(--text-color);
+}
+
+.confirm-value {
+  font-size: 16px;
+  font-weight: 500;
+  color: var(--text-color);
+}
+
+.confirm-value.highlight {
+  color: var(--primary-color);
+  font-weight: 600;
+  display: flex;
+  align-items: center;
+  gap: 4px;
+}
+
+/* 响应式设计 */
+@media (max-width: 375px) {
+  .balance-card {
+    padding: 20px;
+  }
+  
+  .balance-amount {
+    font-size: 28px;
+  }
+  
+  .form-card {
+    padding: 20px;
+  }
+  
+  .confirm-user-info {
+    flex-direction: column;
+    text-align: center;
+  }
+  
+  .confirm-avatar {
+    margin-right: 0;
+    margin-bottom: 12px;
+  }
+  
+  .confirm-user-details {
+    text-align: center;
+  }
+}
+</style>

+ 439 - 0
src/views/CoinExchangeRecord.vue

@@ -0,0 +1,439 @@
+<template>
+  <div class="coin-exchange-record-page">
+    <!-- 顶部标题 -->
+    <div class="page-header">
+      <div class="header-content">
+        <div class="back-btn" @click="goBack">
+          <van-icon name="arrow-left" size="20" />
+        </div>
+        <div class="header-title">金币兑换记录</div>
+        <div class="header-placeholder"></div>
+      </div>
+    </div>
+
+    <!-- 记录列表 -->
+    <div class="record-container">
+      <div v-if="recordList.length > 0" class="record-list">
+        <div 
+          v-for="(record, index) in recordList" 
+          :key="index"
+          class="record-item"
+        >
+          <div class="record-content">
+            <div class="record-header">
+              <div class="record-id">兑换编号: {{ record.id }}</div>
+              <div class="record-status" :class="record.status">
+                {{ getStatusText(record.status) }}
+              </div>
+            </div>
+            
+            <div class="record-info">
+              <div class="record-user">
+                <div class="user-avatar">
+                  <img v-if="record.targetUser.avatar" :src="record.targetUser.avatar" class="avatar" />
+                  <div v-else class="avatar-placeholder">
+                    <van-icon name="user-o" size="20" />
+                  </div>
+                </div>
+                <div class="user-details">
+                  <div class="user-name">{{ record.targetUser.nickname }}</div>
+                  <div class="user-id">ID: {{ record.targetUser.id }}</div>
+                </div>
+              </div>
+              
+              <div class="exchange-details">
+                <div class="exchange-item">
+                  <span class="label">兑换金币:</span>
+                  <span class="value coin">-{{ record.coinAmount }}</span>
+                </div>
+                <div class="exchange-item">
+                  <span class="label">获得钻石:</span>
+                  <span class="value diamond">+{{ record.diamondAmount }}</span>
+                </div>
+              </div>
+            </div>
+            
+            <div class="record-footer">
+              <div class="record-time">{{ formatTime(record.createTime) }}</div>
+            </div>
+          </div>
+        </div>
+      </div>
+      
+      <!-- 空状态 -->
+      <div v-else class="empty-state">
+        <div class="empty-icon">
+          <van-icon name="records" size="64" color="#CCCCCC" />
+        </div>
+        <div class="empty-text">暂无兑换记录</div>
+        <div class="empty-desc">您还没有进行过金币兑换</div>
+      </div>
+    </div>
+
+    <!-- 加载更多 -->
+    <div v-if="hasMore && recordList.length > 0" class="load-more">
+      <van-button 
+        type="default" 
+        size="small" 
+        :loading="loading"
+        @click="loadMore"
+        class="load-more-btn"
+      >
+        {{ loading ? '加载中...' : '加载更多' }}
+      </van-button>
+    </div>
+  </div>
+</template>
+
+<script>
+import { getIntegralExchangeGoldCoinStreamList } from '@/api/coin'
+
+export default {
+  name: 'CoinExchangeRecordPage',
+  data() {
+    return {
+      recordList: [],
+      loading: false,
+      hasMore: true,
+      page: 1,
+      pageSize: 50
+    }
+  },
+  created() {
+    this.loadRecords()
+  },
+  methods: {
+    // 返回上一页
+    goBack() {
+      this.$router.go(-1)
+    },
+    
+    // 加载兑换记录
+    async loadRecords(isLoadMore = false) {
+      if (this.loading) return
+      
+      this.loading = true
+      
+      try {
+        const res = await getIntegralExchangeGoldCoinStreamList(this.page)
+        
+        if (isLoadMore) {
+          this.recordList.push(...res.data.list)
+        } else {
+          this.recordList = res.data.list
+        }
+        
+        this.hasMore = res.data.hasMore
+        
+        if (isLoadMore && res.data.list.length > 0) {
+          // 页码在loadMore方法中已经递增
+        }
+        
+      } catch (error) {
+        this.$toast.fail('加载失败,请重试')
+        console.error('加载兑换记录失败:', error)
+        
+        // 加载失败时恢复页码
+        if (isLoadMore && this.page > 1) {
+          this.page--
+        }
+      } finally {
+        this.loading = false
+      }
+    },
+    
+    // 加载更多
+    async loadMore() {
+      this.page++
+      await this.loadRecords(true)
+    },
+    
+    // 获取状态文本
+    getStatusText(status) {
+      const statusMap = {
+        'success': '兑换成功',
+        'pending': '兑换中',
+        'failed': '兑换失败'
+      }
+      return statusMap[status] || '未知状态'
+    },
+    
+    // 格式化时间
+    formatTime(timestamp) {
+      const date = new Date(timestamp)
+      const now = new Date()
+      const diff = now - date
+      
+      // 一天内显示时分
+      if (diff < 24 * 60 * 60 * 1000) {
+        return date.toLocaleTimeString('zh-CN', {
+          hour: '2-digit',
+          minute: '2-digit'
+        })
+      }
+      
+      // 超过一天显示月日
+      return date.toLocaleDateString('zh-CN', {
+        month: '2-digit',
+        day: '2-digit',
+        hour: '2-digit',
+        minute: '2-digit'
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.coin-exchange-record-page {
+  min-height: 100vh;
+  background-color: var(--background-color);
+}
+
+/* 页面头部 */
+.page-header {
+  background: var(--card-background);
+  padding: 12px 16px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+}
+
+.header-content {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.back-btn {
+  width: 40px;
+  height: 40px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+  color: var(--text-color);
+}
+
+.header-title {
+  font-size: 18px;
+  font-weight: 600;
+  color: var(--text-color);
+}
+
+.header-placeholder {
+  width: 40px;
+}
+
+/* 记录容器 */
+.record-container {
+  padding: 16px;
+}
+
+.record-list {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+/* 记录项 */
+.record-item {
+  background: var(--card-background);
+  border-radius: 12px;
+  overflow: hidden;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+}
+
+.record-content {
+  padding: 16px;
+}
+
+.record-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 12px;
+}
+
+.record-id {
+  font-size: 14px;
+  color: var(--text-lighter-color);
+}
+
+.record-status {
+  font-size: 12px;
+  padding: 4px 8px;
+  border-radius: 12px;
+  font-weight: 500;
+}
+
+.record-status.success {
+  background: #E8F5E8;
+  color: #52C41A;
+}
+
+.record-status.pending {
+  background: #FFF7E6;
+  color: #FA8C16;
+}
+
+.record-status.failed {
+  background: #FFF2F0;
+  color: #FF4D4F;
+}
+
+/* 记录信息 */
+.record-info {
+  margin-bottom: 12px;
+}
+
+.record-user {
+  display: flex;
+  align-items: center;
+  margin-bottom: 12px;
+}
+
+.user-avatar {
+  width: 40px;
+  height: 40px;
+  border-radius: 50%;
+  overflow: hidden;
+  margin-right: 12px;
+  background: var(--input-background);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.avatar {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+}
+
+.avatar-placeholder {
+  color: var(--text-lighter-color);
+}
+
+.user-details {
+  flex: 1;
+}
+
+.user-name {
+  font-size: 16px;
+  font-weight: 500;
+  color: var(--text-color);
+  margin-bottom: 2px;
+}
+
+.user-id {
+  font-size: 12px;
+  color: var(--text-lighter-color);
+}
+
+/* 兑换详情 */
+.exchange-details {
+  background: var(--input-background);
+  border-radius: 8px;
+  padding: 12px;
+}
+
+.exchange-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 8px;
+}
+
+.exchange-item:last-child {
+  margin-bottom: 0;
+}
+
+.label {
+  font-size: 14px;
+  color: var(--text-color);
+}
+
+.value {
+  font-size: 14px;
+  font-weight: 600;
+}
+
+.value.coin {
+  color: #FF6B6B;
+}
+
+.value.diamond {
+  color: var(--primary-color);
+}
+
+/* 记录底部 */
+.record-footer {
+  display: flex;
+  justify-content: flex-end;
+}
+
+.record-time {
+  font-size: 12px;
+  color: var(--text-lighter-color);
+}
+
+/* 空状态 */
+.empty-state {
+  text-align: center;
+  padding: 60px 20px;
+}
+
+.empty-icon {
+  margin-bottom: 16px;
+}
+
+.empty-text {
+  font-size: 16px;
+  color: var(--text-color);
+  margin-bottom: 8px;
+}
+
+.empty-desc {
+  font-size: 14px;
+  color: var(--text-lighter-color);
+}
+
+/* 加载更多 */
+.load-more {
+  text-align: center;
+  padding: 20px;
+}
+
+.load-more-btn {
+  background: var(--card-background);
+  border: 1px solid var(--border-color);
+  color: var(--text-color);
+  padding: 8px 24px;
+  border-radius: 20px;
+}
+
+.load-more-btn:hover {
+  border-color: var(--primary-color);
+  color: var(--primary-color);
+}
+
+/* 响应式设计 */
+@media (max-width: 375px) {
+  .record-content {
+    padding: 12px;
+  }
+  
+  .record-header {
+    flex-direction: column;
+    align-items: flex-start;
+    gap: 8px;
+  }
+  
+  .exchange-details {
+    padding: 10px;
+  }
+  
+  .exchange-item {
+    font-size: 13px;
+  }
+}
+</style>

+ 313 - 0
src/views/Login.vue

@@ -0,0 +1,313 @@
+<template>
+  <div class="login-page">
+    <!-- 顶部标题 -->
+    <div class="page-header">
+      <div class="header-content">
+        <div class="logo-wrapper">
+          <div class="logo-container">
+            <img src="@/assets/images/logo.png" alt="告白语音" class="logo-image" />
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 登录表单 -->
+    <div class="login-container">
+      <div class="login-card">
+        <div class="login-title">手机号登录</div>
+
+        <!-- 手机号输入 -->
+        <div class="input-group">
+          <div class="country-code">+86</div>
+          <van-field v-model="phoneNumber" type="tel" placeholder="请输入手机号" maxlength="11" class="phone-input"
+            :border="false" />
+        </div>
+
+        <!-- 获取验证码按钮 -->
+        <div class="verify-btn-container">
+          <van-button type="primary" block round :disabled="!isPhoneValid || isCountingDown" @click="getVerifyCode"
+            class="verify-btn">
+            {{ countDownText }}
+          </van-button>
+        </div>
+
+        <!-- 协议条款 -->
+        <div class="agreement-section">
+          <div class="agreement-text">
+            登录即表示同意
+            <span class="agreement-link" @click="showUserAgreement">《用户协议》</span>
+            和
+            <span class="agreement-link" @click="showPrivacyPolicy">《隐私政策》</span>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 用户协议弹框 -->
+    <van-dialog v-model="userAgreementVisible" title="用户协议" confirm-button-text="我已阅读" :show-cancel-button="false">
+      <div class="agreement-content">
+        <iframe v-if="userAgreementVisible" src="https://gbyy91.com/agreement/user_agreement.html" frameborder="0"
+          class="agreement-iframe"></iframe>
+      </div>
+    </van-dialog>
+
+    <!-- 隐私政策弹框 -->
+    <van-dialog v-model="privacyPolicyVisible" title="隐私政策" confirm-button-text="我已阅读" :show-cancel-button="false">
+      <div class="agreement-content">
+        <iframe v-if="privacyPolicyVisible" src="https://gbyy91.com/agreement/personal_info_protect.html"
+          frameborder="0" class="agreement-iframe"></iframe>
+      </div>
+    </van-dialog>
+  </div>
+</template>
+
+<script>
+import { getLoginSmsCode } from '@/api/user'
+
+export default {
+  name: "LoginPage",
+  data() {
+    return {
+      phoneNumber: "",
+      isCountingDown: false,
+      countDown: 60,
+      userAgreementVisible: false,
+      privacyPolicyVisible: false,
+    };
+  },
+  computed: {
+    // 验证手机号格式
+    isPhoneValid() {
+      const phoneRegex = /^1[3-9]\d{9}$/;
+      return phoneRegex.test(this.phoneNumber);
+    },
+    // 倒计时文本
+    countDownText() {
+      if (this.isCountingDown) {
+        return `${this.countDown}s后重新获取`;
+      }
+      return "获取验证码";
+    },
+  },
+  methods: {
+    // 获取验证码
+    async getVerifyCode() {
+      if (!this.isPhoneValid) {
+        this.$toast("请输入正确的手机号");
+        return;
+      }
+
+      try {
+        this.$toast.loading({
+          message: "发送中...",
+          forbidClick: true,
+          duration: 0,
+        });
+
+        // 调用发送验证码的API
+        await getLoginSmsCode(this.phoneNumber)
+
+        this.$toast.clear();
+        this.$toast.success("验证码已发送");
+
+        // 开始倒计时
+        this.startCountDown();
+
+        // 跳转到验证码页面
+        this.$router.push({
+          path: "/verify-code",
+          query: {
+            phone: this.phoneNumber,
+          },
+        });
+      } catch (error) {
+        this.$toast.clear();
+        this.$toast.fail("发送失败,请重试");
+        console.error("发送验证码失败:", error);
+      }
+    },
+
+    // 开始倒计时
+    startCountDown() {
+      this.isCountingDown = true;
+      this.countDown = 60;
+
+      const timer = setInterval(() => {
+        this.countDown--;
+        if (this.countDown <= 0) {
+          clearInterval(timer);
+          this.isCountingDown = false;
+          this.countDown = 60;
+        }
+      }, 1000);
+    },
+
+    // 显示用户协议
+    showUserAgreement() {
+      this.userAgreementVisible = true;
+    },
+
+    // 显示隐私政策
+    showPrivacyPolicy() {
+      this.privacyPolicyVisible = true;
+    },
+  },
+};
+</script>
+
+<style scoped>
+.login-page {
+  min-height: 100vh;
+  background-color: var(--background-color);
+}
+
+/* 页面头部 */
+.page-header {
+  padding: 20px 16px;
+  text-align: center;
+}
+
+.header-content {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.logo-wrapper {
+  display: flex;
+  justify-content: center;
+}
+
+.logo-container {
+  width: 100px;
+  height: 100px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.logo-image {
+  width: 100%;
+  height: 100%;
+  object-fit: contain;
+}
+
+/* 登录容器 */
+.login-container {
+  padding: 40px 20px;
+}
+
+.login-card {
+  background: var(--card-background);
+  border-radius: 16px;
+  padding: 32px 24px;
+  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
+}
+
+.login-title {
+  font-size: 24px;
+  font-weight: 600;
+  color: var(--text-color);
+  text-align: left;
+  margin-bottom: 40px;
+}
+
+/* 输入组 */
+.input-group {
+  display: flex;
+  align-items: center;
+  background: var(--input-background);
+  border-radius: 24px;
+  padding: 4px 20px;
+  margin-bottom: 32px;
+  border: 1px solid var(--border-color);
+}
+
+.country-code {
+  font-size: 16px;
+  color: var(--text-color);
+  margin-right: 12px;
+  font-weight: 500;
+}
+
+.phone-input {
+  flex: 1;
+  background: transparent;
+}
+
+.phone-input :deep(.van-field__control) {
+  font-size: 16px;
+  color: var(--text-color);
+  background: transparent;
+}
+
+.phone-input :deep(.van-field__control::placeholder) {
+  color: var(--text-lighter-color);
+}
+
+/* 验证码按钮 */
+.verify-btn-container {
+  margin-bottom: 32px;
+}
+
+.verify-btn {
+  height: 48px;
+  font-size: 16px;
+  font-weight: 500;
+  background: var(--primary-color);
+  border: none;
+}
+
+.verify-btn:disabled {
+  background: var(--text-lighter-color);
+  opacity: 0.6;
+}
+
+/* 协议部分 */
+.agreement-section {
+  text-align: center;
+}
+
+.agreement-text {
+  font-size: 12px;
+  color: var(--text-lighter-color);
+  line-height: 1.5;
+}
+
+.agreement-link {
+  color: var(--primary-color);
+  text-decoration: none;
+}
+
+.agreement-link:hover {
+  text-decoration: underline;
+}
+
+/* 协议内容 */
+.agreement-content {
+  height: 400px;
+  overflow: hidden;
+}
+
+.agreement-iframe {
+  width: 100%;
+  height: 100%;
+  border: none;
+}
+
+/* 响应式设计 */
+@media (max-width: 375px) {
+  .login-container {
+    padding: 30px 16px;
+  }
+
+  .login-card {
+    padding: 24px 20px;
+  }
+
+  .login-title {
+    font-size: 20px;
+    margin-bottom: 32px;
+  }
+}
+</style>

+ 62 - 51
src/views/Recharge.vue

@@ -10,7 +10,7 @@
         </div>
         </div>
       </div>
       </div>
     </div>
     </div>
-    
+
     <!-- 用户信息 -->
     <!-- 用户信息 -->
     <div class="user-info-section">
     <div class="user-info-section">
       <div class="user-info-card">
       <div class="user-info-card">
@@ -29,24 +29,24 @@
             <span>{{ userInfo.id || '未知ID' }}</span>
             <span>{{ userInfo.id || '未知ID' }}</span>
           </div>
           </div>
         </div>
         </div>
-        <div class="switch-account-btn" @click="switchUser">
-          切换账号
+        <div class="action-buttons">
+          <van-button type="primary" size="small" round @click="goToCoinExchange" class="action-btn">
+            金币兑换
+          </van-button>
+          <van-button type="primary" size="small" round @click="logout" class="action-btn logout-btn">
+            退出登录
+          </van-button>
         </div>
         </div>
       </div>
       </div>
     </div>
     </div>
-    
+
     <!-- 充值金额选择 -->
     <!-- 充值金额选择 -->
     <div class="recharge-section">
     <div class="recharge-section">
       <h2 class="section-title">充值金额</h2>
       <h2 class="section-title">充值金额</h2>
-      
+
       <div class="recharge-grid">
       <div class="recharge-grid">
-        <div 
-          v-for="(item, index) in rechargeOptionsWithCustom" 
-          :key="index"
-          class="recharge-item"
-          :class="{ active: selectedRecharge === index }"
-          @click="selectRecharge(index)"
-        >
+        <div v-for="(item, index) in rechargeOptionsWithCustom" :key="index" class="recharge-item"
+          :class="{ active: selectedRecharge === index }" @click="selectRecharge(index)">
           <div class="diamond-icon">
           <div class="diamond-icon">
             <img src="@/assets/images/recharge_zuanshi.png" alt="钻石" class="diamond-img" />
             <img src="@/assets/images/recharge_zuanshi.png" alt="钻石" class="diamond-img" />
           </div>
           </div>
@@ -62,16 +62,10 @@
       </div>
       </div>
 
 
       <div v-if="isCustomSelected" class="custom-input">
       <div v-if="isCustomSelected" class="custom-input">
-        <van-field
-          v-model="customAmount"
-          type="number"
-          input-align="center"
-          placeholder="请输入自定义金额(元)"
-          clearable
-        />
+        <van-field v-model="customAmount" type="number" input-align="center" placeholder="请输入自定义金额(元)" clearable />
       </div>
       </div>
     </div>
     </div>
-    
+
     <!-- 支付方式 -->
     <!-- 支付方式 -->
     <div class="payment-section">
     <div class="payment-section">
       <div class="payment-option">
       <div class="payment-option">
@@ -84,7 +78,7 @@
         </div>
         </div>
       </div>
       </div>
     </div>
     </div>
-    
+
     <!-- 温馨提示 -->
     <!-- 温馨提示 -->
     <div class="tips-section">
     <div class="tips-section">
       <div class="tips-title">温馨提示:</div>
       <div class="tips-title">温馨提示:</div>
@@ -94,7 +88,7 @@
         <div class="tip-item">3. 大额充值,支付方式可选择"零钱通"</div>
         <div class="tip-item">3. 大额充值,支付方式可选择"零钱通"</div>
         <div class="tip-item">4. 如有疑问,请联系服务号客服</div>
         <div class="tip-item">4. 如有疑问,请联系服务号客服</div>
       </div>
       </div>
-      
+
       <!-- 协议勾选 -->
       <!-- 协议勾选 -->
       <div class="agreement-checkbox" @click="toggleAgreement">
       <div class="agreement-checkbox" @click="toggleAgreement">
         <div class="checkbox-wrapper" :class="{ checked: agreementChecked }">
         <div class="checkbox-wrapper" :class="{ checked: agreementChecked }">
@@ -105,22 +99,18 @@
         </div>
         </div>
       </div>
       </div>
     </div>
     </div>
-    
+
     <!-- 支付按钮 -->
     <!-- 支付按钮 -->
     <div class="pay-button">
     <div class="pay-button">
       <van-button type="primary" block round @click="confirmPay">确认支付</van-button>
       <van-button type="primary" block round @click="confirmPay">确认支付</van-button>
     </div>
     </div>
-    
+
     <!-- 协议弹框 -->
     <!-- 协议弹框 -->
-    <van-dialog
-      v-model="agreementVisible"
-      title="用户充值协议"
-      confirm-button-text="我已阅读"
-      @confirm="agreementChecked = true"
-      :show-cancel-button="false"
-    >
+    <van-dialog v-model="agreementVisible" title="用户充值协议" confirm-button-text="我已阅读" @confirm="agreementChecked = true"
+      :show-cancel-button="false">
       <div class="agreement-content">
       <div class="agreement-content">
-        <iframe v-if="agreementVisible" src="https://gbyy91.com/agreement/recharge.html" frameborder="0" class="agreement-iframe"></iframe>
+        <iframe v-if="agreementVisible" src="https://gbyy91.com/agreement/recharge.html" frameborder="0"
+          class="agreement-iframe"></iframe>
       </div>
       </div>
     </van-dialog>
     </van-dialog>
   </div>
   </div>
@@ -169,7 +159,7 @@ export default {
   created() {
   created() {
     // 获取用户信息
     // 获取用户信息
     this.initUserInfo()
     this.initUserInfo()
-    
+
     // 获取产品列表
     // 获取产品列表
     this.fetchProductList()
     this.fetchProductList()
   },
   },
@@ -209,7 +199,7 @@ export default {
           this.$router.replace('/')
           this.$router.replace('/')
         }
         }
       }
       }
-      
+
       // 保存最后使用的用户ID
       // 保存最后使用的用户ID
       if (this.userInfo.id) {
       if (this.userInfo.id) {
         localStorage.setItem('lastUserId', this.userInfo.id)
         localStorage.setItem('lastUserId', this.userInfo.id)
@@ -254,12 +244,12 @@ export default {
     },
     },
     confirmPay() {
     confirmPay() {
       if (this.isLoading) return // 防止重复点击
       if (this.isLoading) return // 防止重复点击
-      
+
       if (!this.userInfo.id) {
       if (!this.userInfo.id) {
         this.$toast('请先选择用户')
         this.$toast('请先选择用户')
         return
         return
       }
       }
-      
+
       // 检查协议是否勾选
       // 检查协议是否勾选
       if (!this.agreementChecked) {
       if (!this.agreementChecked) {
         this.$toast('请阅读并同意充值协议')
         this.$toast('请阅读并同意充值协议')
@@ -274,9 +264,9 @@ export default {
           return
           return
         }
         }
       }
       }
-      
+
       const selectedOption = this.selectedRechargeOption
       const selectedOption = this.selectedRechargeOption
-      
+
       // 使用二次确认弹窗
       // 使用二次确认弹窗
       this.$dialog.confirm({
       this.$dialog.confirm({
         title: '支付确认',
         title: '支付确认',
@@ -304,7 +294,7 @@ export default {
         // 取消支付
         // 取消支付
       })
       })
     },
     },
-    
+
     // 请求支付宝支付
     // 请求支付宝支付
     async requestAliPay(product) {
     async requestAliPay(product) {
       // 显示加载提示
       // 显示加载提示
@@ -314,27 +304,27 @@ export default {
         forbidClick: true,
         forbidClick: true,
         duration: 0
         duration: 0
       })
       })
-      
+
       try {
       try {
         // 获取平台类型
         // 获取平台类型
         const platform = getPlatformType()
         const platform = getPlatformType()
-        
+
         // 构建支付参数
         // 构建支付参数
         const payData = {
         const payData = {
           userId: this.userInfo.id,
           userId: this.userInfo.id,
           productId: product && product.isCustom ? 1018 : product.id
           productId: product && product.isCustom ? 1018 : product.id
         }
         }
-        
+
         // 自定义金额时追加 amount
         // 自定义金额时追加 amount
         if (product && product.isCustom) {
         if (product && product.isCustom) {
           payData.amount = Number(this.customAmount)
           payData.amount = Number(this.customAmount)
         }
         }
-        
+
         // 调用支付宝H5支付接口
         // 调用支付宝H5支付接口
         const res = await aliPayH5(payData, platform)
         const res = await aliPayH5(payData, platform)
         this.$toast.clear()
         this.$toast.clear()
         this.isLoading = false
         this.isLoading = false
-        
+
         // 优先尝试解析服务端 JSON 错误并提示
         // 优先尝试解析服务端 JSON 错误并提示
         try {
         try {
           if (typeof res === 'string') {
           if (typeof res === 'string') {
@@ -353,14 +343,14 @@ export default {
         } catch (e) {
         } catch (e) {
           // 忽略解析错误,继续后续流程
           // 忽略解析错误,继续后续流程
         }
         }
-        
+
         // 处理HTML表单响应
         // 处理HTML表单响应
         if (typeof res === 'string' && res.includes('<form')) {
         if (typeof res === 'string' && res.includes('<form')) {
           // 创建一个div来放置表单
           // 创建一个div来放置表单
           const div = document.createElement('div')
           const div = document.createElement('div')
           div.innerHTML = res
           div.innerHTML = res
           document.body.appendChild(div)
           document.body.appendChild(div)
-          
+
           // 自动提交表单
           // 自动提交表单
           setTimeout(() => {
           setTimeout(() => {
             const form = div.querySelector('form')
             const form = div.querySelector('form')
@@ -380,24 +370,37 @@ export default {
         this.$toast.fail('支付请求失败,请重试')
         this.$toast.fail('支付请求失败,请重试')
       }
       }
     },
     },
-    
+
     // 处理支付成功
     // 处理支付成功
     handlePaymentSuccess() {
     handlePaymentSuccess() {
       console.log('支付成功')
       console.log('支付成功')
       this.$toast.success('支付成功')
       this.$toast.success('支付成功')
       window.location.href = 'https://gbyy91.com/pay'
       window.location.href = 'https://gbyy91.com/pay'
     },
     },
-    
+
     // 处理支付失败
     // 处理支付失败
     handlePaymentFailure(message) {
     handlePaymentFailure(message) {
       console.error('支付失败:', message)
       console.error('支付失败:', message)
       this.$toast.fail(message || '支付失败,请重试')
       this.$toast.fail(message || '支付失败,请重试')
     },
     },
-    
+
     // 处理支付取消
     // 处理支付取消
     handlePaymentCancel() {
     handlePaymentCancel() {
       console.log('用户取消支付')
       console.log('用户取消支付')
       this.$toast('支付已取消')
       this.$toast('支付已取消')
+    },
+
+    // 金币兑换
+    goToCoinExchange() {
+      this.$router.push('/coin-exchange')
+    },
+
+    // 退出登录
+    logout() {
+      // 清除本地存储的用户信息
+      localStorage.removeItem('lastUserId')
+      // 跳转到搜索页面
+      this.$router.push('/')
     }
     }
   }
   }
 }
 }
@@ -482,7 +485,8 @@ export default {
   position: relative;
   position: relative;
 }
 }
 
 
-.avatar, .avatar-placeholder {
+.avatar,
+.avatar-placeholder {
   width: 40px;
   width: 40px;
   height: 40px;
   height: 40px;
   border-radius: 50%;
   border-radius: 50%;
@@ -534,6 +538,13 @@ export default {
   box-shadow: 0 2px 6px rgba(182, 111, 255, 0.3);
   box-shadow: 0 2px 6px rgba(182, 111, 255, 0.3);
 }
 }
 
 
+/* 操作按钮样式 */
+.action-buttons {
+  display: flex;
+  gap: 8px;
+  margin-left: 8px;
+}
+
 /* 充值金额样式 */
 /* 充值金额样式 */
 .recharge-section {
 .recharge-section {
   padding: 0 8px;
   padding: 0 8px;
@@ -854,4 +865,4 @@ export default {
   color: #f44336;
   color: #f44336;
   font-weight: normal;
   font-weight: normal;
 }
 }
-</style> 
+</style>

+ 108 - 52
src/views/Search.vue

@@ -10,35 +10,33 @@
         </div>
         </div>
       </div>
       </div>
     </div>
     </div>
-    
-    
+
+
     <!-- 搜索框 -->
     <!-- 搜索框 -->
-    <div class="search-container">
+    <div v-if="isLoggedIn" class="search-container">
       <div class="search-input-card">
       <div class="search-input-card">
         <div class="search-input">
         <div class="search-input">
-          <van-field
-            v-model="searchValue"
-            placeholder="搜索Id号码"
-            clearable
-            class="user-search-field"
-          />
+          <van-field v-model="searchValue" placeholder="搜索Id号码" clearable class="user-search-field" />
           <van-button type="primary" class="search-btn" @click="onSearch">搜索</van-button>
           <van-button type="primary" class="search-btn" @click="onSearch">搜索</van-button>
         </div>
         </div>
       </div>
       </div>
     </div>
     </div>
-    
+
+    <!-- 未登录提示 -->
+    <div v-if="!isLoggedIn" class="login-prompt-section">
+      <div class="login-prompt-card">
+        <div class="prompt-text">请先完成登录后,即可进行钻石充值~</div>
+      </div>
+    </div>
+
     <!-- 充值金额选择 -->
     <!-- 充值金额选择 -->
     <div class="recharge-section">
     <div class="recharge-section">
       <h2 class="section-title">充值金额</h2>
       <h2 class="section-title">充值金额</h2>
-      
+
       <div class="recharge-grid">
       <div class="recharge-grid">
-        <div 
-          v-for="(item, index) in coinOptionsWithCustom" 
-          :key="index"
-          class="recharge-item"
-          :class="{ active: selectedRecharge === index }"
-          @click="selectRecharge(index)"
-        >
+        <div v-for="(item, index) in coinOptionsWithCustom" :key="index" class="recharge-item"
+          :class="{ active: selectedRecharge === index, disabled: !isLoggedIn }"
+          @click="isLoggedIn ? selectRecharge(index) : null">
           <div class="diamond-icon">
           <div class="diamond-icon">
             <img src="@/assets/images/recharge_zuanshi.png" alt="钻石" class="diamond-img" />
             <img src="@/assets/images/recharge_zuanshi.png" alt="钻石" class="diamond-img" />
           </div>
           </div>
@@ -54,18 +52,13 @@
       </div>
       </div>
 
 
       <div v-if="isCustomSelected" class="custom-input">
       <div v-if="isCustomSelected" class="custom-input">
-        <van-field
-          v-model="customAmount"
-          type="number"
-          input-align="center"
-          placeholder="请输入自定义金额(元)"
-          clearable
-        />
+        <van-field v-model="customAmount" type="number" input-align="center" placeholder="请输入自定义金额(元)" clearable
+          :disabled="!isLoggedIn" />
       </div>
       </div>
     </div>
     </div>
-    
+
     <!-- 支付方式 -->
     <!-- 支付方式 -->
-    <div class="payment-section">
+    <div v-if="isLoggedIn" class="payment-section">
       <div class="payment-option">
       <div class="payment-option">
         <div class="payment-icon">
         <div class="payment-icon">
           <img src="@/assets/images/recharge_paypal.png" alt="支付宝支付" class="alipay-icon" />
           <img src="@/assets/images/recharge_paypal.png" alt="支付宝支付" class="alipay-icon" />
@@ -76,7 +69,12 @@
         </div>
         </div>
       </div>
       </div>
     </div>
     </div>
-    
+
+    <!-- 立即登录按钮 -->
+    <div v-if="!isLoggedIn" class="login-button">
+      <van-button type="primary" block round @click="goToLogin">立即登录</van-button>
+    </div>
+
     <!-- 温馨提示 -->
     <!-- 温馨提示 -->
     <div class="tips-section">
     <div class="tips-section">
       <div class="tips-title">温馨提示:</div>
       <div class="tips-title">温馨提示:</div>
@@ -86,33 +84,29 @@
         <div class="tip-item">3. 大额充值,请确保支付宝账户余额充足</div>
         <div class="tip-item">3. 大额充值,请确保支付宝账户余额充足</div>
         <div class="tip-item">4. 如有疑问,请联系服务号客服</div>
         <div class="tip-item">4. 如有疑问,请联系服务号客服</div>
       </div>
       </div>
-      
+
       <!-- 协议勾选 -->
       <!-- 协议勾选 -->
-      <div class="agreement-checkbox" @click="toggleAgreement">
-        <div class="checkbox-wrapper" :class="{ checked: agreementChecked }">
+      <div class="agreement-checkbox" :class="{ disabled: !isLoggedIn }" @click="isLoggedIn ? toggleAgreement() : null">
+        <div class="checkbox-wrapper" :class="{ checked: agreementChecked, disabled: !isLoggedIn }">
           <van-icon v-if="agreementChecked" name="success" size="14" color="#fff" />
           <van-icon v-if="agreementChecked" name="success" size="14" color="#fff" />
         </div>
         </div>
         <div class="agreement-text">
         <div class="agreement-text">
-          我已阅读并同意 <span class="agreement-link" @click.stop="showAgreement">《用户充值协议》</span>
+          我已阅读并同意 <span class="agreement-link" @click.stop="isLoggedIn ? showAgreement() : null">《用户充值协议》</span>
         </div>
         </div>
       </div>
       </div>
     </div>
     </div>
-    
+
     <!-- 支付按钮 -->
     <!-- 支付按钮 -->
-    <div class="pay-button">
+    <div v-if="isLoggedIn" class="pay-button">
       <van-button type="primary" block round @click="confirmPay">确认支付</van-button>
       <van-button type="primary" block round @click="confirmPay">确认支付</van-button>
     </div>
     </div>
-    
+
     <!-- 协议弹框 -->
     <!-- 协议弹框 -->
-    <van-dialog
-      v-model="agreementVisible"
-      title="用户充值协议"
-      confirm-button-text="我已阅读"
-      @confirm="agreementChecked = true"
-      :show-cancel-button="false"
-    >
+    <van-dialog v-model="agreementVisible" title="用户充值协议" confirm-button-text="我已阅读" @confirm="agreementChecked = true"
+      :show-cancel-button="false">
       <div class="agreement-content">
       <div class="agreement-content">
-        <iframe v-if="agreementVisible" src="https://gbyy91.com/agreement/recharge.html" frameborder="0" class="agreement-iframe"></iframe>
+        <iframe v-if="agreementVisible" src="https://gbyy91.com/agreement/recharge.html" frameborder="0"
+          class="agreement-iframe"></iframe>
       </div>
       </div>
     </van-dialog>
     </van-dialog>
   </div>
   </div>
@@ -137,7 +131,9 @@ export default {
       agreementChecked: false,
       agreementChecked: false,
       agreementVisible: false,
       agreementVisible: false,
       // 自定义金额
       // 自定义金额
-      customAmount: ''
+      customAmount: '',
+      // 登录状态
+      isLoggedIn: false
     }
     }
   },
   },
   created() {
   created() {
@@ -185,7 +181,7 @@ export default {
         this.$toast('请输入用户ID')
         this.$toast('请输入用户ID')
         return
         return
       }
       }
-      
+
       try {
       try {
         // 显示加载提示
         // 显示加载提示
         this.$toast.loading({
         this.$toast.loading({
@@ -193,26 +189,32 @@ export default {
           forbidClick: true,
           forbidClick: true,
           duration: 0
           duration: 0
         })
         })
-        
+
         // 从接口获取用户信息
         // 从接口获取用户信息
         const res = await getUserInfo(this.searchValue)
         const res = await getUserInfo(this.searchValue)
         this.$toast.clear()
         this.$toast.clear()
-        
+
         if (res.data && res.data.userId) {
         if (res.data && res.data.userId) {
+          // 设置登录状态为已登录
+          this.isLoggedIn = true
           // 跳转到充值页面,并传递用户信息
           // 跳转到充值页面,并传递用户信息
           this.$router.push({
           this.$router.push({
             path: '/recharge',
             path: '/recharge',
-            query: { 
+            query: {
               id: res.data.userId,
               id: res.data.userId,
               nickname: res.data.userName,
               nickname: res.data.userName,
               avatar: res.data.avatar
               avatar: res.data.avatar
             }
             }
           })
           })
         } else {
         } else {
+          // 设置登录状态为未登录
+          this.isLoggedIn = false
           this.$toast('未找到该用户')
           this.$toast('未找到该用户')
         }
         }
       } catch (error) {
       } catch (error) {
         this.$toast.clear()
         this.$toast.clear()
+        // 设置登录状态为未登录
+        this.isLoggedIn = false
         console.error('获取用户信息失败:', error)
         console.error('获取用户信息失败:', error)
         this.$toast('搜索用户失败,请重试')
         this.$toast('搜索用户失败,请重试')
       }
       }
@@ -233,7 +235,7 @@ export default {
         this.$toast('请先搜索用户')
         this.$toast('请先搜索用户')
         return
         return
       }
       }
-      
+
       // 检查协议是否勾选
       // 检查协议是否勾选
       if (!this.agreementChecked) {
       if (!this.agreementChecked) {
         this.$toast('请阅读并同意充值协议')
         this.$toast('请阅读并同意充值协议')
@@ -248,12 +250,12 @@ export default {
           return
           return
         }
         }
       }
       }
-      
+
       const selectedOption = this.selectedRechargeOption
       const selectedOption = this.selectedRechargeOption
       const priceToShow = selectedOption && selectedOption.isCustom
       const priceToShow = selectedOption && selectedOption.isCustom
         ? (Number(this.customAmount) || 0)
         ? (Number(this.customAmount) || 0)
         : (selectedOption?.price || 0)
         : (selectedOption?.price || 0)
-      
+
       // 使用二次确认弹窗
       // 使用二次确认弹窗
       this.$dialog.confirm({
       this.$dialog.confirm({
         title: '支付确认',
         title: '支付确认',
@@ -282,6 +284,10 @@ export default {
       }).catch(() => {
       }).catch(() => {
         // 取消支付
         // 取消支付
       })
       })
+    },
+    // 跳转到登录页面
+    goToLogin() {
+      this.$router.push('/login')
     }
     }
   }
   }
 }
 }
@@ -637,6 +643,21 @@ export default {
   box-shadow: 0 4px 12px rgba(182, 111, 255, 0.3);
   box-shadow: 0 4px 12px rgba(182, 111, 255, 0.3);
 }
 }
 
 
+/* 立即登录按钮样式 */
+.login-button {
+  padding: 0 8px;
+  margin-bottom: 15px;
+}
+
+.login-button .van-button {
+  background-color: #B66FFF;
+  border: none;
+  height: 38px;
+  font-size: 15px;
+  font-weight: 500;
+  box-shadow: 0 4px 12px rgba(182, 111, 255, 0.3);
+}
+
 /* 二次确认弹窗样式 */
 /* 二次确认弹窗样式 */
 :deep(.custom-dialog) {
 :deep(.custom-dialog) {
   width: 90%;
   width: 90%;
@@ -722,4 +743,39 @@ export default {
   color: #f44336;
   color: #f44336;
   font-weight: normal;
   font-weight: normal;
 }
 }
-</style> 
+
+/* 未登录提示样式 */
+.login-prompt-section {
+  margin: 15px;
+  margin-bottom: 20px;
+}
+
+.login-prompt-card {
+  background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%);
+  border: 1px solid #ffeaa7;
+  border-radius: 12px;
+  padding: 15px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  box-shadow: 0 2px 8px rgba(255, 234, 167, 0.3);
+}
+
+.prompt-text {
+  font-size: 14px;
+  color: #856404;
+  font-weight: 500;
+}
+
+/* 禁用状态样式 */
+.recharge-item.disabled {
+  opacity: 0.5;
+  pointer-events: none;
+}
+
+.checkbox-wrapper.disabled,
+.agreement-checkbox.disabled {
+  opacity: 0.5;
+  pointer-events: none;
+}
+</style>

+ 471 - 0
src/views/VerifyCode.vue

@@ -0,0 +1,471 @@
+<template>
+  <div class="verify-code-page">
+    <!-- 顶部标题 -->
+    <div class="page-header">
+      <div class="header-content">
+        <div class="back-btn" @click="goBack">
+          <van-icon name="arrow-left" size="20" />
+        </div>
+        <div class="logo-wrapper">
+          <div class="logo-container">
+            <img src="@/assets/images/logo.png" alt="告白语音" class="logo-image" />
+          </div>
+        </div>
+        <div class="header-placeholder"></div>
+      </div>
+    </div>
+
+    <!-- 验证码输入 -->
+    <div class="verify-container">
+      <div class="verify-card">
+        <div class="verify-title">输入验证码</div>
+
+        <div class="phone-info">
+          <span class="phone-label">验证码已发送至:</span>
+          <span class="phone-number">{{ formatPhone(phoneNumber) }}</span>
+        </div>
+
+        <div class="tip-info">
+          <span>为确保您本人操作,请输入验证码以验证身份</span>
+        </div>
+
+        <!-- 验证码输入框 -->
+        <div class="code-input-container">
+          <div class="code-inputs">
+            <input v-for="(digit, index) in verifyCode" :key="index" :ref="'input' + index" v-model="verifyCode[index]"
+              type="tel" maxlength="1" class="code-input" @input="onCodeInput(index, $event)"
+              @keydown="onKeyDown(index, $event)" @focus="onInputFocus(index)" />
+          </div>
+        </div>
+
+        <!-- 重新发送 -->
+        <div class="resend-container">
+          <div v-if="isCountingDown" class="countdown-text">
+            {{ countDown }}s后可重新发送
+          </div>
+          <div v-else class="resend-btn" @click="resendCode">
+            重新发送验证码
+          </div>
+        </div>
+
+        <!-- 确认按钮 -->
+        <div class="confirm-btn-container">
+          <van-button type="primary" block round :disabled="!isCodeComplete" @click="confirmVerifyCode"
+            class="confirm-btn">
+            确认
+          </van-button>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { smsCodeLoginAccount } from '@/api/user'
+
+export default {
+  name: "VerifyCodePage",
+  data() {
+    return {
+      phoneNumber: "",
+      verifyCode: ["", "", "", "", "", ""],
+      isCountingDown: true,
+      countDown: 60,
+      timer: null,
+    };
+  },
+  computed: {
+    // 验证码是否输入完整
+    isCodeComplete() {
+      return this.verifyCode.every((digit) => digit !== "");
+    },
+
+    // 完整验证码
+    fullVerifyCode() {
+      return this.verifyCode.join("");
+    },
+  },
+  created() {
+    // 获取手机号
+    this.phoneNumber = this.$route.query.phone || "";
+
+    // 开始倒计时
+    this.startCountDown();
+  },
+  beforeDestroy() {
+    // 清除定时器
+    if (this.timer) {
+      clearInterval(this.timer);
+    }
+  },
+  methods: {
+    // 返回上一页
+    goBack() {
+      this.$router.go(-1);
+    },
+
+    // 格式化手机号显示
+    formatPhone(phone) {
+      if (!phone) return "";
+      return phone.replace(/(\d{3})(\d{4})(\d{4})/, "$1****$3");
+    },
+
+    // 验证码输入处理
+    onCodeInput(index, event) {
+      const value = event.target.value;
+
+      // 只允许数字
+      if (!/^\d*$/.test(value)) {
+        this.verifyCode[index] = "";
+        return;
+      }
+
+      this.verifyCode[index] = value;
+
+      // 自动跳转到下一个输入框
+      if (value && index < 5) {
+        this.$nextTick(() => {
+          const nextInput = this.$refs["input" + (index + 1)];
+          if (nextInput && nextInput[0]) {
+            nextInput[0].focus();
+          }
+        });
+      }
+
+      // 如果输入完整,自动验证
+      if (this.isCodeComplete) {
+        this.$nextTick(() => {
+          this.confirmVerifyCode();
+        });
+      }
+    },
+
+    // 键盘事件处理
+    onKeyDown(index, event) {
+      // 退格键处理
+      if (event.key === "Backspace" && !this.verifyCode[index] && index > 0) {
+        const prevInput = this.$refs["input" + (index - 1)];
+        if (prevInput && prevInput[0]) {
+          prevInput[0].focus();
+        }
+      }
+    },
+
+    // 输入框获得焦点
+    onInputFocus(index) {
+      // 选中当前输入框的内容
+      this.$nextTick(() => {
+        const input = this.$refs["input" + index];
+        if (input && input[0]) {
+          input[0].select();
+        }
+      });
+    },
+
+    // 开始倒计时
+    startCountDown() {
+      this.isCountingDown = true;
+      this.countDown = 60;
+
+      this.timer = setInterval(() => {
+        this.countDown--;
+        if (this.countDown <= 0) {
+          clearInterval(this.timer);
+          this.isCountingDown = false;
+          this.countDown = 60;
+        }
+      }, 1000);
+    },
+
+    // 重新发送验证码
+    async resendCode() {
+      try {
+        this.$toast.loading({
+          message: "发送中...",
+          forbidClick: true,
+          duration: 0,
+        });
+
+        // 这里调用重新发送验证码的API
+        // await sendVerifyCode(this.phoneNumber)
+
+        this.$toast.clear();
+        this.$toast.success("验证码已重新发送");
+
+        // 重新开始倒计时
+        this.startCountDown();
+
+        // 清空验证码输入
+        this.verifyCode = ["", "", "", "", "", ""];
+
+        // 聚焦第一个输入框
+        this.$nextTick(() => {
+          const firstInput = this.$refs["input0"];
+          if (firstInput && firstInput[0]) {
+            firstInput[0].focus();
+          }
+        });
+      } catch (error) {
+        this.$toast.clear();
+        this.$toast.fail("发送失败,请重试");
+        console.error("重新发送验证码失败:", error);
+      }
+    },
+
+    // 确认验证码
+    async confirmVerifyCode() {
+      if (!this.isCodeComplete) {
+        this.$toast("请输入完整的验证码");
+        return;
+      }
+
+      try {
+        this.$toast.loading({
+          message: "验证中...",
+          forbidClick: true,
+          duration: 0,
+        });
+
+        // 调用短信验证码登录API
+        const res = await smsCodeLoginAccount(this.phoneNumber, this.fullVerifyCode)
+
+        this.$toast.clear();
+        this.$toast.success("登录成功");
+
+        // 将用户数据存储到localStorage
+        if (res.data) {
+          localStorage.setItem('userId', res.data.userId)
+          localStorage.setItem('userName', res.data.userName)
+          localStorage.setItem('avatar', res.data.avatar || '')
+          localStorage.setItem('integral', res.data.integral || '0')
+          localStorage.setItem('token', res.data.token)
+        }
+
+        // 验证成功后跳转到充值页面,传递用户信息
+        this.$router.replace({
+          path: '/recharge',
+          query: {
+            id: res.data.userId,
+            nickname: res.data.userName,
+            avatar: res.data.avatar || ''
+          }
+        });
+      } catch (error) {
+        this.$toast.clear();
+        this.$toast.fail("验证码错误,请重新输入");
+
+        // 清空验证码输入
+        this.verifyCode = ["", "", "", "", "", ""];
+
+        // 聚焦第一个输入框
+        this.$nextTick(() => {
+          const firstInput = this.$refs["input0"];
+          if (firstInput && firstInput[0]) {
+            firstInput[0].focus();
+          }
+        });
+
+        console.error("验证码验证失败:", error);
+      }
+    },
+  },
+};
+</script>
+
+<style scoped>
+.verify-code-page {
+  min-height: 100vh;
+  background-color: var(--background-color);
+}
+
+/* 页面头部 */
+.page-header {
+  padding: 20px 16px;
+}
+
+.header-content {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.back-btn {
+  width: 40px;
+  height: 40px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+  color: var(--text-color);
+}
+
+.logo-wrapper {
+  display: flex;
+  justify-content: center;
+  flex: 1;
+}
+
+.logo-container {
+  width: 60px;
+  height: 60px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.logo-image {
+  width: 100%;
+  height: 100%;
+  object-fit: contain;
+}
+
+.header-placeholder {
+  width: 40px;
+}
+
+/* 验证码容器 */
+.verify-container {
+  padding: 40px 10px;
+}
+
+.verify-card {
+  background: var(--card-background);
+  border-radius: 16px;
+  padding: 32px 24px;
+  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
+}
+
+.verify-title {
+  font-size: 24px;
+  font-weight: 600;
+  color: var(--text-color);
+  text-align: center;
+  margin-bottom: 24px;
+}
+
+/* 手机号信息 */
+.phone-info {
+  text-align: left;
+  margin-bottom: 4px;
+}
+
+.phone-label {
+  font-size: 14px;
+  color: var(--text-lighter-color);
+}
+
+.phone-number {
+  font-size: 16px;
+  color: var(--text-color);
+  font-weight: 500;
+}
+
+.tip-info {
+  margin-bottom: 30px;
+}
+
+.tip-info span {
+  font-size: 14px;
+  color: var(--text-lighter-color);
+}
+
+/* 验证码输入 */
+.code-input-container {
+  margin-bottom: 32px;
+}
+
+.code-inputs {
+  display: flex;
+  justify-content: space-between;
+  /* gap: 10px; */
+}
+
+.code-input {
+  width: 46px;
+  height: 46px;
+  border: 2px solid var(--border-color);
+  border-radius: 8px;
+  text-align: center;
+  font-size: 24px;
+  font-weight: 600;
+  color: var(--text-color);
+  background: var(--card-background);
+  outline: none;
+  transition: all 0.3s;
+}
+
+.code-input:focus {
+  border-color: var(--primary-color);
+  box-shadow: 0 0 0 2px rgba(182, 111, 255, 0.1);
+}
+
+.code-input:not(:placeholder-shown) {
+  border-color: var(--primary-color);
+}
+
+/* 重新发送 */
+.resend-container {
+  text-align: center;
+  margin-bottom: 32px;
+}
+
+.countdown-text {
+  font-size: 14px;
+  color: var(--text-lighter-color);
+}
+
+.resend-btn {
+  font-size: 14px;
+  color: var(--primary-color);
+  cursor: pointer;
+  text-decoration: underline;
+}
+
+.resend-btn:hover {
+  opacity: 0.8;
+}
+
+/* 确认按钮 */
+.confirm-btn-container {
+  margin-bottom: 16px;
+}
+
+.confirm-btn {
+  height: 48px;
+  font-size: 16px;
+  font-weight: 500;
+  background: var(--primary-color);
+  border: none;
+}
+
+.confirm-btn:disabled {
+  background: var(--text-lighter-color);
+  opacity: 0.6;
+}
+
+/* 响应式设计 */
+@media (max-width: 375px) {
+  .verify-container {
+    padding: 30px 16px;
+  }
+
+  .verify-card {
+    padding: 24px 20px;
+  }
+
+  .verify-title {
+    font-size: 20px;
+    margin-bottom: 20px;
+  }
+
+  .code-inputs {
+    gap: 8px;
+  }
+
+  .code-input {
+    width: 40px;
+    height: 52px;
+    font-size: 20px;
+  }
+}
+</style>

BIN
src/views/views.zip