合并代码

This commit is contained in:
szz
2025-07-31 13:55:52 +08:00
parent 60a8fd2669
commit eaa4ca33f2
15 changed files with 3173 additions and 0 deletions

486
src/views/MyLogin.vue Normal file
View File

@@ -0,0 +1,486 @@
<template>
<div class="login-page">
<div class="login-container">
<div class="login-card">
<div class="login-header">
<h2>用户登录</h2>
<p>欢迎来到炬融圈</p>
</div>
<div class="image">
<img src="/imgs/login.png" alt="炬融圈">
</div>
<el-form
ref="loginFormRef"
:model="loginForm"
:rules="loginRules"
class="login-form"
@submit.prevent="handleLogin"
>
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
placeholder="请输入用户名或邮箱"
size="large"
:prefix-icon="User"
clearable
/>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
placeholder="请输入密码"
size="large"
:prefix-icon="Lock"
show-password
clearable
@keyup.enter="handleLogin"
/>
</el-form-item>
<el-form-item prop="captcha">
<Captcha
ref="captchaRef"
v-model="loginForm.captcha"
placeholder="请输入验证码"
size="large"
/>
</el-form-item>
<el-form-item>
<div class="form-options">
<el-checkbox v-model="rememberMe">记住我</el-checkbox>
<el-link type="primary" @click="showForgotPassword">
忘记密码
</el-link>
</div>
</el-form-item>
<el-form-item>
<el-button
type="primary"
size="large"
class="login-button"
:loading="userStore.loading"
@click="handleLogin"
>
{{ userStore.loading ? '登录中...' : '登录' }}
</el-button>
</el-form-item>
</el-form>
<div class="login-footer">
<p>
还没有账号
<el-link type="primary" @click="$router.push('/register')">
立即注册
</el-link>
</p>
</div>
<div class="quick-login">
<el-divider>快速登录</el-divider>
<div class="demo-accounts">
<el-button
type="info"
plain
size="small"
@click="quickLogin('admin')"
>
管理员账号
</el-button>
<el-button
type="success"
plain
size="small"
@click="quickLogin('user')"
>
普通用户
</el-button>
</div>
</div>
</div>
</div>
<!-- 背景装饰 -->
<div class="background-decoration">
<div class="decoration-circle circle-1"></div>
<div class="decoration-circle circle-2"></div>
<div class="decoration-circle circle-3"></div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { ElMessage, ElMessageBox } from 'element-plus'
import { User, Lock } from '@element-plus/icons-vue'
import Captcha from '@/components/Captcha.vue'
const router = useRouter()
const route = useRoute()
const userStore = useUserStore()
// 表单引用
const loginFormRef = ref()
const captchaRef = ref()
// 表单数据
const loginForm = reactive({
username: '',
password: '',
captcha: ''
})
// 其他状态
const rememberMe = ref(false)
// 表单验证规则
const loginRules = {
username: [
{ required: true, message: '请输入用户名或邮箱', trigger: 'blur' },
{ min: 3, message: '用户名至少3个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, message: '密码至少6个字符', trigger: 'blur' }
],
captcha: [
{ required: true, message: '请输入验证码', trigger: 'blur' },
{ min: 4, max: 4, message: '验证码为4位字符', trigger: 'blur' }
]
}
// 处理登录
const handleLogin = async () => {
if (!loginFormRef.value || !captchaRef.value) return
try {
// 先验证表单
const valid = await loginFormRef.value.validate()
if (!valid) return
// 验证验证码
const captchaValid = await captchaRef.value.verifyCaptcha(loginForm.captcha)
if (!captchaValid) {
loginForm.captcha = ''
return
}
// 获取验证码信息
const captchaInfo = captchaRef.value.getCaptchaInfo()
// 提交登录请求(包含验证码信息)
const loginData = {
username: loginForm.username,
password: loginForm.password,
captchaId: captchaInfo.captchaId,
captchaText: captchaInfo.captchaText
}
const result = await userStore.login(loginData)
if (result.success) {
// 登录成功,跳转到目标页面或转账管理
const redirectPath = route.query.redirect || '/mainpage'
router.push(redirectPath)
}
} catch (error) {
console.error('登录失败:', error)
// 登录失败后刷新验证码
if (captchaRef.value) {
await captchaRef.value.refreshCaptcha()
}
loginForm.captcha = ''
}
}
// 快速登录(演示用)
const quickLogin = async (type) => {
if (type === 'admin') {
loginForm.username = 'admin'
loginForm.password = 'admin123'
} else {
loginForm.username = 'user'
loginForm.password = 'user123'
}
// 清空验证码,让用户手动输入
loginForm.captcha = ''
ElMessage.info('请输入验证码后登录')
}
// 忘记密码
const showForgotPassword = () => {
ElMessageBox.alert(
'请联系管理员重置密码,或使用演示账号进行体验。',
'忘记密码',
{
confirmButtonText: '确定',
type: 'info'
}
)
}
// 组件挂载时的处理
onMounted(() => {
// 如果已经登录,直接跳转
if (userStore.isAuthenticated) {
const redirectPath = route.query.redirect || '/transfers'
router.push(redirectPath)
}
// 从localStorage恢复记住我状态
const savedUsername = localStorage.getItem('rememberedUsername')
if (savedUsername) {
loginForm.username = savedUsername
rememberMe.value = true
}
})
// 监听记住我状态变化
const handleRememberMe = () => {
if (rememberMe.value && loginForm.username) {
localStorage.setItem('rememberedUsername', loginForm.username)
} else {
localStorage.removeItem('rememberedUsername')
}
}
</script>
<style scoped>
/* 移除三个输入框背景 */
:deep(.el-input__wrapper) {
background: transparent !important;
border-radius: 8px;
}
/* 输入框聚焦及hover效果 */
:deep(.el-input__wrapper:hover),
:deep(.el-input__wrapper.is-focus) {
box-shadow: 0 0 0 1px #409eff inset;
}
/* 错误状态样式 */
:deep(.el-form-item.is-error .el-input__wrapper) {
box-shadow: 0 0 0 1px #f56c6c inset;
}
/* 成功状态样式 */
:deep(.el-form-item.is-success .el-input__wrapper) {
box-shadow: 0 0 0 1px #67c23a inset;
}
/* 其他原有样式保持不变 */
.login-page {
min-height: 100vh;
background: linear-gradient(135deg, #abbaff 0%, #f3f3f3 100%);
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}
.login-container {
width: 100%;
max-width: 400px;
padding: 20px;
position: relative;
z-index: 10;
}
.login-card {
backdrop-filter: blur(10px);
border-radius: 16px;
padding: 40px 30px;
}
.login-header {
text-align: center;
margin-bottom: 30px;
}
.login-header h2 {
color: #4784ff;
margin-bottom: 8px;
font-weight: 600;
}
.login-header p {
color: white;
font-size: 14px;
}
/* 图片容器样式 */
.image {
width: 375px; /* 固定宽度 */
height: 287px; /* 固定高度 */
margin: 0 auto 20px; /* 水平居中,底部留出间距 */
overflow: hidden; /* 隐藏超出容器的部分 */
border-radius: 8px; /* 可选:添加圆角增强视觉效果 */
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); /* 可选:添加轻微阴影 */
}
/* 图片填充样式 */
.image img {
width: 100%; /* 宽度充满容器 */
height: 100%; /* 高度充满容器 */
object-fit: contain;
display: block; /* 去除图片底部默认空白 */
}
/* 响应式适配(小屏设备自动缩放) */
@media (max-width: 375px) {
.image {
width: 100%; /* 在小于375px的屏幕上宽度自适应 */
height: auto; /* 高度按比例自动计算 */
aspect-ratio: 375 / 287; /* 保持原比例 */
}
}
.login-form {
margin-bottom: 20px;
}
.form-options {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.login-button {
width: 100%;
height: 44px;
font-size: 16px;
font-weight: 600;
}
.login-footer {
text-align: center;
margin-bottom: 20px;
}
.login-footer p {
color: #606266;
font-size: 14px;
}
.quick-login {
margin-top: 20px;
}
.demo-accounts {
display: flex;
gap: 10px;
justify-content: center;
margin-top: 15px;
}
.background-decoration {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.decoration-circle {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
animation: float 6s ease-in-out infinite;
}
.circle-1 {
width: 200px;
height: 200px;
top: 10%;
left: 10%;
animation-delay: 0s;
}
.circle-2 {
width: 150px;
height: 150px;
top: 60%;
right: 10%;
animation-delay: 2s;
}
.circle-3 {
width: 100px;
height: 100px;
bottom: 20%;
left: 20%;
animation-delay: 4s;
}
@keyframes float {
0%, 100% {
transform: translateY(0px) rotate(0deg);
}
50% {
transform: translateY(-20px) rotate(180deg);
}
}
/* 响应式设计 */
@media (max-width: 480px) {
.login-container {
padding: 15px;
}
.login-card {
padding: 30px 20px;
}
.demo-accounts {
flex-direction: column;
}
.form-options {
flex-direction: column;
gap: 10px;
align-items: flex-start;
}
}
/* Element Plus 组件样式覆盖 */
:deep(.el-button) {
border-radius: 8px;
}
:deep(.el-divider__text) {
background-color: rgba(255, 255, 255, 0.95);
color: #909399;
}
/* 加载状态样式 */
.login-button.is-loading {
pointer-events: none;
}
/* 动画效果 */
.login-card {
animation: slideInUp 0.6s ease-out;
}
@keyframes slideInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>

894
src/views/MyMatching.vue Normal file
View File

@@ -0,0 +1,894 @@
<template>
<div class="container">
<div class="spacer"></div>
<div class="main-content">
<!-- 资金匹配部分 -->
<div class="matching-section">
<div class="section-title">
<h3>资金匹配</h3>
<div class="toggle-switch">
<span class="toggle-label">开启大额匹配</span>
<label class="switch">
<input type="checkbox" v-model="matchingType" true-value="large" false-value="small">
<span class="slider round"></span>
</label>
</div>
</div>
<!-- 小额匹配信息 -->
<div v-if="matchingType === 'small'" class="matching-info">
<div class="info-item">
<span class="label">匹配总额:</span>
<span class="value">¥5,000.00</span>
</div>
<div class="info-item">
<span class="label">分配笔数:</span>
<span class="value">3</span>
</div>
<div class="info-item">
<span class="label">单笔范围:</span>
<span class="value">¥1,000 - ¥5,000</span>
</div>
<button
@click="createOrder"
:disabled="creating"
class="create-btn"
>
{{ creating ? '匹配中...' : '开始匹配' }}
</button>
<div class="tips">
<p> 系统将为您匹配3笔转账总金额5000元</p>
<p> 优先匹配已完成出款的用户</p>
<p> 每笔金额随机分配确保资金循环</p>
</div>
</div>
<!-- 大额匹配信息 -->
<div v-if="matchingType === 'large'" class="matching-info">
<div class="info-item">
<span class="label">自定义金额:</span>
<div class="custom-amount-input">
<el-input
v-model="customAmount"
type="number"
:min="5000"
:max="50000"
step="100"
placeholder="5000-50000"
>
<template #prepend>¥</template>
</el-input>
</div>
</div>
<div class="info-item">
<span class="label">分配规则:</span>
<span class="value">{{ getLargeMatchingRule() }}</span>
</div>
<div class="info-item">
<span class="label">预计笔数:</span>
<span class="value">{{ getLargeMatchingCount() }}</span>
</div>
<button
@click="createOrder"
:disabled="creating || !isValidCustomAmount"
class="create-btn"
>
{{ creating ? '匹配中...' : '开始匹配' }}
</button>
<div class="tips">
<p> 金额范围5000-50000</p>
<p> 15000元以下分成3笔随机金额</p>
<p> 15000元以上随机分拆每笔1000-8000</p>
<p> 优先匹配已完成出款的用户</p>
</div>
</div>
</div>
<!-- 我的匹配订单 -->
<div class="orders-section">
<div class="section-title">
<h3>匹配订单</h3>
<router-link to="/transfers">
<span class="view-all">查看全部 ></span>
</router-link>
</div>
<div class="orders-list">
<div
v-for="order in matchingOrders"
:key="order.id"
class="order-card"
@click="viewOrderDetail(order.id)"
>
<div class="order-header">
<span class="order-id">#{{ order.id }}</span>
<span v-if="order.is_system_reverse" class="system-reverse-tag">系统反向匹配</span>
<span :class="['status', order.status]">{{ getStatusText(order.status) }}</span>
</div>
<div class="order-info">
<p>金额: ¥{{ order.amount }}</p>
<p>发起人: {{ order.initiator_name }}</p>
<p v-if="!order.is_system_reverse">轮次: {{ order.cycle_count + 1 }}/{{ order.max_cycles }}</p>
<p v-if="order.is_system_reverse" class="system-note">系统自动发起向负余额用户补充资金</p>
<p>创建时间: {{ formatDate(order.created_at) }}</p>
</div>
</div>
</div>
<div v-if="matchingOrders.length === 0" class="empty-state">
<p>暂无匹配订单</p>
</div>
</div>
</div>
<!-- 订单详情弹窗 -->
<div v-if="showOrderDetail" class="modal-overlay" @click="closeOrderDetail">
<div class="modal-content" @click.stop>
<div class="modal-header">
<h3>订单详情 #{{ selectedOrder?.order?.id }}</h3>
<button @click="closeOrderDetail" class="close-btn">×</button>
</div>
<div class="modal-body" v-if="selectedOrder">
<div class="order-summary">
<p><strong>状态:</strong> {{ getStatusText(selectedOrder.order.status) }}</p>
<p><strong>金额:</strong> ¥{{ selectedOrder.order.amount }}</p>
<p><strong>发起人:</strong> {{ selectedOrder.order.initiator_name }}</p>
<p><strong>轮次:</strong> {{ selectedOrder.order.cycle_count + 1 }}/{{ selectedOrder.order.max_cycles }}</p>
</div>
<div class="allocations-section">
<h4>分配详情</h4>
<div class="allocation-timeline">
<div
v-for="allocation in selectedOrder.allocations"
:key="allocation.id"
class="timeline-item"
>
<div class="timeline-content">
<div class="timeline-header">
<span class="cycle">{{ allocation.cycle_number }}</span>
<span :class="['status', allocation.status]">{{ getStatusText(allocation.status) }}</span>
</div>
<p>{{ allocation.from_user_name }} {{ allocation.to_user_name }}</p>
<p class="amount">¥{{ allocation.amount }}</p>
</div>
</div>
</div>
</div>
<div class="records-section">
<h4>操作记录</h4>
<div class="records-list">
<div
v-for="record in selectedOrder.records"
:key="record.id"
class="record-item"
>
<div class="record-info">
<span class="action">{{ getActionText(record.action) }}</span>
<span class="user">{{ record.username }}</span>
<span class="time">{{ formatDate(record.created_at) }}</span>
</div>
<div v-if="record.amount" class="record-amount">¥{{ record.amount }}</div>
<div v-if="record.note" class="record-note">{{ record.note }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import api from '../utils/api'
import { uploadURL, getImageUrl, getUploadConfig } from '@/config'
export default {
name: 'Matching',
data() {
return {
stats: {
userStats: null
},
creating: false,
processing: false,
matchingOrders: [],
showOrderDetail: false,
selectedOrder: null,
matchingType: 'small', // 匹配类型small(小额) 或 large(大额)
customAmount: '' // 大额匹配自定义金额
}
},
async mounted() {
await this.loadData()
},
methods: {
async loadData() {
try {
await this.loadMatchingOrders()
} catch (error) {
console.error('加载数据失败:', error)
this.$message.error('加载数据失败')
}
},
/**
* 创建匹配订单
*/
async createOrder() {
this.creating = true
try {
// 构建请求参数
const requestData = {
matchingType: this.matchingType
}
// 如果是大额匹配,添加自定义金额
if (this.matchingType === 'large') {
if (!this.isValidCustomAmount) {
this.$message.error('请输入有效的匹配金额5000-50000元')
return
}
requestData.customAmount = parseFloat(this.customAmount)
}
await api.post('/matching/create', requestData)
const successMessage = this.matchingType === 'small'
? '小额匹配成功已为您生成3笔转账分配'
: `大额匹配成功!已为您生成${this.getLargeMatchingCount()}笔转账分配`
this.$message.success(successMessage)
await this.loadData()
} catch (error) {
console.error('创建匹配订单失败:', error)
const errorMessage = error.response?.data?.message || '匹配失败,请稍后重试'
// 检查是否是审核相关的错误
if (errorMessage.includes('审核') || errorMessage.includes('上传') || errorMessage.includes('完善')) {
this.$confirm(errorMessage + ',是否前往个人中心完善资料?', '提示', {
confirmButtonText: '前往完善',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$router.push('/profile')
}).catch(() => {})
} else {
this.$message.error(errorMessage)
}
} finally {
this.creating = false
}
},
async viewOrderDetail(orderId) {
try {
const response = await api.get(`/matching/order/${orderId}`)
this.selectedOrder = response.data.data
this.showOrderDetail = true
} catch (error) {
console.error('获取订单详情失败:', error)
this.$message.error('获取订单详情失败')
}
},
closeOrderDetail() {
this.showOrderDetail = false
this.selectedOrder = null
},
async loadMatchingOrders() {
try {
const response = await api.get('/matching/my-orders')
this.matchingOrders = response.data.data || []
} catch (error) {
console.error('加载匹配订单失败:', error)
}
},
getStatusText(status) {
const statusMap = {
pending: '待处理',
matching: '匹配中',
completed: '已完成',
cancelled: '已取消',
confirmed: '已确认',
rejected: '已拒绝',
failed: '匹配失败'
}
return statusMap[status] || status
},
getActionText(action) {
const actionMap = {
join: '加入',
confirm: '确认',
reject: '拒绝',
complete: '完成'
}
return actionMap[action] || action
},
formatDate(dateString) {
return new Date(dateString).toLocaleString('zh-CN')
},
/**
* 格式化金额显示,确保数字安全
* @param {number|string|null|undefined} amount - 金额值
* @returns {string} 格式化后的金额字符串
*/
formatAmount(amount) {
const num = parseFloat(amount)
return isNaN(num) ? '0.00' : num.toFixed(2)
},
/**
* 获取大额匹配的分配规则描述
* @returns {string} 规则描述
*/
getLargeMatchingRule() {
const amount = parseFloat(this.customAmount) || 0
if (amount <= 0) {
return '请输入金额'
} else if (amount < 5000) {
return '金额不能少于5000元'
} else if (amount > 50000) {
return '金额不能超过50000元'
} else if (amount <= 15000) {
return '分成3笔随机金额'
} else {
return '随机分拆每笔1000-8000元'
}
},
/**
* 获取大额匹配的预计笔数
* @returns {string} 预计笔数描述
*/
getLargeMatchingCount() {
const amount = parseFloat(this.customAmount) || 0
if (amount <= 0 || amount < 5000 || amount > 50000) {
return '0'
} else if (amount <= 15000) {
return '3'
} else {
// 15000以上随机分拆估算笔数范围
const minCount = Math.ceil(amount / 8000) // 按最大单笔8000计算最少笔数
const maxCount = Math.floor(amount / 1000) // 按最小单笔1000计算最多笔数
return `${minCount}-${Math.min(maxCount, 10)}` // 限制最大显示笔数为10
}
},
/**
* 获取图片URL
* @param {string} imagePath - 图片路径
* @returns {string} 完整的图片URL
*/
getImageUrl(imagePath) {
return getImageUrl(imagePath)
}
},
computed: {
/**
* 验证自定义金额是否有效
* @returns {boolean} 金额是否有效
*/
isValidCustomAmount() {
const amount = parseFloat(this.customAmount)
return !isNaN(amount) && amount >= 5000 && amount <= 50000
}
}
}
</script>
<style scoped>
/* 基础样式 */
:root {
--primary-color: #4361ee;
--secondary-color: #3f37c9;
--accent-color: #4895ef;
--light-color: #f8f9fa;
--dark-color: #212529;
--success-color: #4cc9f0;
--warning-color: #f8961e;
--danger-color: #f72585;
--border-radius: 12px;
--box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
--transition: all 0.3s ease;
}
/* 容器布局 */
.container {
display: flex;
flex-direction: column;
min-height: 100vh;
width: 100%;
margin: 0;
padding: 0;
background: linear-gradient(to bottom, #72c9ffae, #f3f3f3);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
position: relative;
}
.main-content {
flex: 1;
display: flex;
flex-direction: column;
padding: 16px;
gap: 16px;
max-width: 375px;
margin: 0 auto;
width: 100%;
}
.spacer {
height: 40px;
}
/* 通用部分样式 */
.section-title {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding: 0;
}
.section-title h3 {
font-size: 16px;
color: #333;
margin: 0;
width: auto;
height: 28px;
display: flex;
align-items: center;
justify-content: flex-start;
padding-left: 0;
}
.view-all {
font-size: 13px;
color: #999;
cursor: pointer;
}
/* 资金匹配区域 */
.matching-section {
background-color: white;
border-radius: var(--border-radius);
padding: 16px;
box-shadow: var(--box-shadow);
}
.matching-info, .orders-list {
padding-left: 0;
}
/* 切换开关样式 */
.toggle-switch {
display: flex;
align-items: center;
gap: 8px;
}
.toggle-label {
font-size: 12px;
color: #666;
width: 77px;
height: 15px;
display: flex;
align-items: center;
}
.switch {
position: relative;
display: inline-block;
width: 34px;
height: 16px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 34px;
}
.slider:before {
position: absolute;
content: "";
height: 12px;
width: 12px;
left: 2px;
bottom: 2px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #a0c4ff;
}
input:focus + .slider {
box-shadow: 0 0 1px #4CAF50;
}
input:checked + .slider:before {
transform: translateX(18px);
}
/* 匹配信息样式 */
.matching-info {
margin-top: 16px;
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.info-item .label {
color: #666;
font-size: 14px;
text-align: left;
width: 80px;
display: inline-block;
}
.info-item .value {
color: #333;
font-weight: 500;
font-size: 14px;
}
.custom-amount-input {
flex: 1;
margin-left: 10px;
}
/* 修复输入框字体大小(新增) */
.custom-amount-input .el-input__inner {
font-size: 14px !important;
height: 32px;
line-height: 32px;
}
.custom-amount-input .el-input-group__prepend {
font-size: 14px !important;
}
.custom-amount-input .el-input__inner::placeholder {
font-size: 14px !important;
}
.custom-amount-input .el-input__wrapper {
border-radius: 12px !important;
overflow: hidden;
}
/* 开始匹配按钮 */
.create-btn {
width: 128px;
height: 44px;
padding: 12px;
background: linear-gradient(to right, #4facfe 0%, #00f2fe 100%);
color: white;
border: none;
border-radius: 12px !important;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: var(--transition);
margin: 16px auto;
box-shadow: var(--box-shadow);
display: flex;
align-items: center;
justify-content: center;
}
.create-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
}
.create-btn:disabled {
background: #ccc;
cursor: not-allowed;
transform: none;
box-shadow: var(--box-shadow);
}
/* 提示信息 */
.tips {
margin-top: 16px;
padding: 12px;
background-color: #f8f9fa;
border-radius: var(--border-radius);
font-size: 12px;
color: #666;
}
.tips p {
margin: 6px 0;
line-height: 1.4;
}
.matching-section,
.orders-section,
.order-card,
.modal-content,
.tips {
border-radius: 12px !important;
overflow: hidden;
}
/* 订单列表样式 */
.orders-section {
background-color: white;
border-radius: var(--border-radius);
padding: 16px;
box-shadow: var(--box-shadow);
}
.orders-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.order-card {
padding: 12px;
border: 1px solid #eee;
border-radius: var(--border-radius);
cursor: pointer;
transition: var(--transition);
}
.order-card:hover {
transform: translateY(-2px);
box-shadow: var(--box-shadow);
}
.order-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
flex-wrap: wrap;
gap: 8px;
}
.order-id {
font-weight: 500;
color: var(--primary-color);
}
.system-reverse-tag {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 2px 6px;
border-radius: 10px;
font-size: 10px;
font-weight: 500;
}
.status {
padding: 2px 6px;
border-radius: 10px;
font-size: 10px;
font-weight: 500;
color: white;
}
.status.pending {
background-color: var(--warning-color);
}
.status.matching {
background-color: var(--accent-color);
}
.status.completed {
background-color: var(--success-color);
}
.status.cancelled, .status.failed {
background-color: var(--danger-color);
}
.order-info p {
margin: 4px 0;
font-size: 12px;
color: #666;
}
.system-note {
color: var(--accent-color);
font-style: italic;
font-size: 11px;
}
.empty-state {
text-align: center;
padding: 20px;
color: #999;
font-size: 14px;
}
/* 弹窗样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: white;
border-radius: var(--border-radius);
max-width: 90%;
max-height: 80vh;
overflow-y: auto;
width: 375px;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid #eee;
}
.modal-header h3 {
margin: 0;
font-size: 16px;
color: #333;
}
.close-btn {
background: none;
border: none;
font-size: 20px;
cursor: pointer;
color: #999;
}
.modal-body {
padding: 16px;
}
.order-summary p {
margin: 8px 0;
font-size: 14px;
color: #333;
}
.allocations-section, .records-section {
margin-top: 16px;
}
.allocations-section h4, .records-section h4 {
font-size: 15px;
color: #333;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #eee;
}
.timeline-item {
border-left: 2px solid var(--accent-color);
padding-left: 12px;
margin-bottom: 12px;
}
.timeline-content {
background: #f8f9fa;
padding: 10px;
border-radius: 6px;
}
.timeline-header {
display: flex;
justify-content: space-between;
margin-bottom: 6px;
}
.cycle {
font-size: 12px;
color: #666;
}
.amount {
font-weight: 500;
color: var(--success-color);
}
.record-item {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
}
.record-info {
display: flex;
gap: 10px;
font-size: 13px;
}
.action {
font-weight: 500;
color: var(--primary-color);
}
.user {
color: #666;
}
.time {
color: #999;
}
.record-amount {
font-weight: 500;
color: var(--success-color);
}
.record-note {
font-size: 12px;
color: #999;
margin-top: 4px;
}
/* 移动端适配 */
@media (max-width: 375px) {
.main-content {
padding: 12px;
}
.matching-section, .orders-section {
padding: 12px;
}
.modal-content {
width: 95%;
}
/* 确保输入框在移动端也保持14px */
.custom-amount-input .el-input__inner,
.custom-amount-input .el-input-group__prepend,
.custom-amount-input .el-input__inner::placeholder {
font-size: 14px !important;
}
}
</style>

View File

@@ -0,0 +1,880 @@
<template>
<div class="container">
<div class="main-content">
<!-- 导航栏 -->
<nav class="navbar transparent">
<div class="nav-left">
<el-button type="text" @click="$router.back()" class="back-btn">
<el-icon><ArrowLeft /></el-icon>
</el-button>
</div>
<div class="nav-center">
<h1 class="nav-title">积分记录</h1>
</div>
<div class="nav-right">
<el-button type="text" @click="$router.push('/myshop')" class="shop-btn">
<el-icon><ShoppingBag /></el-icon>
商城
</el-button>
</div>
</nav>
<!-- 积分概览 -->
<div class="points-overview">
<div class="overview-card transparent-card">
<div class="current-points">
<div class="points-icon">
<el-icon size="24"><Coin /></el-icon>
</div>
<div class="points-info">
<div class="points-value">{{ userPoints }}</div>
<div class="points-label">当前积分</div>
</div>
<img src="/imgs/point.png" alt="我的积分图标" class="balance-image">
</div>
<div class="points-stats">
<div class="stat-item">
<div class="stat-value">{{ totalEarned }}</div>
<div class="stat-label">累计获得</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ totalSpent }}</div>
<div class="stat-label">累计消费</div>
</div>
</div>
</div>
</div>
<!-- 筛选器 -->
<div class="filter-section">
<div class="filter-tabs">
<div
v-for="tab in filterTabs"
:key="tab.value"
:class="['tab-item', { active: selectedFilter === tab.value }]"
@click="selectFilter(tab.value)"
>
{{ tab.label }}
</div>
</div>
</div>
<!-- 积分记录列表 -->
<div class="history-content">
<div v-loading="loading" class="history-list">
<div v-if="filteredHistory.length === 0" class="empty-state">
<el-icon size="60"><DocumentRemove /></el-icon>
<p>{{ getEmptyText() }}</p>
<el-button type="primary" @click="$router.push('/shop')">
去赚积分
</el-button>
</div>
<div v-else>
<div v-for="record in filteredHistory" :key="record.id" class="history-item">
<div class="item-icon">
<el-icon :size="20" :class="getIconClass(record.type)">
<component :is="getIcon(record.type)" />
</el-icon>
</div>
<div class="item-content">
<div class="item-header">
<h4 class="item-title">{{ record.title }}</h4>
<div :class="['item-points', getPointsClass(record.type)]">
{{ getPointsText(record.type, record.points) }}
</div>
</div>
<div class="item-details">
<p class="item-description">{{ record.description }}</p>
<div class="item-meta">
<span class="item-date">{{ formatDateTime(record.createdAt) }}</span>
<span v-if="record.orderId" class="item-order">
订单号{{ record.orderId }}
</span>
</div>
</div>
</div>
<div class="item-action">
<el-button
v-if="record.orderId"
type="text"
size="small"
@click="viewOrder(record.orderId)"
>
查看订单
</el-button>
</div>
</div>
</div>
</div>
<!-- 加载更多 -->
<div v-if="hasMore" class="load-more">
<el-button @click="loadMore" :loading="loadingMore">
加载更多
</el-button>
</div>
</div>
<!-- 积分规则说明 -->
<div class="points-rules">
<el-collapse v-model="activeRules">
<el-collapse-item title="积分获取规则" name="earn">
<div class="rules-content">
<div class="rule-item">
<el-icon><UserFilled /></el-icon>
<span>注册账户+100积分</span>
</div>
<div class="rule-item">
<el-icon><Calendar /></el-icon>
<span>每日签到+10积分</span>
</div>
<div class="rule-item">
<el-icon><Share /></el-icon>
<span>分享商品+5积分</span>
</div>
<div class="rule-item">
<el-icon><Star /></el-icon>
<span>商品评价+20积分</span>
</div>
<div class="rule-item">
<el-icon><Trophy /></el-icon>
<span>完成任务+50积分</span>
</div>
</div>
</el-collapse-item>
<el-collapse-item title="积分使用规则" name="spend">
<div class="rules-content">
<div class="rule-item">
<el-icon><ShoppingBag /></el-icon>
<span>商品兑换按商品标价扣除</span>
</div>
<div class="rule-item">
<el-icon><Clock /></el-icon>
<span>积分有效期永久有效</span>
</div>
<div class="rule-item">
<el-icon><Warning /></el-icon>
<span>积分不可转让不可提现</span>
</div>
</div>
</el-collapse-item>
</el-collapse>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { ElMessage } from 'element-plus'
import {
ArrowLeft,
ShoppingBag,
Coin,
DocumentRemove,
Plus,
Minus,
ShoppingCart,
Star,
Calendar,
Share,
Trophy,
UserFilled,
Clock,
Warning
} from '@element-plus/icons-vue'
import api from '@/utils/api'
const router = useRouter()
const userStore = useUserStore()
// 响应式数据
const loading = ref(false)
const loadingMore = ref(false)
const selectedFilter = ref('all')
const history = ref([])
const page = ref(1)
const hasMore = ref(true)
const userPoints = ref(0)
const totalEarned = ref(0)
const totalSpent = ref(0)
const activeRules = ref([])
// 筛选标签
const filterTabs = ref([
{ label: '全部', value: 'all' },
{ label: '获得', value: 'earn' },
{ label: '消费', value: 'spend' },
{ label: '任务', value: 'task' },
{ label: '兑换', value: 'exchange' }
])
// 计算属性
const filteredHistory = computed(() => {
let filtered = history.value || []
if (selectedFilter.value !== 'all') {
filtered = filtered.filter(record => record.type === selectedFilter.value)
}
return filtered
})
// 方法
const selectFilter = (filter) => {
selectedFilter.value = filter
getHistory()
}
const getEmptyText = () => {
const textMap = {
all: '暂无积分记录',
earn: '暂无获得记录',
spend: '暂无消费记录',
task: '暂无任务记录',
exchange: '暂无兑换记录'
}
return textMap[selectedFilter.value]
}
const getIcon = (type) => {
const iconMap = {
earn: Plus,
spend: Minus,
task: Trophy,
exchange: ShoppingCart,
review: Star,
share: Share
}
return iconMap[type] || Plus
}
const getIconClass = (type) => {
const classMap = {
earn: 'icon-earn',
spend: 'icon-spend',
task: 'icon-task',
exchange: 'icon-exchange',
review: 'icon-review',
share: 'icon-share'
}
return classMap[type] || 'icon-default'
}
const getPointsClass = (type) => {
return type === 'spend' || type === 'exchange' ? 'points-negative' : 'points-positive'
}
const getPointsText = (type, points) => {
const isNegative = type === 'spend' || type === 'exchange'
return isNegative ? `-${points}` : `+${points}`
}
const formatDateTime = (date) => {
return new Date(date).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
}
const viewOrder = (orderId) => {
router.push(`/orders?orderId=${orderId}`)
}
const getUserPoints = async () => {
try {
const response = await api.get('/user/points')
userPoints.value = response.data.currentPoints
totalEarned.value = response.data.totalEarned
totalSpent.value = response.data.totalSpent
} catch (error) {
ElMessage.error('获取积分信息失败')
}
}
const getHistory = async (isLoadMore = false) => {
try {
if (!isLoadMore) {
loading.value = true
page.value = 1
} else {
loadingMore.value = true
}
const params = {
page: page.value,
limit: 20,
type: selectedFilter.value !== 'all' ? selectedFilter.value : undefined
}
const response = await api.get('/user/points/history', { params })
const historyData = response.data.history || []
if (isLoadMore) {
history.value.push(...historyData)
} else {
history.value = historyData
}
hasMore.value = response.data.hasMore || false
page.value++
} catch (error) {
console.error('获取积分记录失败:', error)
ElMessage.error('获取积分记录失败')
if (!isLoadMore) {
history.value = []
}
} finally {
loading.value = false
loadingMore.value = false
}
}
const loadMore = () => {
getHistory(true)
}
onMounted(() => {
getUserPoints()
getHistory()
})
</script>
<style scoped>
/* 基础样式变量 */
/* 基础样式变量 */
:root {
--primary-color: #4361ee;
--secondary-color: #3f37c9;
--accent-color: #4895ef;
--light-color: #f8f9fa;
--dark-color: #212529;
--success-color: #4cc9f0;
--warning-color: #f8961e;
--danger-color: #f72585;
--border-radius: 12px;
--box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
--transition: all 0.3s ease;
}
/* 容器布局 */
.container {
display: flex;
flex-direction: column;
min-height: 100vh;
width: 100%;
margin: 0;
padding: 0;
background: linear-gradient(to bottom, #72c9ffae, #f3f3f3);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.main-content {
border-radius: 12px;
flex: 1;
display: flex;
flex-direction: column;
padding: 16px;
gap: 16px;
max-width: 375px;
margin: 0 auto;
width: 100%;
}
/* 导航栏样式 */
.navbar {
border-radius: 12px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 0px;
height: 56px;
transition: var(--transition);
width: 100%;
}
.nav-left {
border-radius: 12px;
margin-right: auto;
padding-left: 0px;
}
.nav-right {
border-radius: 12px;
margin-left: auto;
padding-right: 0px;
}
.nav-center {
border-radius: 12px;
flex: 1;
text-align: center;
}
.navbar.transparent {
background: transparent;
box-shadow: none;
}
.back-btn,
.shop-btn,
.nav-title {
color: white;
font-size: 14px;
}
.nav-title {
margin: 0;
font-size: 18px;
font-weight: 500;
}
/* 积分概览区域 */
.points-overview {
border-radius: 12px;
width: 100%;
}
.overview-card {
border-radius: 12px;
padding: 24px;
color: white;
width: 100%;
overflow: hidden;
}
.transparent-card {
background: rgba(255, 255, 255, 0.2);
box-shadow: none;
}
.current-points {
border-radius: 12px;
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 24px;
}
.points-icon {
border-radius: 50%;
width: 48px;
height: 48px;
background: rgba(255, 255, 255, 0.3);
display: flex;
align-items: center;
justify-content: center;
}
.points-value {
font-size: 32px;
font-weight: 700;
line-height: 1;
color: #333;
}
.points-label {
font-size: 14px;
opacity: 0.8;
margin-top: 4px;
color: #666;
}
.points-stats {
border-radius: 12px;
display: flex;
gap: 32px;
}
.stat-item {
border-radius: 12px;
text-align: center;
}
.stat-value {
font-size: 20px;
font-weight: 600;
line-height: 1;
color: #333;
}
.stat-label {
font-size: 12px;
opacity: 0.8;
margin-top: 4px;
color: #666;
}
/* 筛选器区域 - 按钮均分 */
.filter-section {
border-radius: 12px;
background: white;
box-shadow: var(--box-shadow);
padding: 16px;
width: 100%;
overflow: hidden;
}
.filter-tabs {
border-radius: 12px;
display: flex;
justify-content: space-between;
gap: 6px;
width: 100%;
}
.tab-item {
border-radius: 20px;
flex: 1;
padding: 6px 12px;
background: #f5f5f5;
color: #666;
font-size: 12px;
cursor: pointer;
transition: var(--transition);
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tab-item.active {
background: rgb(125, 216, 255);
color: white;
}
/* 积分记录列表 */
.history-content {
border-radius: 12px;
width: 100%;
display: flex;
flex-direction: column;
gap: 16px;
}
.empty-state {
border-radius: 12px;
text-align: center;
padding: 60px 20px;
color: #999;
background: white;
box-shadow: var(--box-shadow);
overflow: hidden;
}
.history-list {
border-radius: 12px;
display: flex;
flex-direction: column;
gap: 12px;
}
.history-item {
border-radius: 12px;
display: flex;
align-items: flex-start;
gap: 12px;
background: white;
padding: 16px;
box-shadow: var(--box-shadow);
transition: var(--transition);
overflow: hidden;
}
.history-item:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
}
.item-icon {
border-radius: 50%;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.icon-earn {
background: #e8f5e8;
color: #52c41a;
}
.icon-spend {
background: #fff2e8;
color: #fa8c16;
}
.icon-task {
background: #f6ffed;
color: #52c41a;
}
.icon-exchange {
background: #fff0f6;
color: #eb2f96;
}
.icon-review {
background: #fff7e6;
color: #fa8c16;
}
.icon-share {
background: #e6fffb;
color: #13c2c2;
}
.icon-default {
background: #f5f5f5;
color: #999;
}
.item-content {
border-radius: 12px;
flex: 1;
}
.item-header {
border-radius: 12px;
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 8px;
}
.item-title {
margin: 0;
font-size: 16px;
color: #333;
font-weight: 500;
line-height: 1.4;
}
.item-points {
font-size: 16px;
font-weight: 600;
white-space: nowrap;
}
.points-positive {
color: #52c41a;
}
.points-negative {
color: #ff4d4f;
}
.item-details {
border-radius: 12px;
display: flex;
flex-direction: column;
gap: 8px;
}
.item-description {
margin: 0;
font-size: 14px;
color: #666;
line-height: 1.4;
}
.item-meta {
border-radius: 12px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: #999;
}
.item-date {
flex: 1;
}
.item-order {
color: var(--primary-color);
}
.item-action {
border-radius: 12px;
display: flex;
align-items: center;
}
/* 加载更多按钮 */
.load-more {
border-radius: 12px;
text-align: center;
padding: 8px;
}
.load-more .el-button {
border-radius: 12px;
width: 128px;
height: 44px;
padding: 12px;
background: linear-gradient(to right, #4facfe 0%, #00f2fe 100%);
color: white;
border: none;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: var(--transition);
box-shadow: var(--box-shadow);
}
.load-more .el-button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
background: linear-gradient(to right, #4facfe 0%, #00f2fe 100%);
}
/* 积分规则说明 */
.points-rules {
border-radius: 12px;
background: white;
box-shadow: var(--box-shadow);
overflow: hidden;
width: 100%;
margin-bottom: 20px;
}
.rules-content {
border-radius: 12px;
padding: 16px;
}
.rule-item {
border-radius: 12px;
display: flex;
align-items: center;
gap: 8px;
padding: 8px 0;
font-size: 14px;
color: #666;
}
.rule-item .el-icon {
color: var(--primary-color);
}
/* Element Plus 组件样式修复 */
.el-collapse {
border-radius: 12px !important;
border: none !important;
overflow: hidden !important;
}
.el-collapse-item {
border-radius: 12px !important;
overflow: hidden !important;
}
.el-collapse-item__header {
border-radius: 12px !important;
padding: 16px !important;
border-bottom: 1px solid #f0f0f0 !important;
font-size: 16px !important;
color: #333 !important;
}
.el-collapse-item__content {
border-radius: 12px !important;
padding: 0 !important;
}
.el-button {
border-radius: 12px !important;
}
.el-button--primary {
background: linear-gradient(to right, #4facfe 0%, #00f2fe 100%) !important;
border: none !important;
border-radius: 12px !important;
transition: var(--transition) !important;
}
.el-button--primary:hover {
transform: translateY(-2px) !important;
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15) !important;
}
.el-loading-mask {
border-radius: 12px !important;
}
/* 响应式设计 */
@media (max-width: 375px) {
.main-content {
padding: 12px;
}
.points-stats {
gap: 16px;
}
.item-header {
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.item-meta {
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.current-points {
flex-direction: column;
text-align: center;
gap: 8px;
}
.filter-tabs {
gap: 4px;
}
.tab-item {
padding: 5px 10px;
font-size: 11px;
}
}
/* 图片样式 */
.balance-image {
position: absolute;
right: 20px;
top: 50%;
transform: translateY(-50%);
height: 120px;
max-width: 100px;
object-fit: contain;
}
/* 调整当前积分容器为相对定位 */
.current-points {
position: relative;
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 24px;
padding-right: 20px; /* 为图片留出空间 */
}
/* 调整积分概览卡片为相对定位 */
.overview-card {
position: relative;
border-radius: 12px;
padding: 24px;
color: white;
width: 100%;
overflow: hidden;
}
</style>

614
src/views/MyProfile.vue Normal file
View File

@@ -0,0 +1,614 @@
<template>
<div class="personal-center">
<!-- 用户信息区域 -->
<div class="user-info-section">
<div class="user-avatar">
<el-avatar
:size="76"
:src="avatarUrl"
class="avatar-img"
>
<el-icon><User /></el-icon>
</el-avatar>
<el-button
type="primary"
size="small"
@click="showAvatarUpload = true"
class="upload-btn"
>
更换头像
</el-button>
</div>
<div class="user-actions">
<template v-if="userStore.isAuthenticated">
<span class="username">{{ userStore.user?.username || '用户' }}</span>
<button class="logout-btn" @click="handleLogout">退出登录</button>
</template>
<template v-else>
<div class="auth-buttons">
<router-link to="/mylogin">
<button class="login-btn">立即登录</button>
</router-link>
<router-link to="/register">
<button class="register-btn">注册</button>
</router-link>
</div>
</template>
</div>
</div>
<!-- 功能入口区域 -->
<div class="function-section">
<div class="function-grid">
<router-link
v-for="(item, index) in functionItems"
:key="index"
:to="item.path"
class="function-item"
custom
v-slot="{ navigate }"
>
<div @click="navigate" class="function-item-content">
<div class="function-icon">
<img :src="item.image" :alt="item.text" class="function-image">
</div>
<div class="function-text">{{ item.text }}</div>
</div>
</router-link>
</div>
</div>
<!-- 余额和记录区域 -->
<div class="balance-section">
<div class="balance-card">
<div class="balance-item">
<span class="balance-label">我的余额</span>
<span class="balance-value">¥{{ accountInfo.balance }}</span>
</div>
<div class="divider"></div>
<div class="balance-item">
<router-link to="/mymatching">
<span class="balance-label">匹配记录</span>
<span class="balance-arrow">></span>
</router-link>
</div>
</div>
</div>
<!-- 我的订单 -->
<div class="order-section">
<div class="section-header">
<h3>我的订单</h3>
<router-link to="/orders">
<span class="view-all">查看全部 ></span>
</router-link>
</div>
</div>
<!-- 设置选项区域 -->
<div class="settings-section">
<div class="setting-item" v-for="(item, index) in settings" :key="index">
<router-link :to=item.path>
<span class="setting-text">{{ item.text }}</span>
<span class="setting-arrow">></span>
</router-link>
</div>
</div>
<!-- 头像上传对话框 -->
<el-dialog
v-model="showAvatarUpload"
title="更换头像"
width="400px"
>
<el-upload
class="avatar-uploader"
action="#"
:show-file-list="false"
:before-upload="beforeAvatarUpload"
:http-request="uploadAvatar"
>
<img v-if="newAvatar" :src="newAvatar" class="avatar-preview" />
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
</el-upload>
<template #footer>
<span class="dialog-footer">
<el-button @click="showAvatarUpload = false">取消</el-button>
<el-button type="primary" @click="confirmAvatarUpload" :disabled="!newAvatar">
确定
</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script>
import { ref, onMounted } from 'vue';
import { useUserStore } from '@/stores/user';
import { useRouter } from 'vue-router';
import { ElMessage, ElMessageBox } from 'element-plus';
import { User, Plus } from '@element-plus/icons-vue';
import api from '@/utils/api';
export default {
setup() {
const userStore = useUserStore();
const router = useRouter();
const avatarUrl = ref('');
const newAvatar = ref('');
const showAvatarUpload = ref(false);
const accountInfo = ref({ balance: '0.00' });
const isLoading = ref(false);
const settings = ref([
{text:'账号安全',path:'/editpasswordpage'},
{text:'商户资料',path:'/editdetailspage'},
{text:'通知设置'},
{text:'积分获取规则'},
{text:'隐私协议'},
]);
const functionItems = ref([
{ image: "/imgs/mainpage/交易记录.png", text: "购物车", path: "" },
{ image: "/imgs/mainpage/订单查询.png", text: "地址", path: "" },
{ image: "/imgs/mainpage/客服中心.png", text: "收藏", path: "" }
]);
// 加载账户信息
const loadAccountInfo = async () => {
try {
if (userStore.user?.id) {
const response = await api.get(`/user/profile`);
if (response.data.success) {
accountInfo.value = response.data.data || { balance: '0.00' };
// 确保加载头像
if (response.data.data?.avatar) {
avatarUrl.value = response.data.data.avatar;
// 更新store中的头像
if(userStore.user) {
userStore.setUserAvatar(response.data.data.avatar);
}
}
}
}
} catch (error) {
console.error('加载账户信息失败:', error);
}
};
// 头像上传前的验证
const beforeAvatarUpload = (file) => {
const isJPG = file.type === 'image/jpeg' || file.type === 'image/png';
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isJPG) {
ElMessage.error('头像只能是 JPG/PNG 格式!');
return false;
}
if (!isLt2M) {
ElMessage.error('头像大小不能超过 2MB!');
return false;
}
return true;
};
// 上传头像
const uploadAvatar = async (options) => {
try {
const formData = new FormData();
formData.append('file', options.file);
const response = await api.post('/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
'Authorization': `Bearer ${userStore.token}`
}
});
if (response.data.success) {
newAvatar.value = response.data.url;
ElMessage.success('头像上传成功');
} else {
ElMessage.error(response.data.message || '上传失败');
}
} catch (error) {
console.error('头像上传失败:', error);
ElMessage.error('头像上传失败');
}
}
// 确认更新头像
const confirmAvatarUpload = async () => {
try {
if (!newAvatar.value) {
ElMessage.error('请先选择头像');
return;
}
const response = await api.put('/user/profile', {
avatar: newAvatar.value
});
if (response.data.success) {
avatarUrl.value = newAvatar.value;
// 更新用户store中的头像信息 - 修改这里
if(userStore.user) {
userStore.setUser({
...userStore.user,
avatar: newAvatar.value
});
}
showAvatarUpload.value = false;
newAvatar.value = '';
ElMessage.success('头像更新成功');
} else {
ElMessage.error(response.data.message || '头像更新失败');
}
} catch (error) {
console.error('头像更新失败:', error);
if (error.response) {
ElMessage.error(error.response.data.message || '头像更新失败');
} else {
ElMessage.error('头像更新失败');
}
}
}
const handleLogout = async () => {
try {
await ElMessageBox.confirm('确定要退出登录吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
});
userStore.logout();
router.push('/mylogin');
ElMessage.success('已退出登录');
} catch {
// 用户取消
}
};
onMounted(() => {
// 初始化时从store中获取头像
if (userStore.user?.avatar) {
avatarUrl.value = userStore.user.avatar;
}
loadAccountInfo();
});
return {
avatarUrl,
newAvatar,
showAvatarUpload,
accountInfo,
isLoading,
functionItems,
settings,
beforeAvatarUpload,
uploadAvatar,
confirmAvatarUpload,
handleLogout,
userStore
};
}
};
</script>
<style scoped>
.personal-center {
max-width: 600px;
margin: 0 auto;
padding: 20px;
font-family: 'PingFang SC', 'Helvetica Neue', Arial, sans-serif;
background-color: #f8f8f8;
min-height: 100vh;
background: linear-gradient(to bottom, #72c9ffae, #f3f3f3);
}
/* 用户信息区域 - 修改部分 */
.user-info-section {
display: flex;
align-items: center;
padding: 20px;
background-color: transparent;
border-radius: 12px;
color: #333;
margin-bottom: 20px;
position: relative;
}
.user-avatar {
position: relative;
width: 76px;
height: 76px;
margin-right: 15px;
flex-shrink: 0;
}
.avatar-img {
width: 76px;
height: 76px;
border-radius: 50%;
object-fit: cover;
border: 3px solid rgba(0, 0, 0, 0.1);
}
.upload-btn {
position: absolute;
bottom: -10px;
left: 50%;
transform: translateX(-50%);
font-size: 12px;
padding: 4px 8px;
border-radius: 12px;
}
.user-actions {
display: flex;
align-items: center;
flex-grow: 1;
min-width: 0;
}
.username {
font-size: 16px;
font-weight: 500;
color: #333;
margin-right: auto;
padding-left: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 60%;
}
.auth-buttons {
display: flex;
gap: 10px;
margin-left: auto;
}
.login-btn, .register-btn {
padding: 8px 16px;
border-radius: 20px;
font-weight: 500;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
background-color: transparent;
border: 1px solid #6a11cb;
color: #6a11cb;
}
.register-btn {
border: 1px solid #6a11cb;
color: #6a11cb;
}
.login-btn:hover, .register-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.logout-btn {
padding: 8px 16px;
border-radius: 20px;
font-weight: 500;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
background-color: transparent;
border: 1px solid #ff4d4f;
color: #ff4d4f;
flex-shrink: 0;
margin-left: 10px;
}
.logout-btn:hover {
background-color: #fff2f0;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
/* 功能入口区域 */
.function-section {
border-radius: 12px;
padding: 15px 0;
margin-bottom: 20px;
}
.function-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
padding: 0 15px;
}
.function-item-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 10px 0;
cursor: pointer;
transition: all 0.2s ease;
background-color: white;
border-radius: 12px;
height: 80px;
}
.function-item-content:hover {
transform: scale(1.05);
}
.function-icon {
width: 40px;
height: 40px;
margin-bottom: 8px;
display: flex;
align-items: center;
justify-content: center;
}
.function-image {
width: 30px;
height: 30px;
object-fit: contain;
}
.function-text {
font-size: 13px;
color: #333;
}
/* 余额和记录区域 */
.balance-section {
background-color: #2f89ff;
border-radius: 12px;
padding: 15px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
color: white;
position: relative;
height: 85px;
}
.balance-card {
display: flex;
justify-content: space-between;
align-items: center;
height: 100%;
}
.balance-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 10px;
}
.divider {
width: 1px;
height: 40px;
background-color: rgba(255, 255, 255, 0.3);
}
.balance-label {
font-size: 14px;
color: rgba(255, 255, 255, 0.8);
margin-bottom: 5px;
}
.balance-value {
font-size: 18px;
font-weight: bold;
color: white;
}
.balance-arrow {
font-size: 16px;
color: rgba(255, 255, 255, 0.8);
}
/* 我的订单 */
.order-section {
background-color: white;
border-radius: 12px;
padding: 15px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.section-header h3 {
font-size: 16px;
color: #333;
margin: 0;
}
.view-all {
font-size: 13px;
color: #999;
cursor: pointer;
}
/* 设置区域 */
.settings-section {
background-color: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.setting-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
transition: background-color 0.2s ease;
}
.setting-item:last-child {
border-bottom: none;
}
.setting-item:hover {
background-color: #f9f9f9;
}
.setting-text {
font-size: 15px;
color: #333;
}
.setting-arrow {
color: #ccc;
font-size: 14px;
}
/* 头像上传对话框样式 */
.avatar-uploader {
text-align: center;
}
.avatar-uploader :deep(.el-upload) {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: 0.2s;
width: 178px;
height: 178px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
}
.avatar-uploader :deep(.el-upload:hover) {
border-color: #409eff;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
}
.avatar-preview {
width: 178px;
height: 178px;
object-fit: cover;
}
</style>

257
src/views/MyShop.vue Normal file
View File

@@ -0,0 +1,257 @@
<template>
<div class="container">
<div class="spacer"></div>
<div class="main-content">
<!-- 积分余额区域 -->
<div class="balance-section">
<div class="balance-label">积分余额</div>
<div class="balance-value">{{ userPoints }}</div>
<router-link to="/mypoints-history" class="detail-btn">
<div>点击查看积分明细</div>
</router-link>
<img src="/imgs/shop.png" alt="积分商城图标" class="balance-image">
</div>
<!-- 公告区域 -->
<div class="announcement-section">
这里是公告内容
</div>
<!-- 空白间隔 -->
<div class="empty-spacer"></div>
<!-- 分类区域 -->
<div class="category-section">
<div class="category-header-container">
<div class="category-header">分类区</div>
</div>
<div class="category-items">
<div class="category-grid">1</div>
<div class="category-grid">2</div>
<div class="category-grid">3</div>
<div class="category-grid">4</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { ref } from 'vue'
import { transferAPI } from '../utils/api'
import { ElMessage } from 'element-plus'
export default {
setup() {
const userPoints = ref(0)
// 获取用户积分
const getUserPoints = async () => {
try {
const response = await transferAPI.getUserAccount()
userPoints.value = response.data.balance ||
response.data.points ||
response.data.currentPoints ||
0
} catch (error) {
console.error('获取积分信息失败', error)
ElMessage.error('获取积分信息失败,请稍后重试')
}
}
getUserPoints()
return {
userPoints,
getUserPoints
}
}
}
</script>
<style scoped>
/* 使用与主页面一致的容器样式 */
.container {
display: flex;
flex-direction: column;
min-height: 100vh;
width: 100%;
margin: 0;
padding: 0;
background: linear-gradient(to bottom, #ffae00, #f3f3f3);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
position: relative;
}
.main-content {
flex: 1;
display: flex;
flex-direction: column;
padding: 16px;
gap: 16px;
max-width: 375px;
margin: 0 auto;
width: 100%;
}
.spacer {
height: 40px;
}
/* 积分余额区域样式 */
.balance-section {
width: 343px;
height: 159px;
background: rgb(245, 245, 185);
border-radius: 12px;
box-shadow: var(--box-shadow);
padding: 20px;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
margin: 0 auto;
position: relative; /* 为绝对定位提供参考 */
}
.balance-label {
font-size: 32px; /* 与积分数字相同大小 */
font-weight: bold; /* 与积分数字相同粗细 */
color: #666;
margin-bottom: 8px;
text-align: left;
}
.balance-value {
font-size: 32px;
font-weight: bold;
color: var(--primary-color);
margin-bottom: 20px;
text-align: left;
width: 100%;
}
.detail-btn {
width: 123px;
height: 35px;
background: #ffde73;
color: black;
border: none;
border-radius: 18px;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
text-decoration: none;
cursor: pointer;
transition: var(--transition);
margin-bottom: 10px; /* 增加底部间距 */
}
.detail-btn:hover {
background: #c19a6b;
transform: translateY(-2px);
}
.balance-image {
position: absolute; /* 绝对定位 */
right: 20px; /* 右侧距离 */
bottom: 20px; /* 底部距离,与按钮底部对齐 */
height: 150px; /* 增加图片高度 */
max-width: 120px; /* 增加最大宽度 */
object-fit: contain; /* 保持图片比例 */
}
/* 公告区域样式 */
.announcement-section {
width: 343px;
height: 46px;
background: #ffffff;
border-radius: 12px;
box-shadow: var(--box-shadow);
padding: 12px 16px;
margin: 0 auto;
display: flex;
align-items: center;
font-size: 14px;
color: #666;
}
/* 空白间隔 - 添加圆角 */
.empty-spacer {
width: 344px;
height: 71px;
margin: 0 auto;
background: white;
border-radius: 12px;
}
/* 分类区域样式 */
.category-section {
width: 343px;
height: 274px;
background: white;
border-radius: 12px;
box-shadow: var(--box-shadow);
padding: 16px;
margin: 0 auto;
display: flex;
}
.category-header-container {
width: 114px;
display: flex;
align-items: flex-start;
justify-content: center;
padding-right: 16px;
border-right: 1px solid #f0f0f0;
}
.category-header {
width: 100%;
height: 100%; /* 高度由父容器决定 */
font-size: 16px;
font-weight: 600;
color: var(--dark-color);
padding: 16px;
background-color: #cae5ff;
border-radius: 8px;
display: flex;
align-items: flex-start; /* 改为flex-start使内容置顶 */
justify-content: center; /* 保持水平居中 */
}
.category-items {
flex: 1;
display: grid;
grid-template-rows: repeat(4, 1fr);
gap: 16px;
margin-left: 16px;
}
.category-grid {
width: 100%;
height: 100%;
background-color: #fff0d0;
border-radius: 8px;
display: flex;
align-items: center;
padding: 0 16px;
}
/* 使用与主页面一致的变量 */
:root {
--primary-color: #4361ee;
--secondary-color: #3f37c9;
--accent-color: #4895ef;
--light-color: #f8f9fa;
--dark-color: #212529;
--success-color: #4cc9f0;
--warning-color: #f8961e;
--danger-color: #f72585;
--border-radius: 12px;
--box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
--transition: all 0.3s ease;
}
</style>