Files
AIGC/demo/frontend/src/views/Subscription.vue
AIGC Developer a13ff70055 feat: 实现邮箱验证码登录和腾讯云SES集成
- 实现邮箱验证码登录功能,支持自动注册新用户
- 修复验证码生成逻辑,确保前后端验证码一致
- 添加腾讯云SES webhook回调接口,支持6种邮件事件
- 配置ngrok内网穿透支持,允许外部访问
- 优化登录页面UI,采用全屏背景和居中布局
- 清理调试代码和未使用的导入
- 添加完整的配置文档和测试脚本
2025-10-23 17:50:12 +08:00

783 lines
21 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="subscription-page">
<!-- 左侧导航栏 -->
<aside class="sidebar">
<!-- Logo -->
<div class="logo">logo</div>
<!-- 导航菜单 -->
<nav class="nav-menu">
<div class="nav-item" @click="goToProfile">
<el-icon><User /></el-icon>
<span>个人主页</span>
</div>
<div class="nav-item" :class="{ active: currentSection === 'subscription' }" @click="setSection('subscription')">
<el-icon><Compass /></el-icon>
<span>会员订阅</span>
</div>
<div class="nav-item" @click="goToMyWorks">
<el-icon><Document /></el-icon>
<span>我的作品</span>
</div>
</nav>
<!-- 工具分隔线 -->
<div class="divider">
<span>工具</span>
</div>
<!-- 工具菜单 -->
<nav class="tools-menu">
<div class="nav-item" @click="goToTextToVideo">
<el-icon><VideoPlay /></el-icon>
<span>文生视频</span>
</div>
<div class="nav-item" @click="goToImageToVideo">
<el-icon><Picture /></el-icon>
<span>图生视频</span>
</div>
<div class="nav-item storyboard-item" @click="goToStoryboardVideo">
<el-icon><Film /></el-icon>
<span>分镜视频</span>
<el-tag size="small" type="primary" class="sora-tag">Sora2.0</el-tag>
</div>
</nav>
</aside>
<!-- 主内容区域 -->
<main class="main-content">
<template v-if="currentSection === 'works'">
<MyWorks />
</template>
<template v-else>
<!-- 顶部 两层合并为一个盒子 -->
<section class="top-merged-card">
<!-- 上层用户信息 + 右侧按钮 -->
<div class="row-top">
<div class="user-left">
<div class="avatar-wrap">
<div class="avatar-circle">
<div class="pause-line"></div>
<div class="pause-line second"></div>
</div>
</div>
<div class="user-meta">
<div class="username">mingzi_FBx7foZYDS7inLQb</div>
<div class="user-id">ID 2994509784706419</div>
</div>
</div>
<div class="user-right">
<div class="points-pill">
<div class="star-icon">
<el-icon><Star /></el-icon>
</div>
<span>50</span>
</div>
<button class="mini-btn" @click="goToOrderDetails">积分详情</button>
<button class="mini-btn" @click="goToWorks">我的订单</button>
</div>
</div>
<!-- 下层三项总结 -->
<div class="row-bottom">
<div class="summary-item">
<div class="summary-label">当前生效权益</div>
<div class="summary-value">免费版</div>
</div>
<div class="divider-v"></div>
<div class="summary-item">
<div class="summary-label">到期时间</div>
<div class="summary-value">永久</div>
</div>
<div class="divider-v"></div>
<div class="summary-item">
<div class="summary-label">剩余积分</div>
<div class="summary-value highlight">
<div class="star-icon">
<el-icon><Star /></el-icon>
</div>
<span class="points-number">50</span>
</div>
</div>
</div>
</section>
<!-- 套餐选择 -->
<section class="subscription-packages">
<h3 class="section-title">套餐</h3>
<div class="packages-grid">
<!-- 免费版 -->
<div class="package-card free-card" :class="{ selected: selectedPlan === 'free' }" @click="selectPlan('free')">
<div class="package-header">
<h4 class="package-title">免费版</h4>
</div>
<div class="package-price">$0/</div>
<button class="package-button current">当前套餐</button>
<div class="package-features">
<div class="feature-item">
<el-icon class="check-icon"><Check /></el-icon>
<span>新用户首次登陆免费获得50积分</span>
</div>
</div>
</div>
<!-- 标准版 -->
<div class="package-card standard-card" :class="{ selected: selectedPlan === 'standard' }" @click="selectPlan('standard')">
<div class="package-header">
<h4 class="package-title">标准版</h4>
<div class="discount-tag">首购低至8.5</div>
</div>
<div class="package-price">$59/</div>
<div class="points-box">每月200积分</div>
<button class="package-button subscribe">立即订阅</button>
<div class="package-features">
<div class="feature-item">
<el-icon class="check-icon"><Check /></el-icon>
<span>快速通道生成</span>
</div>
<div class="feature-item">
<el-icon class="check-icon"><Check /></el-icon>
<span>支持商用</span>
</div>
<div class="feature-item">
<el-icon class="check-icon"><Check /></el-icon>
<span>下载去水印</span>
</div>
</div>
</div>
<!-- 专业版 -->
<div class="package-card premium-card" :class="{ selected: selectedPlan === 'premium' }" @click="selectPlan('premium')">
<div class="package-header">
<h4 class="package-title">专业版</h4>
<div class="value-tag">超值之选</div>
</div>
<div class="package-price">$259/</div>
<div class="points-box">每月1000积分</div>
<button class="package-button premium">立即订阅</button>
<div class="package-features">
<div class="feature-item">
<el-icon class="check-icon"><Check /></el-icon>
<span>极速通道生成</span>
</div>
<div class="feature-item">
<el-icon class="check-icon"><Check /></el-icon>
<span>支持商用</span>
</div>
<div class="feature-item">
<el-icon class="check-icon"><Check /></el-icon>
<span>下载去水印</span>
</div>
<div class="feature-item">
<el-icon class="check-icon"><Check /></el-icon>
<span>新功能优先体验</span>
</div>
</div>
</div>
</div>
</section>
</template>
</main>
</div>
<!-- 订单详情模态框 -->
<el-dialog
v-model="orderDialogVisible"
title="订单详情"
width="80%"
class="order-dialog"
:modal="true"
:close-on-click-modal="true"
:close-on-press-escape="true"
@close="handleOrderDialogClose"
>
<div class="order-content">
<div class="order-summary">
<h3>账户订单总览</h3>
<div class="summary-stats">
<div class="stat-item">
<span class="stat-label">总订单数</span>
<span class="stat-value">{{ orders.length }}</span>
</div>
<div class="stat-item">
<span class="stat-label">总金额</span>
<span class="stat-value">¥{{ totalAmount }}</span>
</div>
</div>
</div>
<div class="orders-list">
<div class="order-item" v-for="order in orders" :key="order.id">
<div class="order-header">
<span class="order-id">订单号{{ order.id }}</span>
<span class="order-status" :class="order.status">{{ order.statusText }}</span>
</div>
<div class="order-details">
<div class="order-info">
<p><strong>创建时间</strong>{{ order.createdAt }}</p>
<p><strong>订单类型</strong>{{ order.type }}</p>
<p><strong>金额</strong>¥{{ order.amount }}</p>
</div>
<div class="order-actions">
<el-button type="primary" size="small" @click="viewOrderDetail(order)">查看详情</el-button>
</div>
</div>
</div>
</div>
</div>
</el-dialog>
</template>
<script setup>
import { ref, computed } from 'vue'
import MyWorks from '@/views/MyWorks.vue'
import { useRouter } from 'vue-router'
import {
User,
Document,
User as Plus,
Bell,
Check,
Compass,
VideoPlay,
Picture,
Film,
Star
} from '@element-plus/icons-vue'
const router = useRouter()
// 跳转到个人主页
const goToProfile = () => {
router.push('/profile')
}
const goToMyWorks = () => {
router.push('/works')
}
const goToTextToVideo = () => {
router.push('/text-to-video')
}
const goToImageToVideo = () => {
router.push('/image-to-video')
}
const goToStoryboardVideo = () => {
router.push('/storyboard-video')
}
// 订单模态框相关
const orderDialogVisible = ref(false)
const orders = ref([
{
id: 'ORD-2024-001',
status: 'completed',
statusText: '已完成',
createdAt: '2024-01-15 10:30:00',
type: '标准版订阅',
amount: 59.00
},
{
id: 'ORD-2024-002',
status: 'pending',
statusText: '待支付',
createdAt: '2024-01-20 14:20:00',
type: '专业版订阅',
amount: 259.00
},
{
id: 'ORD-2024-003',
status: 'completed',
statusText: '已完成',
createdAt: '2024-01-25 09:15:00',
type: '积分充值',
amount: 100.00
}
])
// 计算总金额
const totalAmount = computed(() => {
return orders.value.reduce((sum, order) => sum + order.amount, 0).toFixed(2)
})
// 显示订单详情模态框
const goToOrderDetails = () => {
orderDialogVisible.value = true
}
// 关闭订单模态框
const handleOrderDialogClose = () => {
orderDialogVisible.value = false
}
// 查看订单详情
const viewOrderDetail = (order) => {
// 这里可以添加查看订单详情的逻辑
}
// 跳转到我的作品页面
const goToWorks = () => {
router.push('/works')
}
// 选中套餐(紫色边框)
const selectedPlan = ref('free')
const selectPlan = (plan) => {
selectedPlan.value = plan
}
</script>
<style scoped>
.subscription-page {
min-height: 100vh;
background: #0a0a0a;
color: white;
display: flex !important;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0 !important;
padding: 0 !important;
width: 100vw !important;
height: 100vh !important;
overflow: hidden;
position: relative;
}
/* 左侧导航栏 */
.sidebar {
width: 280px !important; /* 放大侧边栏 */
background: #1a1a1a !important;
padding: 24px 0 !important;
border-right: 1px solid #1a1a1a !important; /* 弱化分割线,与背景融为一体 */
flex-shrink: 0 !important;
z-index: 100 !important;
display: block !important;
position: relative !important;
}
.logo {
padding: 0 24px 32px;
font-size: 20px; /* 放大标题 */
font-weight: 500;
color: white;
}
.nav-menu, .tools-menu {
padding: 0 24px; /* 左右内边距同步放大 */
}
.nav-item {
display: flex;
align-items: center;
padding: 14px 18px; /* 项高度放大 */
margin-bottom: 4px;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
}
.nav-item:hover {
background: #2a2a2a;
}
.nav-item.active {
background: #1e3a8a;
}
.nav-item .el-icon {
margin-right: 14px;
font-size: 20px; /* 放大图标 */
}
.nav-item span {
font-size: 15px; /* 放大文字 */
flex: 1;
}
.sora-tag {
margin-left: 8px;
font-size: 10px;
padding: 2px 6px;
}
/* 分镜视频特殊样式 */
.storyboard-item {
position: relative;
}
.storyboard-item .sora-tag {
background: linear-gradient(135deg, #667eea, #764ba2) !important;
border: none !important;
color: #fff !important;
font-weight: 700 !important;
font-size: 11px !important;
padding: 2px 8px !important;
border-radius: 12px !important;
box-shadow: 0 2px 6px rgba(102, 126, 234, 0.3) !important;
animation: pulse-glow 2s ease-in-out infinite alternate;
}
@keyframes pulse-glow {
0% {
box-shadow: 0 2px 6px rgba(102, 126, 234, 0.3);
}
100% {
box-shadow: 0 2px 12px rgba(102, 126, 234, 0.6);
}
}
.divider {
margin: 30px 20px 20px;
padding: 0 16px;
color: #666;
font-size: 12px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 1px;
}
/* 主内容区域 */
.main-content {
flex: 1;
padding: 0;
background: #0a0a0a;
overflow-y: auto;
min-width: 0;
}
/* 套餐选择 */
.subscription-packages {
padding: 0 30px 30px; /* 与顶部盒子保持一致的左右留白 */
margin: 0 auto; /* 居中显示,与上方盒子对齐 */
max-width: calc(100% - 80px); /* 限制最大宽度,与上方盒子一致 */
margin-left: 15px; /* 增加左边距,与上方盒子更精确对齐 */
}
.subscription-packages .section-title {
font-size: 24px;
font-weight: 600;
color: white;
margin: 0 0 30px 0;
text-align: left !important;
position: relative;
}
.subscription-packages .section-title::after {
content: '';
position: absolute;
bottom: -10px;
left: 0;
transform: none;
width: 60px;
height: 2px;
background: #4a9eff;
}
.subscription-packages .packages-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
max-width: 1440px;
margin: 0 30px 0 0; /* 右侧留白与左侧 padding(30px) 保持一致 */
width: 100%;
align-items: stretch; /* 卡片等高 */
}
.subscription-packages .package-card {
background: #1a1a1a;
border-radius: 12px;
padding: 28px;
border: 1px solid #333;
position: relative;
transition: all 0.3s ease;
min-height: 700px; /* 进一步拉长 */
display: flex;
flex-direction: column;
}
.subscription-packages .package-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}
.subscription-packages .package-card.selected {
border: 2px solid #8b5cf6;
box-shadow: 0 0 20px rgba(139, 92, 246, 0.3);
}
.subscription-packages .package-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 20px;
}
.subscription-packages .package-title {
font-size: 20px;
font-weight: 600;
color: white;
margin: 0;
}
.subscription-packages .discount-tag, .subscription-packages .value-tag {
font-size: 12px;
padding: 4px 8px;
border-radius: 4px;
font-weight: 500;
}
.subscription-packages .discount-tag { background:#666; color:#fff; }
.subscription-packages .value-tag { background:#8b5cf6; color:#fff; }
.subscription-packages .package-price {
font-size: 32px;
font-weight: 700;
color: white;
margin-bottom: 15px;
}
.subscription-packages .points-box {
background: #2a2a2a;
color: white;
padding: 10px 15px;
border-radius: 6px;
font-size: 14px;
margin-bottom: 20px;
text-align: center;
}
.subscription-packages .package-button {
width: 100%;
padding: 12px;
border-radius: 8px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
margin-bottom: 25px;
}
.subscription-packages .package-button.current { background:#666; color:#fff; border:none; }
.subscription-packages .package-button.subscribe { background:#4a9eff; color:#fff; border:none; }
.subscription-packages .package-button.subscribe:hover { background:#3a8bdf; }
.subscription-packages .package-button.premium { background:#8b5cf6; color:#fff; border:none; }
.subscription-packages .package-button.premium:hover { background:#7c3aed; }
.subscription-packages .package-features {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: auto; /* 将特性列表推至卡片下部,保持视觉均衡 */
}
.subscription-packages .feature-item {
display: flex;
align-items: center;
gap: 10px;
font-size: 14px;
color: #ccc;
}
.subscription-packages .check-icon { color:#4a9eff; font-size:16px; }
/* 响应式设计 */
@media (max-width: 1024px) {
.subscription-packages .packages-grid {
grid-template-columns: 1fr;
gap: 20px;
max-width: 720px;
}
.membership-overview {
flex-direction: column;
gap: 20px;
}
}
@media (max-width: 768px) {
.sidebar {
width: 240px;
}
.user-profile-section {
flex-direction: column;
text-align: center;
}
.profile-actions {
justify-content: center;
}
}
/* 顶部合并的两层盒子 */
.top-merged-card {
max-width: calc(100% - 80px); /* 限制最大宽度减去套餐区域向右移动的80px */
width: 100%;
margin: 20px auto; /* 上下20px左右自动居中 */
background: #1a1a1a; /* 与卡片、页面风格保持一致 */
border: 1px solid #333;
border-radius: 10px;
}
.top-merged-card .row-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px; /* 与个人主页保持一致 */
padding: 30px; /* 与个人主页保持一致 */
border-bottom: 1px solid #1f2937;
}
.top-merged-card .row-bottom {
display: grid;
grid-template-columns: 1fr auto 1fr auto 1fr;
gap: 30px;
padding: 30px; /* 与row-top保持一致 */
align-items: center;
}
.top-merged-card .divider-v { width:1px; height:48px; background:#1f2937; }
.top-merged-card .summary-item { display:flex; flex-direction:column; gap:8px; }
.top-merged-card .summary-label { font-size:15px; color:#9ca3af; }
.top-merged-card .summary-value { font-size:17px; color:#e5e7eb; font-weight:600; }
.top-merged-card .summary-value.highlight { color:#60a5fa; display:flex; align-items:center; gap:8px; }
.top-merged-card .plus-icon { font-size:22px; color:#60a5fa; }
/* 顶部行内的用户信息与按钮样式(沿用原样式类) */
.user-left { display:flex; align-items:center; gap:18px; }
.avatar-wrap { width: 56px; height: 56px; }
.avatar-circle { width:56px; height:56px; border-radius:50%; background:linear-gradient(180deg,#1e3a8a,#111827); border:2px solid #4a9eff; display:flex; align-items:center; justify-content:center; position:relative; }
.pause-line { width:5px; height:20px; background:#fff; border-radius:2px; }
.pause-line.second { position:absolute; right:18px; width:5px; height:20px; background:#fff; border-radius:2px; }
.user-meta { display:flex; flex-direction:column; }
.username { font-size:19px; font-weight:600; color:#e5e7eb; }
.user-id { font-size:15px; color:#9ca3af; }
.user-right { display:flex; align-items:center; gap:12px; }
.points-pill { display:flex; align-items:center; gap:6px; padding:9px 14px; border-radius:999px; background:#0b1220; border:1px solid #1f3758; color:#60a5fa; font-weight:600; font-size:16px; }
.star-icon {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
background: #3b82f6;
border-radius: 50%;
color: white;
font-size: 12px;
}
.mini-btn { background:#0f172a; color:#e5e7eb; border:1px solid #334155; padding:9px 14px; border-radius:6px; font-size:14px; cursor:pointer; transition:.2s ease; }
.mini-btn:hover { background:#111827; border-color:#3b82f6; }
@media (max-width: 1024px) {
.top-merged-card { margin: 0 16px 16px; }
.top-merged-card .row-top { flex-direction: column; align-items: flex-start; gap: 10px; }
.top-merged-card .row-bottom { grid-template-columns: 1fr; gap: 12px; }
}
/* 订单详情模态框样式 */
.order-dialog {
background: #1a1a1a;
color: white;
}
.order-content {
background: #1a1a1a;
color: white;
}
.order-summary {
margin-bottom: 30px;
padding: 20px;
background: #2a2a2a;
border-radius: 8px;
}
.order-summary h3 {
color: white;
margin: 0 0 15px 0;
font-size: 18px;
}
.summary-stats {
display: flex;
gap: 30px;
}
.stat-item {
display: flex;
flex-direction: column;
gap: 5px;
}
.stat-label {
color: #9ca3af;
font-size: 14px;
}
.stat-value {
color: #60a5fa;
font-size: 16px;
font-weight: 600;
}
.orders-list {
display: flex;
flex-direction: column;
gap: 15px;
}
.order-item {
background: #2a2a2a;
border-radius: 8px;
padding: 20px;
border: 1px solid #333;
}
.order-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.order-id {
color: white;
font-weight: 600;
}
.order-status {
padding: 4px 12px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.order-status.completed {
background: #10b981;
color: white;
}
.order-status.pending {
background: #f59e0b;
color: white;
}
.order-details {
display: flex;
justify-content: space-between;
align-items: center;
}
.order-info p {
margin: 5px 0;
color: #d1d5db;
font-size: 14px;
}
.order-info strong {
color: white;
}
</style>