合并代码
This commit is contained in:
486
src/views/MyLogin.vue
Normal file
486
src/views/MyLogin.vue
Normal 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
894
src/views/MyMatching.vue
Normal 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>
|
||||
880
src/views/MyPointsHistory.vue
Normal file
880
src/views/MyPointsHistory.vue
Normal 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
614
src/views/MyProfile.vue
Normal 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
257
src/views/MyShop.vue
Normal 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>
|
||||
Reference in New Issue
Block a user