Initial commit: AIGC项目完整代码

This commit is contained in:
AIGC Developer
2025-10-21 16:50:33 +08:00
commit 47c8e02ab0
137 changed files with 30676 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
<template>
<div id="app">
<h1>测试应用</h1>
<p>如果您能看到这个页面说明Vue应用正常工作</p>
<router-view />
</div>
</template>
<script setup>
console.log('App.vue 加载成功')
</script>
<style>
#app {
padding: 20px;
font-family: Arial, sans-serif;
}
</style>

438
demo/frontend/src/App.vue Normal file
View File

@@ -0,0 +1,438 @@
<template>
<div id="app" :data-route="route.name">
<!-- 导航栏 - 根据路由条件显示 -->
<NavBar v-if="shouldShowNavBar" />
<!-- 主要内容区域 -->
<main :class="{ 'with-navbar': shouldShowNavBar }">
<router-view />
</main>
<!-- 页脚 - 根据路由条件显示 -->
<Footer v-if="shouldShowFooter" />
</div>
</template>
<script setup>
import { computed, watch } from 'vue'
import { useRoute } from 'vue-router'
import NavBar from '@/components/NavBar.vue'
import Footer from '@/components/Footer.vue'
const route = useRoute()
// 计算是否显示导航栏和页脚
const shouldShowNavBar = computed(() => {
// 登录和注册页面不显示导航栏
return !['login', 'register'].includes(route.name)
})
const shouldShowFooter = computed(() => {
// 登录和注册页面不显示页脚
return !['login', 'register'].includes(route.name)
})
// 监听路由变化,动态设置页面样式
watch(route, (newRoute) => {
console.log('路由变化:', newRoute.name)
}, { immediate: true })
console.log('App.vue 加载成功')
</script>
<style>
/* 全局样式重置 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
height: 100%;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
color: #333;
background: #f5f5f5;
}
#app {
min-height: 100vh;
display: flex;
flex-direction: column;
position: relative;
}
main {
flex: 1;
padding: 0;
position: relative;
}
main.with-navbar {
padding-top: 0; /* NavBar 是 fixed 定位,不需要 padding-top */
}
/* 确保登录页面全屏显示 */
#app .login-page {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 1000;
}
/* ========== 页面特殊样式 ========== */
/* 欢迎页面 - 彩虹渐变背景 */
#app[data-route="Welcome"] {
background: linear-gradient(45deg, #ff6b6b, #4ecdc4, #45b7d1, #96ceb4, #feca57, #ff9ff3);
background-size: 400% 400%;
animation: rainbowShift 8s ease-in-out infinite;
}
@keyframes rainbowShift {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
/* 首页 - 科技感蓝色渐变 */
#app[data-route="Home"] {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
position: relative;
}
#app[data-route="Home"]::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background:
radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(255, 119, 198, 0.3) 0%, transparent 50%),
radial-gradient(circle at 40% 40%, rgba(120, 219, 255, 0.3) 0%, transparent 50%);
animation: homeGlow 6s ease-in-out infinite alternate;
pointer-events: none;
z-index: 1;
}
@keyframes homeGlow {
0% { opacity: 0.3; }
100% { opacity: 0.7; }
}
/* 个人主页 - 深色科技风 */
#app[data-route="Profile"] {
background: #0a0a0a;
color: white;
}
#app[data-route="Profile"]::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background:
radial-gradient(circle at 10% 20%, rgba(64, 158, 255, 0.1) 0%, transparent 50%),
radial-gradient(circle at 90% 80%, rgba(103, 194, 58, 0.1) 0%, transparent 50%),
radial-gradient(circle at 50% 50%, rgba(230, 162, 60, 0.05) 0%, transparent 50%);
animation: profileGlow 6s ease-in-out infinite alternate;
pointer-events: none;
z-index: 1;
}
@keyframes profileGlow {
0% { opacity: 0.3; }
100% { opacity: 0.6; }
}
/* 订单管理 - 商务紫色渐变 */
#app[data-route="Orders"] {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
#app[data-route="Orders"]::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background:
radial-gradient(circle at 30% 20%, rgba(255, 255, 255, 0.1) 0%, transparent 50%),
radial-gradient(circle at 70% 80%, rgba(255, 255, 255, 0.05) 0%, transparent 50%);
animation: ordersPulse 5s ease-in-out infinite alternate;
pointer-events: none;
z-index: 1;
}
@keyframes ordersPulse {
0% { opacity: 0.3; transform: scale(1); }
100% { opacity: 0.6; transform: scale(1.02); }
}
/* 支付记录 - 金色渐变 */
#app[data-route="Payments"] {
background: linear-gradient(135deg, #f6d365 0%, #fda085 100%);
}
#app[data-route="Payments"]::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background:
radial-gradient(circle at 25% 25%, rgba(255, 215, 0, 0.2) 0%, transparent 50%),
radial-gradient(circle at 75% 75%, rgba(255, 165, 0, 0.1) 0%, transparent 50%);
animation: paymentShine 4s ease-in-out infinite alternate;
pointer-events: none;
z-index: 1;
}
@keyframes paymentShine {
0% { opacity: 0.4; }
100% { opacity: 0.8; }
}
/* 我的作品 - 创意绿色渐变 */
#app[data-route="MyWorks"] {
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
}
#app[data-route="MyWorks"]::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background:
radial-gradient(circle at 40% 60%, rgba(168, 237, 234, 0.3) 0%, transparent 50%),
radial-gradient(circle at 60% 40%, rgba(254, 214, 227, 0.3) 0%, transparent 50%);
animation: worksFloat 7s ease-in-out infinite alternate;
pointer-events: none;
z-index: 1;
}
@keyframes worksFloat {
0% { transform: translateY(0px) rotate(0deg); }
100% { transform: translateY(-15px) rotate(2deg); }
}
/* 文生视频 - 蓝色科技风 */
#app[data-route="TextToVideo"] {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
#app[data-route="TextToVideo"]::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background:
radial-gradient(circle at 20% 20%, rgba(102, 126, 234, 0.3) 0%, transparent 50%),
radial-gradient(circle at 80% 80%, rgba(118, 75, 162, 0.3) 0%, transparent 50%);
animation: textVideoFlow 6s ease-in-out infinite alternate;
pointer-events: none;
z-index: 1;
}
@keyframes textVideoFlow {
0% { opacity: 0.3; }
100% { opacity: 0.7; }
}
/* 图生视频 - 紫色梦幻风 */
#app[data-route="ImageToVideo"] {
background: linear-gradient(135deg, #a8c0ff 0%, #3f2b96 100%);
}
#app[data-route="ImageToVideo"]::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background:
radial-gradient(circle at 30% 30%, rgba(168, 192, 255, 0.3) 0%, transparent 50%),
radial-gradient(circle at 70% 70%, rgba(63, 43, 150, 0.3) 0%, transparent 50%);
animation: imageVideoDream 8s ease-in-out infinite alternate;
pointer-events: none;
z-index: 1;
}
@keyframes imageVideoDream {
0% { opacity: 0.2; transform: scale(1); }
100% { opacity: 0.6; transform: scale(1.05); }
}
/* 分镜视频 - 橙色活力风 */
#app[data-route="StoryboardVideo"] {
background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%);
}
#app[data-route="StoryboardVideo"]::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background:
radial-gradient(circle at 25% 75%, rgba(255, 154, 158, 0.3) 0%, transparent 50%),
radial-gradient(circle at 75% 25%, rgba(254, 207, 239, 0.3) 0%, transparent 50%);
animation: storyboardBounce 5s ease-in-out infinite alternate;
pointer-events: none;
z-index: 1;
}
@keyframes storyboardBounce {
0% { opacity: 0.3; transform: translateY(0px); }
100% { opacity: 0.7; transform: translateY(-10px); }
}
/* 会员订阅 - 奢华金色 */
#app[data-route="Subscription"] {
background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%);
}
#app[data-route="Subscription"]::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background:
radial-gradient(circle at 50% 50%, rgba(255, 215, 0, 0.2) 0%, transparent 50%),
radial-gradient(circle at 20% 80%, rgba(252, 182, 159, 0.3) 0%, transparent 50%);
animation: subscriptionLuxury 6s ease-in-out infinite alternate;
pointer-events: none;
z-index: 1;
}
@keyframes subscriptionLuxury {
0% { opacity: 0.4; }
100% { opacity: 0.8; }
}
/* 管理员页面 - 深色专业风 */
#app[data-route="AdminDashboard"],
#app[data-route="AdminOrders"],
#app[data-route="AdminUsers"] {
background: #1a1a1a;
color: white;
}
#app[data-route="AdminDashboard"]::before,
#app[data-route="AdminOrders"]::before,
#app[data-route="AdminUsers"]::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background:
radial-gradient(circle at 10% 10%, rgba(0, 150, 255, 0.1) 0%, transparent 50%),
radial-gradient(circle at 90% 90%, rgba(255, 0, 150, 0.1) 0%, transparent 50%),
radial-gradient(circle at 50% 50%, rgba(0, 255, 150, 0.05) 0%, transparent 50%);
animation: adminTech 8s ease-in-out infinite alternate;
pointer-events: none;
z-index: 1;
}
@keyframes adminTech {
0% { opacity: 0.2; }
100% { opacity: 0.5; }
}
/* 注册页面 - 清新绿色渐变 */
#app[data-route="Register"] {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
#app[data-route="Register"]::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background:
radial-gradient(circle at 25% 25%, rgba(255, 255, 255, 0.1) 0%, transparent 50%),
radial-gradient(circle at 75% 75%, rgba(255, 255, 255, 0.05) 0%, transparent 50%);
animation: registerFloat 4s ease-in-out infinite alternate;
pointer-events: none;
z-index: 1;
}
@keyframes registerFloat {
0% { transform: translateY(0px) rotate(0deg); }
100% { transform: translateY(-10px) rotate(1deg); }
}
/* 内容层级确保在所有背景效果之上 */
#app main > * {
position: relative;
z-index: 2;
}
/* 响应式设计 */
@media (max-width: 768px) {
main {
padding: 0;
}
body {
font-size: 14px;
}
/* 移动端减少动画效果 */
#app[data-route]::before {
animation-duration: 10s;
}
}
/* Element Plus 样式覆盖 */
.el-button {
font-family: inherit;
}
.el-input {
font-family: inherit;
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
</style>

View File

@@ -0,0 +1,60 @@
import api from './request'
// 认证相关API
export const login = (credentials) => {
return api.post('/auth/login', credentials)
}
export const register = (userData) => {
return api.post('/auth/register', userData)
}
export const logout = () => {
return api.post('/auth/logout')
}
export const getCurrentUser = () => {
return api.get('/auth/me')
}
// 用户相关API
export const getUsers = (params) => {
return api.get('/users', { params })
}
export const getUserById = (id) => {
return api.get(`/users/${id}`)
}
export const createUser = (userData) => {
return api.post('/users', userData)
}
export const updateUser = (id, userData) => {
return api.put(`/users/${id}`, userData)
}
export const deleteUser = (id) => {
return api.delete(`/users/${id}`)
}
// 检查用户名是否存在
export const checkUsernameExists = (username) => {
return api.get(`/public/users/exists/username`, {
params: { value: username }
})
}
// 检查邮箱是否存在
export const checkEmailExists = (email) => {
return api.get(`/public/users/exists/email`, {
params: { value: email }
})
}

View File

@@ -0,0 +1,38 @@
import request from './request'
export const dashboardApi = {
// 获取仪表盘概览数据
getOverview() {
return request.get('/dashboard/overview')
},
// 获取日活数据
getDailyActiveUsers() {
return request.get('/dashboard/daily-active-users')
},
// 获取收入趋势数据
getRevenueTrend() {
return request.get('/dashboard/revenue-trend')
},
// 获取订单状态分布
getOrderStatusDistribution() {
return request.get('/dashboard/order-status-distribution')
},
// 获取支付方式分布
getPaymentMethodDistribution() {
return request.get('/dashboard/payment-method-distribution')
},
// 获取最近订单列表
getRecentOrders() {
return request.get('/dashboard/recent-orders')
},
// 获取所有仪表盘数据
getAllData() {
return request.get('/dashboard/all')
}
}

View File

@@ -0,0 +1,62 @@
import api from './request'
// 订单相关API
export const getOrders = (params) => {
return api.get('/orders', { params })
}
export const getOrderById = (id) => {
return api.get(`/orders/${id}`)
}
export const createOrder = (orderData) => {
return api.post('/orders/create', orderData)
}
export const updateOrderStatus = (id, status, notes) => {
return api.post(`/orders/${id}/status`, {
status,
notes
})
}
export const cancelOrder = (id, reason) => {
return api.post(`/orders/${id}/cancel`, {
reason
})
}
export const shipOrder = (id, trackingNumber) => {
return api.post(`/orders/${id}/ship`, {
trackingNumber
})
}
export const completeOrder = (id) => {
return api.post(`/orders/${id}/complete`)
}
export const createOrderPayment = (id, paymentMethod) => {
return api.post(`/orders/${id}/pay`, {
paymentMethod
})
}
// 管理员订单API
export const getAdminOrders = (params) => {
return api.get('/orders/admin', { params })
}
// 订单统计API
export const getOrderStats = () => {
return api.get('/orders/stats')
}

View File

@@ -0,0 +1,62 @@
import api from './request'
// 支付相关API
export const getPayments = (params) => {
return api.get('/payments', { params })
}
export const getPaymentById = (id) => {
return api.get(`/payments/${id}`)
}
export const createPayment = (paymentData) => {
return api.post('/payments/create', paymentData)
}
export const createTestPayment = (paymentData) => {
return api.post('/payments/create-test', paymentData)
}
export const updatePaymentStatus = (id, status) => {
return api.put(`/payments/${id}/status`, { status })
}
export const confirmPaymentSuccess = (id, externalTransactionId) => {
return api.post(`/payments/${id}/success`, {
externalTransactionId
})
}
export const confirmPaymentFailure = (id, failureReason) => {
return api.post(`/payments/${id}/failure`, {
failureReason
})
}
// 测试支付完成API
export const testPaymentComplete = (id) => {
return api.post(`/payments/${id}/test-complete`)
}
// 支付宝支付API
export const createAlipayPayment = (paymentData) => {
return api.post(`/payments/alipay/create`, paymentData)
}
export const handleAlipayCallback = (params) => {
return api.post('/payments/alipay/callback', params)
}
// PayPal支付API
export const createPayPalPayment = (paymentData) => {
return api.post(`/payments/paypal/create`, paymentData)
}
export const handlePayPalCallback = (params) => {
return api.post('/payment/paypal/callback', params)
}
// 支付统计API
export const getPaymentStats = () => {
return api.get('/payments/stats')
}

View File

@@ -0,0 +1,68 @@
import axios from 'axios'
import { ElMessage } from 'element-plus'
import router from '@/router'
// 创建axios实例
const api = axios.create({
baseURL: 'http://localhost:8080/api',
timeout: 10000,
withCredentials: true,
headers: {
'Content-Type': 'application/json'
}
})
// 请求拦截器
api.interceptors.request.use(
(config) => {
// 使用JWT认证添加Authorization头
const token = sessionStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => {
console.error('请求拦截器错误:', error)
return Promise.reject(error)
}
)
// 响应拦截器
api.interceptors.response.use(
(response) => {
return response.data
},
(error) => {
if (error.response) {
const { status, data } = error.response
switch (status) {
case 401:
ElMessage.error('未授权,请重新登录')
sessionStorage.removeItem('token')
sessionStorage.removeItem('user')
// 使用Vue Router进行路由跳转避免页面刷新
router.push('/login')
break
case 403:
ElMessage.error('权限不足')
break
case 404:
ElMessage.error('请求的资源不存在')
break
case 500:
ElMessage.error('服务器内部错误')
break
default:
ElMessage.error(data.message || '请求失败')
}
} else {
ElMessage.error('网络错误,请检查网络连接')
}
return Promise.reject(error)
}
)
export default api

View File

@@ -0,0 +1,84 @@
<template>
<el-footer class="footer">
<div class="footer-content">
<div class="footer-info">
<p>&copy; 2024 AIGC Demo. All rights reserved.</p>
<p>基于 Vue.js 3 + Element Plus 构建</p>
</div>
<div class="footer-links">
<a href="#" class="footer-link">关于我们</a>
<a href="#" class="footer-link">联系我们</a>
<a href="#" class="footer-link">隐私政策</a>
<a href="#" class="footer-link">服务条款</a>
</div>
</div>
</el-footer>
</template>
<script setup>
// Footer组件逻辑
</script>
<style scoped>
.footer {
height: 60px;
background-color: #f5f5f5;
border-top: 1px solid #e4e7ed;
}
.footer-content {
display: flex;
justify-content: space-between;
align-items: center;
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
height: 100%;
}
.footer-info {
color: #606266;
font-size: 14px;
}
.footer-info p {
margin: 0;
line-height: 1.5;
}
.footer-links {
display: flex;
gap: 20px;
}
.footer-link {
color: #606266;
text-decoration: none;
font-size: 14px;
transition: color 0.3s;
}
.footer-link:hover {
color: #409EFF;
}
@media (max-width: 768px) {
.footer-content {
flex-direction: column;
gap: 10px;
}
.footer-links {
gap: 15px;
}
}
</style>

View File

@@ -0,0 +1,268 @@
<template>
<el-header class="navbar">
<div class="navbar-container">
<!-- Logo -->
<div class="navbar-brand">
<router-link to="/" class="brand-link">
<span class="brand-text">AIGC Demo</span>
</router-link>
</div>
<!-- 导航菜单 -->
<el-menu
mode="horizontal"
class="navbar-menu"
background-color="#409EFF"
text-color="#fff"
active-text-color="#ffd04b"
router
@select="handleMenuSelect"
>
<el-menu-item index="/welcome">
<span>欢迎页</span>
</el-menu-item>
<el-menu-item index="/home">
<span>首页</span>
</el-menu-item>
<el-menu-item v-if="userStore.isAuthenticated" index="/profile">
<span>个人主页</span>
</el-menu-item>
<el-menu-item v-if="userStore.isAuthenticated" index="/orders">
<span>订单管理</span>
</el-menu-item>
<el-menu-item v-if="userStore.isAuthenticated" index="/payments">
<span>支付记录</span>
</el-menu-item>
<el-menu-item v-if="userStore.isAdmin" index="/admin/orders">
<span>后台管理</span>
</el-menu-item>
<el-menu-item v-if="userStore.isAdmin" index="/admin/users">
<span>用户管理</span>
</el-menu-item>
<el-menu-item v-if="userStore.isAdmin" index="/admin/dashboard">
<span>数据仪表盘</span>
</el-menu-item>
</el-menu>
<!-- 快速切换提示暂时隐藏 -->
<!-- <div class="quick-switch-hint" v-if="showShortcutHint">
<el-tooltip content="使用 Alt + 数字键快速切换页面" placement="bottom">
<el-icon><Keyboard /></el-icon>
</el-tooltip>
</div> -->
<!-- 用户菜单 -->
<div class="navbar-user">
<template v-if="userStore.isAuthenticated">
<el-dropdown @command="handleUserCommand">
<span class="user-dropdown">
<span>{{ userStore.username }}</span>
<el-tag v-if="userStore.user?.points" size="small" type="success" class="points-tag">
{{ userStore.user.points }}积分
</el-tag>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="profile">
个人资料
</el-dropdown-item>
<el-dropdown-item v-if="userStore.isAdmin" command="admin">
后台管理
</el-dropdown-item>
<el-dropdown-item command="settings">
设置
</el-dropdown-item>
<el-dropdown-item divided command="logout">
退出登录
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<template v-else>
<el-button type="primary" plain @click="$router.push('/login')">
登录
</el-button>
<el-button type="success" plain @click="$router.push('/register')">
注册
</el-button>
</template>
</div>
</div>
</el-header>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { useUserStore } from '@/stores/user'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
const userStore = useUserStore()
const router = useRouter()
// 显示快捷键提示(暂时禁用)
// const showShortcutHint = ref(true)
// 快速切换处理函数
const handleMenuSelect = (index) => {
// 使用replace而不是push避免浏览器历史记录堆积
router.replace(index)
}
// 暂时禁用快捷键功能,确保基本功能正常
// const handleKeydown = (event) => {
// // 快捷键功能暂时禁用
// }
// onMounted(() => {
// // 暂时不添加键盘事件监听
// })
// onUnmounted(() => {
// // 暂时不移除键盘事件监听
// })
const handleUserCommand = async (command) => {
switch (command) {
case 'profile':
ElMessage.info('个人资料功能开发中')
break
case 'admin':
router.push('/admin/dashboard')
break
case 'settings':
ElMessage.info('设置功能开发中')
break
case 'logout':
try {
await ElMessageBox.confirm('确定要退出登录吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
await userStore.logoutUser()
ElMessage.success('退出登录成功')
router.push('/')
} catch (error) {
// 用户取消
}
break
}
}
</script>
<style scoped>
.navbar {
height: 60px;
line-height: 60px;
padding: 0;
}
.navbar-container {
display: flex;
align-items: center;
height: 100%;
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
.navbar-brand {
margin-right: 40px;
}
.brand-link {
display: flex;
align-items: center;
text-decoration: none;
color: white;
font-size: 20px;
font-weight: bold;
}
.brand-icon {
margin-right: 8px;
font-size: 24px;
}
.brand-text {
font-size: 18px;
}
.navbar-menu {
flex: 1;
border-bottom: none;
}
.navbar-menu .el-menu-item {
height: 60px;
line-height: 60px;
border-bottom: none;
}
.navbar-menu .el-menu-item:hover {
background-color: rgba(255, 255, 255, 0.1) !important;
}
.navbar-user {
margin-left: auto;
}
.user-dropdown {
display: flex;
align-items: center;
color: white;
cursor: pointer;
padding: 0 12px;
border-radius: 4px;
transition: background-color 0.3s;
}
.user-dropdown:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.points-tag {
margin-left: 4px;
font-size: 12px;
}
.user-dropdown .el-icon {
margin-right: 4px;
}
.navbar-user .el-button {
margin-left: 8px;
}
.shortcut-hint {
font-size: 10px;
opacity: 0.7;
margin-left: 8px;
padding: 2px 4px;
background-color: rgba(255, 255, 255, 0.2);
border-radius: 3px;
font-family: monospace;
}
.quick-switch-hint {
margin-right: 20px;
cursor: pointer;
color: rgba(255, 255, 255, 0.8);
transition: color 0.3s;
}
.quick-switch-hint:hover {
color: white;
}
</style>

27
demo/frontend/src/main.js Normal file
View File

@@ -0,0 +1,27 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
import App from './App.vue'
import router from './router'
import { useUserStore } from './stores/user'
const app = createApp(App)
// 注册Element Plus图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
const pinia = createPinia()
app.use(pinia)
app.use(router)
app.use(ElementPlus, {
locale: zhCn,
})
// 立即挂载应用
app.mount('#app')

View File

@@ -0,0 +1,229 @@
import { createRouter, createWebHistory } from 'vue-router'
import { useUserStore } from '@/stores/user'
// 路由组件
import Home from '@/views/Home.vue'
import Login from '@/views/Login.vue'
import Register from '@/views/Register.vue'
import Orders from '@/views/Orders.vue'
import OrderDetail from '@/views/OrderDetail.vue'
import OrderCreate from '@/views/OrderCreate.vue'
import Payments from '@/views/Payments.vue'
import PaymentCreate from '@/views/PaymentCreate.vue'
import AdminOrders from '@/views/AdminOrders.vue'
import AdminUsers from '@/views/AdminUsers.vue'
import AdminDashboard from '@/views/AdminDashboard.vue'
import Dashboard from '@/views/Dashboard.vue'
import Welcome from '@/views/Welcome.vue'
import Profile from '@/views/Profile.vue'
import Subscription from '@/views/Subscription.vue'
import MyWorks from '@/views/MyWorks.vue'
import VideoDetail from '@/views/VideoDetail.vue'
import TextToVideo from '@/views/TextToVideo.vue'
import TextToVideoCreate from '@/views/TextToVideoCreate.vue'
import ImageToVideo from '@/views/ImageToVideo.vue'
import ImageToVideoCreate from '@/views/ImageToVideoCreate.vue'
import StoryboardVideo from '@/views/StoryboardVideo.vue'
import StoryboardVideoCreate from '@/views/StoryboardVideoCreate.vue'
const routes = [
{
path: '/works',
name: 'MyWorks',
component: MyWorks,
meta: { title: '我的作品', requiresAuth: true }
},
{
path: '/video/:id',
name: 'VideoDetail',
component: VideoDetail,
meta: { title: '视频详情', requiresAuth: true }
},
{
path: '/text-to-video',
name: 'TextToVideo',
component: TextToVideo,
meta: { title: '文生视频', requiresAuth: true }
},
{
path: '/text-to-video/create',
name: 'TextToVideoCreate',
component: TextToVideoCreate,
meta: { title: '文生视频创作', requiresAuth: true }
},
{
path: '/image-to-video',
name: 'ImageToVideo',
component: ImageToVideo,
meta: { title: '图生视频', requiresAuth: true }
},
{
path: '/image-to-video/create',
name: 'ImageToVideoCreate',
component: ImageToVideoCreate,
meta: { title: '图生视频创作', requiresAuth: true }
},
{
path: '/storyboard-video',
name: 'StoryboardVideo',
component: StoryboardVideo,
meta: { title: '分镜视频', requiresAuth: true }
},
{
path: '/storyboard-video/create',
name: 'StoryboardVideoCreate',
component: StoryboardVideoCreate,
meta: { title: '分镜视频创作', requiresAuth: true }
},
{
path: '/',
redirect: '/welcome' // 重定向到欢迎页面
},
{
path: '/welcome',
name: 'Welcome',
component: Welcome,
meta: { title: '欢迎', guest: true }
},
{
path: '/home',
name: 'Home',
component: Home,
meta: { title: '首页', requiresAuth: true }
},
{
path: '/profile',
name: 'Profile',
component: Profile,
meta: { title: '个人主页', requiresAuth: true }
},
{
path: '/subscription',
name: 'Subscription',
component: Subscription,
meta: { title: '会员订阅', requiresAuth: true }
},
{
path: '/login',
name: 'Login',
component: Login,
meta: { title: '登录', guest: true }
},
{
path: '/register',
name: 'Register',
component: Register,
meta: { title: '注册', guest: true }
},
{
path: '/orders',
name: 'Orders',
component: Orders,
meta: { title: '订单管理', requiresAuth: true }
},
{
path: '/orders/:id',
name: 'OrderDetail',
component: OrderDetail,
meta: { title: '订单详情', requiresAuth: true }
},
{
path: '/orders/create',
name: 'OrderCreate',
component: OrderCreate,
meta: { title: '创建订单', requiresAuth: true }
},
{
path: '/payments',
name: 'Payments',
component: Payments,
meta: { title: '支付记录', requiresAuth: true }
},
{
path: '/payments/create',
name: 'PaymentCreate',
component: PaymentCreate,
meta: { title: '创建支付', requiresAuth: true }
},
{
path: '/admin/orders',
name: 'AdminOrders',
component: AdminOrders,
meta: { title: '订单管理', requiresAuth: true, requiresAdmin: true }
},
{
path: '/admin/users',
name: 'AdminUsers',
component: AdminUsers,
meta: { title: '用户管理', requiresAuth: true, requiresAdmin: true }
},
{
path: '/admin/dashboard',
name: 'AdminDashboard',
component: AdminDashboard,
meta: { title: '后台管理', requiresAuth: true, requiresAdmin: true }
}
]
const router = createRouter({
history: createWebHistory(),
routes,
// 添加路由缓存配置
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
return { top: 0 }
}
}
})
// 路由守卫
router.beforeEach(async (to, from, next) => {
try {
const userStore = useUserStore()
// 初始化用户状态
if (!userStore.initialized) {
await userStore.init()
}
// 检查是否需要认证
if (to.meta.requiresAuth) {
if (!userStore.isAuthenticated) {
// 未登录,跳转到登录页
next({
path: '/login',
query: { redirect: to.fullPath }
})
return
}
// 检查管理员权限
if (to.meta.requiresAdmin && !userStore.isAdmin) {
// 权限不足,跳转到首页
next('/home')
return
}
}
// 已登录用户访问登录页,重定向到首页
if (to.meta.guest && userStore.isAuthenticated) {
next('/home')
return
}
// 设置页面标题
if (to.meta.title) {
document.title = `${to.meta.title} - AIGC Demo`
}
next()
} catch (error) {
console.error('路由守卫错误:', error)
// 发生错误时,允许访问但显示错误信息
next()
}
})
export default router

View File

@@ -0,0 +1,219 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { getOrders, getOrderById, createOrder, updateOrderStatus, cancelOrder, shipOrder, completeOrder } from '@/api/orders'
export const useOrderStore = defineStore('orders', () => {
// 状态
const orders = ref([])
const currentOrder = ref(null)
const loading = ref(false)
const pagination = ref({
page: 0,
size: 10,
total: 0,
totalPages: 0
})
// 获取订单列表
const fetchOrders = async (params = {}) => {
try {
loading.value = true
console.log('OrderStore: 开始获取订单,参数:', params)
const response = await getOrders(params)
console.log('OrderStore: API原始响应:', response)
if (response.success) {
orders.value = response.data.content || response.data
pagination.value = {
page: response.data.number || 0,
size: response.data.size || 10,
total: response.data.totalElements || response.data.length,
totalPages: response.data.totalPages || 1
}
console.log('OrderStore: 处理后的订单数据:', orders.value)
console.log('OrderStore: 分页信息:', pagination.value)
} else {
console.error('OrderStore: API返回失败:', response.message)
}
return response
} catch (error) {
console.error('OrderStore: 获取订单异常:', error)
return { success: false, message: '获取订单列表失败' }
} finally {
loading.value = false
}
}
// 获取订单详情
const fetchOrderById = async (id) => {
try {
loading.value = true
const response = await getOrderById(id)
if (response.success) {
currentOrder.value = response.data
}
return response
} catch (error) {
console.error('Fetch order error:', error)
return { success: false, message: '获取订单详情失败' }
} finally {
loading.value = false
}
}
// 创建订单
const createNewOrder = async (orderData) => {
try {
loading.value = true
const response = await createOrder(orderData)
if (response.success) {
// 刷新订单列表
await fetchOrders()
}
return response
} catch (error) {
console.error('Create order error:', error)
return { success: false, message: '创建订单失败' }
} finally {
loading.value = false
}
}
// 更新订单状态
const updateOrder = async (id, status, notes) => {
try {
loading.value = true
const response = await updateOrderStatus(id, status, notes)
if (response.success) {
// 更新本地订单状态
const order = orders.value.find(o => o.id === id)
if (order) {
order.status = status
order.updatedAt = new Date().toISOString()
}
// 更新当前订单
if (currentOrder.value && currentOrder.value.id === id) {
currentOrder.value.status = status
currentOrder.value.updatedAt = new Date().toISOString()
}
}
return response
} catch (error) {
console.error('Update order error:', error)
return { success: false, message: '更新订单状态失败' }
} finally {
loading.value = false
}
}
// 取消订单
const cancelOrderById = async (id, reason) => {
try {
loading.value = true
const response = await cancelOrder(id, reason)
if (response.success) {
// 更新本地订单状态
const order = orders.value.find(o => o.id === id)
if (order) {
order.status = 'CANCELLED'
order.cancelledAt = new Date().toISOString()
}
// 更新当前订单
if (currentOrder.value && currentOrder.value.id === id) {
currentOrder.value.status = 'CANCELLED'
currentOrder.value.cancelledAt = new Date().toISOString()
}
}
return response
} catch (error) {
console.error('Cancel order error:', error)
return { success: false, message: '取消订单失败' }
} finally {
loading.value = false
}
}
// 发货
const shipOrderById = async (id, trackingNumber) => {
try {
loading.value = true
const response = await shipOrder(id, trackingNumber)
if (response.success) {
// 更新本地订单状态
const order = orders.value.find(o => o.id === id)
if (order) {
order.status = 'SHIPPED'
order.shippedAt = new Date().toISOString()
}
// 更新当前订单
if (currentOrder.value && currentOrder.value.id === id) {
currentOrder.value.status = 'SHIPPED'
currentOrder.value.shippedAt = new Date().toISOString()
}
}
return response
} catch (error) {
console.error('Ship order error:', error)
return { success: false, message: '发货失败' }
} finally {
loading.value = false
}
}
// 完成订单
const completeOrderById = async (id) => {
try {
loading.value = true
const response = await completeOrder(id)
if (response.success) {
// 更新本地订单状态
const order = orders.value.find(o => o.id === id)
if (order) {
order.status = 'COMPLETED'
order.deliveredAt = new Date().toISOString()
}
// 更新当前订单
if (currentOrder.value && currentOrder.value.id === id) {
currentOrder.value.status = 'COMPLETED'
currentOrder.value.deliveredAt = new Date().toISOString()
}
}
return response
} catch (error) {
console.error('Complete order error:', error)
return { success: false, message: '完成订单失败' }
} finally {
loading.value = false
}
}
return {
// 状态
orders,
currentOrder,
loading,
pagination,
// 方法
fetchOrders,
fetchOrderById,
createNewOrder,
updateOrder,
cancelOrderById,
shipOrderById,
completeOrderById
}
})

View File

@@ -0,0 +1,159 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { login, register, logout, getCurrentUser } from '@/api/auth'
export const useUserStore = defineStore('user', () => {
// 状态 - 从 sessionStorage 尝试恢复用户信息
const user = ref(null)
const token = ref(null)
const loading = ref(false)
const initialized = ref(false)
try {
const cachedUser = sessionStorage.getItem('user')
const cachedToken = sessionStorage.getItem('token')
if (cachedUser && cachedToken) {
user.value = JSON.parse(cachedUser)
token.value = cachedToken
}
} catch (_) {
// ignore sessionStorage parse errors
}
// 计算属性
const isAuthenticated = computed(() => !!user.value)
const isAdmin = computed(() => user.value?.role === 'ROLE_ADMIN')
const username = computed(() => user.value?.username || '')
// 登录
const loginUser = async (credentials) => {
try {
loading.value = true
const response = await login(credentials)
if (response.success) {
// 使用JWT认证保存token和用户信息
user.value = response.data.user
token.value = response.data.token
// 保存到sessionStorage关闭页面时自动清除
sessionStorage.setItem('token', response.data.token)
sessionStorage.setItem('user', JSON.stringify(user.value))
return { success: true }
} else {
return { success: false, message: response.message }
}
} catch (error) {
console.error('Login error:', error)
return { success: false, message: '登录失败,请检查网络连接' }
} finally {
loading.value = false
}
}
// 注册
const registerUser = async (userData) => {
try {
loading.value = true
const response = await register(userData)
if (response.success) {
return { success: true, message: '注册成功,请登录' }
} else {
return { success: false, message: response.message }
}
} catch (error) {
console.error('Register error:', error)
return { success: false, message: '注册失败,请检查网络连接' }
} finally {
loading.value = false
}
}
// 登出
const logoutUser = async () => {
try {
// JWT无状态直接清除sessionStorage即可
token.value = null
user.value = null
sessionStorage.removeItem('token')
sessionStorage.removeItem('user')
} catch (error) {
console.error('Logout error:', error)
}
}
// 获取当前用户信息
const fetchCurrentUser = async () => {
try {
const response = await getCurrentUser()
if (response.success) {
user.value = response.data
sessionStorage.setItem('user', JSON.stringify(user.value))
} else {
// 会话无效,清除本地存储
clearUserData()
}
} catch (error) {
console.error('Fetch user error:', error)
// 请求失败时不强制清除,保持现有本地态
}
}
// 清除用户数据
const clearUserData = () => {
token.value = null
user.value = null
// 清除 sessionStorage 中的用户数据
sessionStorage.removeItem('token')
sessionStorage.removeItem('user')
}
// 初始化
const init = async () => {
if (initialized.value) {
return
}
// 从sessionStorage恢复用户状态
const savedToken = sessionStorage.getItem('token')
const savedUser = sessionStorage.getItem('user')
if (savedToken && savedUser) {
try {
token.value = savedToken
user.value = JSON.parse(savedUser)
// 只在开发环境输出详细日志
if (process.env.NODE_ENV === 'development') {
console.log('恢复用户状态:', user.value?.username, '角色:', user.value?.role)
}
} catch (error) {
console.error('Failed to restore user state:', error)
clearUserData()
}
}
initialized.value = true
}
return {
// 状态
user,
token,
loading,
// 计算属性
isAuthenticated,
isAdmin,
username,
// 方法
loginUser,
registerUser,
logoutUser,
fetchCurrentUser,
clearUserData,
init,
initialized
}
})

View File

@@ -0,0 +1,556 @@
<template>
<div class="admin-dashboard">
<!-- 左侧导航栏 -->
<aside class="sidebar">
<div class="logo">
<span class="logo-text">LOGO</span>
</div>
<nav class="nav-menu">
<div class="nav-item active">
<el-icon><Grid /></el-icon>
<span>数据仪表台</span>
</div>
<div class="nav-item" @click="goToUsers">
<el-icon><User /></el-icon>
<span>会员管理</span>
</div>
<div class="nav-item" @click="goToOrders">
<el-icon><ShoppingCart /></el-icon>
<span>订单管理</span>
</div>
<div class="nav-item">
<el-icon><Document /></el-icon>
<span>API管理</span>
</div>
<div class="nav-item">
<el-icon><Briefcase /></el-icon>
<span>生成任务记录</span>
</div>
<div class="nav-item">
<el-icon><Setting /></el-icon>
<span>系统设置</span>
</div>
</nav>
<div class="sidebar-footer">
<div class="online-users">
<span>当前在线用户: </span>
<span class="online-count">87/500</span>
</div>
<div class="system-uptime">
<span>系统运行时间: 48小时32分</span>
</div>
</div>
</aside>
<!-- 主内容区域 -->
<main class="main-content">
<!-- 顶部搜索栏 -->
<header class="top-header">
<div class="search-bar">
<el-icon class="search-icon"><Search /></el-icon>
<input type="text" placeholder="搜索你的想要的内容" class="search-input">
</div>
<div class="header-right">
<div class="notification-icon">
<el-icon><Bell /></el-icon>
<div class="notification-badge"></div>
</div>
<div class="user-avatar">
<el-icon><Avatar /></el-icon>
<el-icon class="arrow-down"><ArrowDown /></el-icon>
</div>
</div>
</header>
<!-- 统计卡片 -->
<div class="stats-cards">
<div class="stat-card">
<div class="stat-icon users">
<el-icon><User /></el-icon>
</div>
<div class="stat-content">
<div class="stat-title">用户总数</div>
<div class="stat-number">12,847</div>
<div class="stat-change positive">+12% 较上月同期</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon paid-users">
<el-icon><User /></el-icon>
</div>
<div class="stat-content">
<div class="stat-title">付费用户数</div>
<div class="stat-number">3,215</div>
<div class="stat-change negative">-5% 较上月同期</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon revenue">
<el-icon><Money /></el-icon>
</div>
<div class="stat-content">
<div class="stat-title">今日收入</div>
<div class="stat-number">¥28,450</div>
<div class="stat-change positive">+15% 较上月同期</div>
</div>
</div>
</div>
<!-- 图表区域 -->
<div class="charts-section">
<!-- 日活用户趋势 -->
<div class="chart-card">
<div class="chart-header">
<h3>日活用户趋势</h3>
<el-select v-model="selectedYear" class="year-select">
<el-option label="2025年" value="2025"></el-option>
<el-option label="2024年" value="2024"></el-option>
</el-select>
</div>
<div class="chart-content">
<div class="chart-placeholder">
<div class="chart-title">日活用户趋势图</div>
<div class="chart-description">显示每日活跃用户数量变化趋势</div>
</div>
</div>
</div>
<!-- 用户转化率 -->
<div class="chart-card">
<div class="chart-header">
<h3>用户转化率</h3>
<el-select v-model="selectedYear2" class="year-select">
<el-option label="2025年" value="2025"></el-option>
<el-option label="2024年" value="2024"></el-option>
</el-select>
</div>
<div class="chart-content">
<div class="chart-placeholder">
<div class="chart-title">用户转化率图</div>
<div class="chart-description">显示各月份用户转化率情况</div>
</div>
</div>
</div>
</div>
</main>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import {
Grid,
User,
ShoppingCart,
Document,
Briefcase,
Setting,
Search,
Bell,
Avatar,
ArrowDown,
Money
} from '@element-plus/icons-vue'
const router = useRouter()
// 年份选择
const selectedYear = ref('2025')
const selectedYear2 = ref('2025')
// 导航函数
const goToUsers = () => {
router.push('/admin/users')
}
const goToOrders = () => {
router.push('/admin/orders')
}
// 页面加载时获取数据
onMounted(() => {
console.log('后台管理页面加载完成')
})
</script>
<style scoped>
.admin-dashboard {
display: flex;
height: 100vh;
background: #f8f9fa;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
/* 左侧导航栏 */
.sidebar {
width: 240px;
background: #ffffff;
border-right: 1px solid #e9ecef;
display: flex;
flex-direction: column;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
}
.logo {
padding: 24px 20px;
border-bottom: 1px solid #e9ecef;
}
.logo-text {
font-size: 20px;
font-weight: bold;
color: #3b82f6;
}
.nav-menu {
flex: 1;
padding: 20px 0;
}
.nav-item {
display: flex;
align-items: center;
padding: 12px 20px;
margin: 4px 16px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
color: #6b7280;
font-size: 14px;
font-weight: 500;
}
.nav-item:hover {
background: #f3f4f6;
color: #374151;
}
.nav-item.active {
background: #dbeafe;
color: #3b82f6;
}
.nav-item .el-icon {
margin-right: 12px;
font-size: 16px;
}
.sidebar-footer {
padding: 20px;
border-top: 1px solid #e9ecef;
background: #f8f9fa;
}
.online-users {
margin-bottom: 8px;
font-size: 13px;
color: #6b7280;
}
.online-count {
color: #3b82f6;
font-weight: 600;
}
.system-uptime {
font-size: 13px;
color: #6b7280;
}
/* 主内容区域 */
.main-content {
flex: 1;
display: flex;
flex-direction: column;
background: #f8f9fa;
}
/* 顶部搜索栏 */
.top-header {
background: #ffffff;
padding: 16px 24px;
border-bottom: 1px solid #e9ecef;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.search-bar {
position: relative;
display: flex;
align-items: center;
}
.search-icon {
position: absolute;
left: 12px;
color: #9ca3af;
font-size: 16px;
}
.search-input {
width: 300px;
padding: 10px 12px 10px 40px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 14px;
outline: none;
transition: border-color 0.2s ease;
}
.search-input:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.search-input::placeholder {
color: #9ca3af;
}
.header-right {
display: flex;
align-items: center;
gap: 16px;
}
.notification-icon {
position: relative;
padding: 8px;
cursor: pointer;
border-radius: 6px;
transition: background 0.2s ease;
}
.notification-icon:hover {
background: #f3f4f6;
}
.notification-badge {
position: absolute;
top: 4px;
right: 4px;
width: 8px;
height: 8px;
background: #ef4444;
border-radius: 50%;
}
.user-avatar {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
cursor: pointer;
border-radius: 6px;
transition: background 0.2s ease;
}
.user-avatar:hover {
background: #f3f4f6;
}
.arrow-down {
font-size: 12px;
color: #6b7280;
}
/* 统计卡片 */
.stats-cards {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
padding: 24px;
}
.stat-card {
background: #ffffff;
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
gap: 16px;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
.stat-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
}
.stat-icon.users {
background: #fef3c7;
color: #f59e0b;
}
.stat-icon.paid-users {
background: #dbeafe;
color: #3b82f6;
}
.stat-icon.revenue {
background: #fce7f3;
color: #ec4899;
}
.stat-content {
flex: 1;
}
.stat-title {
font-size: 14px;
color: #6b7280;
margin-bottom: 8px;
font-weight: 500;
}
.stat-number {
font-size: 28px;
font-weight: bold;
color: #111827;
margin-bottom: 4px;
}
.stat-change {
font-size: 13px;
font-weight: 500;
}
.stat-change.positive {
color: #059669;
}
.stat-change.negative {
color: #dc2626;
}
/* 图表区域 */
.charts-section {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
padding: 0 24px 24px;
}
.chart-card {
background: #ffffff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.chart-header {
padding: 20px 24px 16px;
border-bottom: 1px solid #e9ecef;
display: flex;
align-items: center;
justify-content: space-between;
}
.chart-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #111827;
}
.year-select {
width: 100px;
}
.chart-content {
padding: 24px;
height: 300px;
}
.chart-placeholder {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #f8f9fa;
border-radius: 8px;
border: 2px dashed #d1d5db;
}
.chart-title {
font-size: 16px;
font-weight: 600;
color: #374151;
margin-bottom: 8px;
}
.chart-description {
font-size: 14px;
color: #6b7280;
text-align: center;
}
/* 响应式设计 */
@media (max-width: 1200px) {
.charts-section {
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) {
.admin-dashboard {
flex-direction: column;
}
.sidebar {
width: 100%;
height: auto;
}
.stats-cards {
grid-template-columns: 1fr;
padding: 16px;
}
.charts-section {
padding: 0 16px 16px;
}
.search-input {
width: 200px;
}
.top-header {
padding: 12px 16px;
}
}
@media (max-width: 480px) {
.stat-card {
padding: 16px;
}
.stat-number {
font-size: 24px;
}
.chart-content {
padding: 16px;
height: 250px;
}
}
</style>

View File

@@ -0,0 +1,784 @@
<template>
<div class="admin-orders">
<!-- 页面标题 -->
<div class="page-header">
<h2>
<el-icon><Management /></el-icon>
订单管理 - 管理员
</h2>
</div>
<!-- 统计面板 -->
<el-row :gutter="20" class="stats-row">
<el-col :xs="12" :sm="6">
<el-card class="stat-card clickable" @click="handleStatClick('all')">
<div class="stat-content">
<div class="stat-number">{{ stats.totalOrders || 0 }}</div>
<div class="stat-label">总订单数</div>
</div>
<el-icon class="stat-icon" color="#409EFF"><List /></el-icon>
</el-card>
</el-col>
<el-col :xs="12" :sm="6">
<el-card class="stat-card clickable" @click="handleStatClick('PENDING')">
<div class="stat-content">
<div class="stat-number">{{ stats.pendingOrders || 0 }}</div>
<div class="stat-label">待支付</div>
</div>
<el-icon class="stat-icon" color="#E6A23C"><Clock /></el-icon>
</el-card>
</el-col>
<el-col :xs="12" :sm="6">
<el-card class="stat-card clickable" @click="handleStatClick('COMPLETED')">
<div class="stat-content">
<div class="stat-number">{{ stats.completedOrders || 0 }}</div>
<div class="stat-label">已完成</div>
</div>
<el-icon class="stat-icon" color="#67C23A"><Check /></el-icon>
</el-card>
</el-col>
<el-col :xs="12" :sm="6">
<el-card class="stat-card clickable" @click="handleStatClick('today')">
<div class="stat-content">
<div class="stat-number">{{ stats.todayOrders || 0 }}</div>
<div class="stat-label">今日订单</div>
</div>
<el-icon class="stat-icon" color="#F56C6C"><Calendar /></el-icon>
</el-card>
</el-col>
</el-row>
<!-- 第二行统计卡片 -->
<el-row :gutter="20" class="stats-row" style="margin-top: 20px;">
<el-col :xs="12" :sm="6">
<el-card class="stat-card clickable" @click="handleStatClick('PAID')">
<div class="stat-content">
<div class="stat-number">{{ stats.paidOrders || 0 }}</div>
<div class="stat-label">已支付</div>
</div>
<el-icon class="stat-icon" color="#409EFF"><CreditCard /></el-icon>
</el-card>
</el-col>
<el-col :xs="12" :sm="6">
<el-card class="stat-card clickable" @click="handleStatClick('PROCESSING')">
<div class="stat-content">
<div class="stat-number">{{ stats.processingOrders || 0 }}</div>
<div class="stat-label">处理中</div>
</div>
<el-icon class="stat-icon" color="#909399"><Loading /></el-icon>
</el-card>
</el-col>
<!-- 只有存在实体商品时才显示发货统计 -->
<el-col v-if="hasPhysicalOrders" :xs="12" :sm="6">
<el-card class="stat-card clickable" @click="handleStatClick('SHIPPED')">
<div class="stat-content">
<div class="stat-number">{{ stats.shippedOrders || 0 }}</div>
<div class="stat-label">已发货</div>
</div>
<el-icon class="stat-icon" color="#67C23A"><Truck /></el-icon>
</el-card>
</el-col>
<!-- 如果没有实体商品显示已退款统计 -->
<el-col v-else :xs="12" :sm="6">
<el-card class="stat-card clickable" @click="handleStatClick('REFUNDED')">
<div class="stat-content">
<div class="stat-number">{{ stats.refundedOrders || 0 }}</div>
<div class="stat-label">已退款</div>
</div>
<el-icon class="stat-icon" color="#909399"><Money /></el-icon>
</el-card>
</el-col>
<el-col :xs="12" :sm="6">
<el-card class="stat-card clickable" @click="handleStatClick('CANCELLED')">
<div class="stat-content">
<div class="stat-number">{{ stats.cancelledOrders || 0 }}</div>
<div class="stat-label">已取消</div>
</div>
<el-icon class="stat-icon" color="#F56C6C"><Close /></el-icon>
</el-card>
</el-col>
</el-row>
<!-- 筛选和搜索 -->
<el-card class="filter-card">
<el-row :gutter="20">
<el-col :xs="24" :sm="12" :md="8">
<el-select
v-model="filters.status"
placeholder="选择订单状态"
clearable
@change="handleFilterChange"
>
<el-option label="全部状态" value="" />
<el-option label="待支付" value="PENDING" />
<el-option label="已确认" value="CONFIRMED" />
<el-option label="已支付" value="PAID" />
<el-option label="处理中" value="PROCESSING" />
<el-option label="已发货" value="SHIPPED" />
<el-option label="已送达" value="DELIVERED" />
<el-option label="已完成" value="COMPLETED" />
<el-option label="已取消" value="CANCELLED" />
<el-option label="已退款" value="REFUNDED" />
</el-select>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-input
v-model="filters.search"
placeholder="搜索订单号或用户名"
clearable
@input="handleSearch"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-button @click="resetFilters">重置筛选</el-button>
</el-col>
</el-row>
</el-card>
<!-- 订单列表 -->
<el-card class="orders-card">
<el-table
:data="orders"
v-loading="loading"
empty-text="暂无订单"
@sort-change="handleSortChange"
>
<el-table-column prop="orderNumber" label="订单号" width="150" sortable="custom">
<template #default="{ row }">
<router-link :to="`/orders/${row.id}`" class="order-link">
{{ row.orderNumber }}
</router-link>
</template>
</el-table-column>
<el-table-column prop="user.username" label="用户" width="120">
<template #default="{ row }">
<div class="user-info">
<el-avatar :size="24">{{ row.user.username.charAt(0).toUpperCase() }}</el-avatar>
<span class="username">{{ row.user.username }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="totalAmount" label="金额" width="120" sortable="custom">
<template #default="{ row }">
<span class="amount">{{ row.currency }} {{ row.totalAmount }}</span>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="120">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">
{{ getStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="orderType" label="类型" width="120">
<template #default="{ row }">
{{ getOrderTypeText(row.orderType) }}
</template>
</el-table-column>
<el-table-column prop="createdAt" label="创建时间" width="160" sortable="custom">
<template #default="{ row }">
{{ formatDate(row.createdAt) }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button-group>
<el-button size="small" @click="$router.push(`/orders/${row.id}`)">
查看
</el-button>
<el-dropdown trigger="click" :teleported="true" popper-class="table-dropdown" @command="(command) => handleAdminAction(row, command)">
<el-button size="small" type="primary">
管理<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-if="canShip(row)" command="ship">
<el-icon><Truck /></el-icon>
发货
</el-dropdown-item>
<el-dropdown-item v-if="canComplete(row)" command="complete">
<el-icon><Check /></el-icon>
完成订单
</el-dropdown-item>
<el-dropdown-item v-if="canCancel(row)" command="cancel">
<el-icon><Close /></el-icon>
取消订单
</el-dropdown-item>
<el-dropdown-item divided command="updateStatus">
<el-icon><Edit /></el-icon>
更新状态
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-button-group>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.size"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
<!-- 发货对话框 -->
<el-dialog
v-model="shipDialogVisible"
title="订单发货"
width="400px"
>
<el-form :model="shipForm" label-width="80px">
<el-form-item label="物流单号">
<el-input
v-model="shipForm.trackingNumber"
placeholder="请输入物流单号(可选)"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="shipDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmShip">确认发货</el-button>
</template>
</el-dialog>
<!-- 更新状态对话框 -->
<el-dialog
v-model="statusDialogVisible"
title="更新订单状态"
width="400px"
>
<el-form :model="statusForm" label-width="80px">
<el-form-item label="新状态">
<el-select v-model="statusForm.status" placeholder="选择新状态">
<el-option label="待支付" value="PENDING" />
<el-option label="已确认" value="CONFIRMED" />
<el-option label="已支付" value="PAID" />
<el-option label="处理中" value="PROCESSING" />
<!-- 实体商品才显示发货相关状态 -->
<template v-if="isPhysicalOrder(currentStatusOrder)">
<el-option label="已发货" value="SHIPPED" />
<el-option label="已送达" value="DELIVERED" />
</template>
<el-option label="已完成" value="COMPLETED" />
<el-option label="已取消" value="CANCELLED" />
<el-option label="已退款" value="REFUNDED" />
</el-select>
</el-form-item>
<el-form-item label="备注">
<el-input
v-model="statusForm.notes"
type="textarea"
:rows="3"
placeholder="请输入备注(可选)"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="statusDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmUpdateStatus">确认更新</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getOrders, getOrderStats } from '@/api/orders'
const loading = ref(false)
const orders = ref([])
// 统计数据
const stats = ref({
totalOrders: 0,
pendingOrders: 0,
completedOrders: 0,
todayOrders: 0
})
// 筛选条件
const filters = reactive({
status: '',
search: ''
})
// 分页信息
const pagination = reactive({
page: 1,
size: 10,
total: 0
})
// 排序
const sortBy = ref('createdAt')
const sortDir = ref('desc')
// 发货对话框
const shipDialogVisible = ref(false)
const shipForm = reactive({
trackingNumber: ''
})
const currentShipOrder = ref(null)
// 状态更新对话框
const statusDialogVisible = ref(false)
const statusForm = reactive({
status: '',
notes: ''
})
const currentStatusOrder = ref(null)
// 获取状态类型
const getStatusType = (status) => {
const statusMap = {
'PENDING': 'warning',
'CONFIRMED': 'info',
'PAID': 'primary',
'PROCESSING': '',
'SHIPPED': 'success',
'DELIVERED': 'success',
'COMPLETED': 'success',
'CANCELLED': 'danger',
'REFUNDED': 'info'
}
return statusMap[status] || ''
}
// 获取状态文本
const getStatusText = (status) => {
const statusMap = {
'PENDING': '待支付',
'CONFIRMED': '已确认',
'PAID': '已支付',
'PROCESSING': '处理中',
'SHIPPED': '已发货',
'DELIVERED': '已送达',
'COMPLETED': '已完成',
'CANCELLED': '已取消',
'REFUNDED': '已退款'
}
return statusMap[status] || status
}
// 获取订单类型文本
const getOrderTypeText = (orderType) => {
const typeMap = {
'PRODUCT': '商品订单',
'SERVICE': '服务订单',
'SUBSCRIPTION': '订阅订单',
'DIGITAL': '数字商品',
'PHYSICAL': '实体商品'
}
return typeMap[orderType] || orderType
}
// 格式化日期
const formatDate = (dateString) => {
const date = new Date(dateString)
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
}
// 检查是否可以发货(仅限实体商品)
const canShip = (order) => {
// 只有实体商品才需要发货
const physicalOrderTypes = ['PRODUCT', 'PHYSICAL']
return physicalOrderTypes.includes(order.orderType) &&
(order.status === 'PAID' || order.status === 'CONFIRMED')
}
// 检查是否可以完成
const canComplete = (order) => {
// 实体商品需要先发货才能完成
if (['PRODUCT', 'PHYSICAL'].includes(order.orderType)) {
return order.status === 'SHIPPED'
}
// 虚拟商品支付后可以直接完成
return ['PAID', 'CONFIRMED'].includes(order.status)
}
// 检查是否可以取消
const canCancel = (order) => {
return order.status === 'PENDING' || order.status === 'CONFIRMED'
}
// 检查是否为实体商品
const isPhysicalOrder = (order) => {
if (!order) return false
const physicalOrderTypes = ['PRODUCT', 'PHYSICAL']
return physicalOrderTypes.includes(order.orderType)
}
// 检查是否有实体商品订单
const hasPhysicalOrders = computed(() => {
return orders.value.some(order => isPhysicalOrder(order))
})
// 处理统计卡片点击
const handleStatClick = (type) => {
if (type === 'all') {
// 显示所有订单
filters.status = ''
filters.search = ''
} else if (type === 'today') {
// 显示今日订单(这里可以添加日期筛选逻辑)
filters.status = ''
filters.search = ''
// 可以添加日期筛选逻辑
} else {
// 按状态筛选
filters.status = type
filters.search = ''
}
// 重置分页并重新加载数据
pagination.page = 1
fetchOrders()
}
// 获取订单列表
const fetchOrders = async () => {
try {
loading.value = true
// 调用真实API获取订单数据
const response = await getOrders({
page: pagination.page - 1,
size: pagination.size,
status: filters.status,
search: filters.search
})
if (response.success) {
orders.value = response.data.content || []
pagination.total = response.data.totalElements || 0
} else {
ElMessage.error('获取订单列表失败')
}
// 获取统计数据
const statsResponse = await getOrderStats()
if (statsResponse.success) {
stats.value = statsResponse.data
}
} catch (error) {
console.error('Fetch orders error:', error)
ElMessage.error('获取订单列表失败')
} finally {
loading.value = false
}
}
// 筛选变化
const handleFilterChange = () => {
pagination.page = 1
fetchOrders()
}
// 搜索
const handleSearch = () => {
pagination.page = 1
fetchOrders()
}
// 重置筛选
const resetFilters = () => {
filters.status = ''
filters.search = ''
pagination.page = 1
fetchOrders()
}
// 排序变化
const handleSortChange = ({ prop, order }) => {
if (prop) {
sortBy.value = prop
sortDir.value = order === 'ascending' ? 'asc' : 'desc'
fetchOrders()
}
}
// 分页大小变化
const handleSizeChange = (size) => {
pagination.size = size
pagination.page = 1
fetchOrders()
}
// 当前页变化
const handleCurrentChange = (page) => {
pagination.page = page
fetchOrders()
}
// 处理管理员操作
const handleAdminAction = (order, command) => {
switch (command) {
case 'ship':
currentShipOrder.value = order
shipForm.trackingNumber = ''
shipDialogVisible.value = true
break
case 'complete':
handleCompleteOrder(order)
break
case 'cancel':
handleCancelOrder(order)
break
case 'updateStatus':
currentStatusOrder.value = order
statusForm.status = order.status
statusForm.notes = ''
statusDialogVisible.value = true
break
}
}
// 完成订单
const handleCompleteOrder = async (order) => {
try {
await ElMessageBox.confirm('确定要完成此订单吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
ElMessage.success('订单完成成功')
fetchOrders()
} catch (error) {
// 用户取消
}
}
// 更新统计数据
const updateStats = () => {
const today = new Date().toISOString().split('T')[0]
stats.value = {
totalOrders: orders.value.length,
pendingOrders: orders.value.filter(order => order.status === 'PENDING').length,
paidOrders: orders.value.filter(order => order.status === 'PAID').length,
processingOrders: orders.value.filter(order => order.status === 'PROCESSING').length,
shippedOrders: orders.value.filter(order => order.status === 'SHIPPED').length,
completedOrders: orders.value.filter(order => order.status === 'COMPLETED').length,
cancelledOrders: orders.value.filter(order => order.status === 'CANCELLED').length,
refundedOrders: orders.value.filter(order => order.status === 'REFUNDED').length,
todayOrders: orders.value.filter(order => order.createdAt.startsWith(today)).length
}
}
// 取消订单
const handleCancelOrder = async (order) => {
try {
await ElMessageBox.confirm('确定要取消此订单吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
// 更新订单状态为已取消
const orderIndex = orders.value.findIndex(o => o.id === order.id)
if (orderIndex !== -1) {
orders.value[orderIndex].status = 'CANCELLED'
}
// 更新统计数据
updateStats()
ElMessage.success('订单取消成功')
} catch (error) {
// 用户取消
}
}
// 确认发货
const confirmShip = async () => {
if (!currentShipOrder.value) return
try {
ElMessage.success('发货成功')
shipDialogVisible.value = false
fetchOrders()
} catch (error) {
ElMessage.error('发货失败')
}
}
// 确认更新状态
const confirmUpdateStatus = async () => {
if (!currentStatusOrder.value) return
try {
// 更新订单状态
const orderIndex = orders.value.findIndex(o => o.id === currentStatusOrder.value.id)
if (orderIndex !== -1) {
orders.value[orderIndex].status = statusForm.status
}
// 更新统计数据
updateStats()
ElMessage.success('状态更新成功')
statusDialogVisible.value = false
} catch (error) {
ElMessage.error('状态更新失败')
}
}
onMounted(() => {
fetchOrders()
})
</script>
<style scoped>
.admin-orders {
max-width: 1400px;
margin: 0 auto;
}
.page-header {
margin-bottom: 20px;
}
.page-header h2 {
margin: 0;
color: #303133;
display: flex;
align-items: center;
gap: 8px;
}
.stats-row {
margin-bottom: 20px;
}
.stat-card {
display: flex;
align-items: center;
justify-content: space-between;
transition: all 0.3s ease;
}
.stat-card.clickable {
cursor: pointer;
}
.stat-card.clickable:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.stat-content {
flex: 1;
}
.stat-number {
font-size: 2rem;
font-weight: bold;
color: #303133;
margin-bottom: 4px;
}
.stat-label {
color: #909399;
font-size: 14px;
}
.stat-icon {
font-size: 2rem;
opacity: 0.8;
}
.filter-card {
margin-bottom: 20px;
}
.orders-card {
margin-bottom: 20px;
}
.order-link {
color: #409EFF;
text-decoration: none;
font-weight: 500;
}
.order-link:hover {
text-decoration: underline;
}
.user-info {
display: flex;
align-items: center;
gap: 8px;
}
.username {
font-size: 14px;
}
.amount {
font-weight: 600;
color: #E6A23C;
}
.pagination-container {
display: flex;
justify-content: center;
margin-top: 20px;
}
/* 确保表格内下拉菜单不被裁剪/遮挡 */
:deep(.table-dropdown) {
z-index: 3000 !important;
}
@media (max-width: 768px) {
.page-header {
text-align: center;
}
.stat-card {
margin-bottom: 16px;
}
}
</style>

View File

@@ -0,0 +1,745 @@
<template>
<div class="admin-users">
<!-- 页面标题 -->
<div class="page-header">
<h2>
<el-icon><User /></el-icon>
用户管理 - 管理员
</h2>
</div>
<!-- 统计面板 -->
<el-row :gutter="20" class="stats-row">
<el-col :xs="12" :sm="6">
<el-card class="stat-card clickable" @click="handleStatClick('all')">
<div class="stat-content">
<div class="stat-number">{{ stats.totalUsers || 0 }}</div>
<div class="stat-label">总用户数</div>
</div>
<el-icon class="stat-icon" color="#409EFF"><User /></el-icon>
</el-card>
</el-col>
<el-col :xs="12" :sm="6">
<el-card class="stat-card clickable" @click="handleStatClick('admin')">
<div class="stat-content">
<div class="stat-number">{{ stats.adminUsers || 0 }}</div>
<div class="stat-label">管理员</div>
</div>
<el-icon class="stat-icon" color="#67C23A"><UserFilled /></el-icon>
</el-card>
</el-col>
<el-col :xs="12" :sm="6">
<el-card class="stat-card clickable" @click="handleStatClick('user')">
<div class="stat-content">
<div class="stat-number">{{ stats.normalUsers || 0 }}</div>
<div class="stat-label">普通用户</div>
</div>
<el-icon class="stat-icon" color="#E6A23C"><Avatar /></el-icon>
</el-card>
</el-col>
<el-col :xs="12" :sm="6">
<el-card class="stat-card clickable" @click="handleStatClick('today')">
<div class="stat-content">
<div class="stat-number">{{ stats.todayUsers || 0 }}</div>
<div class="stat-label">今日注册</div>
</div>
<el-icon class="stat-icon" color="#F56C6C"><Calendar /></el-icon>
</el-card>
</el-col>
</el-row>
<!-- 筛选和搜索 -->
<el-card class="filter-card">
<el-row :gutter="20">
<el-col :xs="24" :sm="12" :md="8">
<el-select
v-model="filters.role"
placeholder="选择用户角色"
clearable
@change="handleFilterChange"
>
<el-option label="全部角色" value="" />
<el-option label="管理员" value="ROLE_ADMIN" />
<el-option label="普通用户" value="ROLE_USER" />
</el-select>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-input
v-model="filters.search"
placeholder="搜索用户名或邮箱"
clearable
@input="handleSearch"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-button @click="resetFilters">重置筛选</el-button>
</el-col>
</el-row>
</el-card>
<!-- 用户列表 -->
<el-card class="users-card">
<template #header>
<div class="card-header">
<span>用户列表</span>
<el-button type="primary" @click="showCreateUserDialog">
<el-icon><Plus /></el-icon>
添加用户
</el-button>
</div>
</template>
<el-table
:data="users"
v-loading="loading"
empty-text="暂无用户"
@sort-change="handleSortChange"
>
<el-table-column prop="id" label="ID" width="80" sortable="custom" />
<el-table-column prop="username" label="用户名" width="150" sortable="custom">
<template #default="{ row }">
<div class="user-info">
<el-avatar :size="32">{{ row.username.charAt(0).toUpperCase() }}</el-avatar>
<div class="user-details">
<div class="username">{{ row.username }}</div>
<div class="user-id">ID: {{ row.id }}</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column prop="email" label="邮箱" min-width="200" sortable="custom">
<template #default="{ row }">
<span class="email">{{ row.email }}</span>
</template>
</el-table-column>
<el-table-column prop="role" label="角色" width="120">
<template #default="{ row }">
<el-tag :type="getRoleType(row.role)">
{{ getRoleText(row.role) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createdAt" label="注册时间" width="160" sortable="custom">
<template #default="{ row }">
{{ formatDate(row.createdAt) }}
</template>
</el-table-column>
<el-table-column prop="lastLoginAt" label="最后登录" width="160">
<template #default="{ row }">
{{ row.lastLoginAt ? formatDate(row.lastLoginAt) : '从未登录' }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button-group>
<el-button size="small" @click="viewUserDetail(row)">
查看
</el-button>
<el-button size="small" type="primary" @click="editUser(row)">
编辑
</el-button>
<el-button
size="small"
type="danger"
@click="deleteUser(row)"
:disabled="row.role === 'ROLE_ADMIN'"
>
删除
</el-button>
</el-button-group>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.size"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
<!-- 创建/编辑用户对话框 -->
<el-dialog
v-model="userDialogVisible"
:title="isEdit ? '编辑用户' : '添加用户'"
width="600px"
>
<el-form
ref="userFormRef"
:model="userForm"
:rules="userRules"
label-width="100px"
>
<el-form-item label="用户名" prop="username">
<el-input
v-model="userForm.username"
placeholder="请输入用户名"
:disabled="isEdit"
/>
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input
v-model="userForm.email"
type="email"
placeholder="请输入邮箱"
/>
</el-form-item>
<el-form-item label="密码" prop="password" v-if="!isEdit">
<el-input
v-model="userForm.password"
type="password"
placeholder="请输入密码"
show-password
/>
</el-form-item>
<el-form-item label="角色" prop="role">
<el-radio-group v-model="userForm.role">
<el-radio value="ROLE_USER">普通用户</el-radio>
<el-radio value="ROLE_ADMIN">管理员</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="userDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmitUser" :loading="submitLoading">
{{ isEdit ? '更新' : '创建' }}
</el-button>
</template>
</el-dialog>
<!-- 用户详情对话框 -->
<el-dialog
v-model="detailDialogVisible"
title="用户详情"
width="600px"
>
<div v-if="currentUser">
<el-descriptions :column="2" border>
<el-descriptions-item label="用户ID">{{ currentUser.id }}</el-descriptions-item>
<el-descriptions-item label="用户名">{{ currentUser.username }}</el-descriptions-item>
<el-descriptions-item label="邮箱">{{ currentUser.email }}</el-descriptions-item>
<el-descriptions-item label="角色">
<el-tag :type="getRoleType(currentUser.role)">
{{ getRoleText(currentUser.role) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="注册时间">{{ formatDate(currentUser.createdAt) }}</el-descriptions-item>
<el-descriptions-item label="最后登录" v-if="currentUser.lastLoginAt">
{{ formatDate(currentUser.lastLoginAt) }}
</el-descriptions-item>
</el-descriptions>
</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
const loading = ref(false)
const users = ref([])
const submitLoading = ref(false)
// 统计数据
const stats = ref({
totalUsers: 0,
adminUsers: 0,
normalUsers: 0,
todayUsers: 0
})
// 筛选条件
const filters = reactive({
role: '',
search: '',
todayOnly: false
})
// 分页信息
const pagination = reactive({
page: 1,
size: 10,
total: 0
})
// 排序
const sortBy = ref('createdAt')
const sortDir = ref('desc')
// 用户对话框
const userDialogVisible = ref(false)
const isEdit = ref(false)
const userFormRef = ref()
const userForm = reactive({
username: '',
email: '',
password: '',
role: 'ROLE_USER'
})
const userRules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 20, message: '用户名长度在 3 到 20 个字符', trigger: 'blur' }
],
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 20, message: '密码长度在 6 到 20 个字符', trigger: 'blur' }
],
role: [
{ required: true, message: '请选择角色', trigger: 'change' }
]
}
// 用户详情对话框
const detailDialogVisible = ref(false)
const currentUser = ref(null)
// 获取角色类型
const getRoleType = (role) => {
return role === 'ROLE_ADMIN' ? 'danger' : 'primary'
}
// 获取角色文本
const getRoleText = (role) => {
return role === 'ROLE_ADMIN' ? '管理员' : '普通用户'
}
// 格式化日期
const formatDate = (dateString) => {
const date = new Date(dateString)
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
}
// 获取用户列表
const fetchUsers = async () => {
try {
loading.value = true
// 模拟数据
const today = new Date().toISOString().split('T')[0]
const mockUsers = [
{
id: 1,
username: 'admin',
email: 'admin@example.com',
role: 'ROLE_ADMIN',
createdAt: '2024-01-01T10:00:00Z',
lastLoginAt: '2024-01-01T15:00:00Z'
},
{
id: 2,
username: 'user1',
email: 'user1@example.com',
role: 'ROLE_USER',
createdAt: '2024-01-01T11:00:00Z',
lastLoginAt: '2024-01-01T14:00:00Z'
},
{
id: 3,
username: 'user2',
email: 'user2@example.com',
role: 'ROLE_USER',
createdAt: '2024-01-01T12:00:00Z',
lastLoginAt: null
},
{
id: 4,
username: 'admin2',
email: 'admin2@example.com',
role: 'ROLE_ADMIN',
createdAt: '2024-01-01T13:00:00Z',
lastLoginAt: '2024-01-01T16:00:00Z'
},
{
id: 5,
username: 'user3',
email: 'user3@example.com',
role: 'ROLE_USER',
createdAt: '2024-01-01T14:00:00Z',
lastLoginAt: null
},
{
id: 6,
username: 'newuser1',
email: 'newuser1@example.com',
role: 'ROLE_USER',
createdAt: `${today}T10:00:00Z`,
lastLoginAt: null
},
{
id: 7,
username: 'newuser2',
email: 'newuser2@example.com',
role: 'ROLE_USER',
createdAt: `${today}T11:00:00Z`,
lastLoginAt: null
},
{
id: 8,
username: 'newadmin',
email: 'newadmin@example.com',
role: 'ROLE_ADMIN',
createdAt: `${today}T12:00:00Z`,
lastLoginAt: null
}
]
// 根据筛选条件过滤用户
let filteredUsers = mockUsers
// 按角色筛选
if (filters.role) {
filteredUsers = filteredUsers.filter(user => user.role === filters.role)
}
// 按搜索关键词筛选
if (filters.search) {
const searchLower = filters.search.toLowerCase()
filteredUsers = filteredUsers.filter(user =>
user.username.toLowerCase().includes(searchLower) ||
user.email.toLowerCase().includes(searchLower)
)
}
// 按今日注册筛选(模拟)
if (filters.todayOnly) {
const today = new Date().toISOString().split('T')[0]
filteredUsers = filteredUsers.filter(user =>
user.createdAt.startsWith(today)
)
}
users.value = filteredUsers
pagination.total = filteredUsers.length
// 更新统计数据
stats.value = {
totalUsers: mockUsers.length,
adminUsers: mockUsers.filter(user => user.role === 'ROLE_ADMIN').length,
normalUsers: mockUsers.filter(user => user.role === 'ROLE_USER').length,
todayUsers: mockUsers.filter(user => {
const today = new Date().toISOString().split('T')[0]
return user.createdAt.startsWith(today)
}).length
}
} catch (error) {
console.error('Fetch users error:', error)
ElMessage.error('获取用户列表失败')
} finally {
loading.value = false
}
}
// 筛选变化
const handleFilterChange = () => {
pagination.page = 1
fetchUsers()
}
// 搜索
const handleSearch = () => {
pagination.page = 1
fetchUsers()
}
// 重置筛选
const resetFilters = () => {
filters.role = ''
filters.search = ''
pagination.page = 1
fetchUsers()
}
// 排序变化
const handleSortChange = ({ prop, order }) => {
if (prop) {
sortBy.value = prop
sortDir.value = order === 'ascending' ? 'asc' : 'desc'
fetchUsers()
}
}
// 分页大小变化
const handleSizeChange = (size) => {
pagination.size = size
pagination.page = 1
fetchUsers()
}
// 当前页变化
const handleCurrentChange = (page) => {
pagination.page = page
fetchUsers()
}
// 显示创建用户对话框
const showCreateUserDialog = () => {
isEdit.value = false
resetUserForm()
userDialogVisible.value = true
}
// 编辑用户
const editUser = (user) => {
isEdit.value = true
userForm.username = user.username
userForm.email = user.email
userForm.role = user.role
userForm.password = ''
userDialogVisible.value = true
}
// 重置用户表单
const resetUserForm = () => {
userForm.username = ''
userForm.email = ''
userForm.password = ''
userForm.role = 'ROLE_USER'
}
// 提交用户表单
const handleSubmitUser = async () => {
if (!userFormRef.value) return
try {
const valid = await userFormRef.value.validate()
if (!valid) return
submitLoading.value = true
// 模拟提交
await new Promise(resolve => setTimeout(resolve, 1000))
ElMessage.success(isEdit.value ? '用户更新成功' : '用户创建成功')
userDialogVisible.value = false
fetchUsers()
} catch (error) {
console.error('Submit user error:', error)
ElMessage.error(isEdit.value ? '用户更新失败' : '用户创建失败')
} finally {
submitLoading.value = false
}
}
// 查看用户详情
const viewUserDetail = (user) => {
currentUser.value = user
detailDialogVisible.value = true
}
// 删除用户
const deleteUser = async (user) => {
try {
await ElMessageBox.confirm(`确定要删除用户 "${user.username}" 吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
ElMessage.success('用户删除成功')
fetchUsers()
} catch (error) {
// 用户取消
}
}
// 处理统计卡片点击事件
const handleStatClick = (type) => {
switch (type) {
case 'all':
// 显示所有用户
filters.role = ''
filters.search = ''
filters.todayOnly = false
ElMessage.info('显示所有用户')
break
case 'admin':
// 筛选管理员用户
filters.role = 'ROLE_ADMIN'
filters.search = ''
filters.todayOnly = false
ElMessage.info('筛选管理员用户')
break
case 'user':
// 筛选普通用户
filters.role = 'ROLE_USER'
filters.search = ''
filters.todayOnly = false
ElMessage.info('筛选普通用户')
break
case 'today':
// 筛选今日注册用户
filters.role = ''
filters.search = ''
filters.todayOnly = true
ElMessage.info('筛选今日注册用户')
break
}
// 重新获取用户列表
fetchUsers()
}
onMounted(() => {
fetchUsers()
})
</script>
<style scoped>
.admin-users {
max-width: 1400px;
margin: 0 auto;
}
.page-header {
margin-bottom: 20px;
}
.page-header h2 {
margin: 0;
color: #303133;
display: flex;
align-items: center;
gap: 8px;
}
.stats-row {
margin-bottom: 20px;
}
.stat-card {
display: flex;
align-items: center;
justify-content: space-between;
transition: all 0.3s ease;
}
.stat-card.clickable {
cursor: pointer;
}
.stat-card.clickable:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.stat-content {
flex: 1;
}
.stat-number {
font-size: 2rem;
font-weight: bold;
color: #303133;
margin-bottom: 4px;
}
.stat-label {
color: #909399;
font-size: 14px;
}
.stat-icon {
font-size: 2rem;
opacity: 0.8;
}
.filter-card {
margin-bottom: 20px;
}
.users-card {
margin-bottom: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.user-info {
display: flex;
align-items: center;
gap: 12px;
}
.user-details {
display: flex;
flex-direction: column;
}
.username {
font-weight: 500;
color: #303133;
}
.user-id {
font-size: 12px;
color: #909399;
}
.email {
color: #606266;
}
.pagination-container {
display: flex;
justify-content: center;
margin-top: 20px;
}
@media (max-width: 768px) {
.page-header {
text-align: center;
}
.stat-card {
margin-bottom: 16px;
}
.card-header {
flex-direction: column;
gap: 16px;
}
}
</style>

View File

@@ -0,0 +1,621 @@
<template>
<div class="dashboard">
<div class="dashboard-header">
<h1>数据仪表盘</h1>
<p class="dashboard-subtitle">系统数据概览与关键指标监控</p>
</div>
<!-- 概览卡片 -->
<div class="overview-cards">
<div class="card">
<div class="card-icon users">
<i class="fas fa-users"></i>
</div>
<div class="card-content">
<h3>{{ overviewData.totalUsers || 0 }}</h3>
<p>用户总数</p>
</div>
</div>
<div class="card">
<div class="card-icon paying">
<i class="fas fa-credit-card"></i>
</div>
<div class="card-content">
<h3>{{ overviewData.payingUsers || 0 }}</h3>
<p>付费用户数</p>
</div>
</div>
<div class="card">
<div class="card-icon revenue">
<i class="fas fa-dollar-sign"></i>
</div>
<div class="card-content">
<h3>¥{{ formatNumber(overviewData.todayRevenue || 0) }}</h3>
<p>今日收入</p>
</div>
</div>
<div class="card">
<div class="card-icon conversion">
<i class="fas fa-chart-line"></i>
</div>
<div class="card-content">
<h3>{{ overviewData.conversionRate || 0 }}%</h3>
<p>转化率</p>
</div>
</div>
</div>
<!-- 图表区域 -->
<div class="charts-section">
<div class="chart-container">
<h3>日活用户趋势</h3>
<div class="chart" ref="dailyActiveChart"></div>
</div>
<div class="chart-container">
<h3>收入趋势</h3>
<div class="chart" ref="revenueChart"></div>
</div>
</div>
<!-- 分布图表 -->
<div class="distribution-section">
<div class="chart-container">
<h3>订单状态分布</h3>
<div class="chart" ref="orderStatusChart"></div>
</div>
<div class="chart-container">
<h3>支付方式分布</h3>
<div class="chart" ref="paymentMethodChart"></div>
</div>
</div>
<!-- 最近订单 -->
<div class="recent-orders">
<h3>最近订单</h3>
<div class="table-container">
<table>
<thead>
<tr>
<th>订单号</th>
<th>用户</th>
<th>金额</th>
<th>状态</th>
<th>创建时间</th>
</tr>
</thead>
<tbody>
<tr v-for="order in recentOrders" :key="order.id">
<td>{{ order.orderNumber }}</td>
<td>{{ order.username }}</td>
<td>¥{{ formatNumber(order.totalAmount) }}</td>
<td>
<span class="status-badge" :class="getStatusClass(order.status)">
{{ getStatusText(order.status) }}
</span>
</td>
<td>{{ formatDate(order.createdAt) }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script>
import { ref, onMounted, nextTick } from 'vue'
import { dashboardApi } from '@/api/dashboard'
// 动态加载ECharts
const loadECharts = () => {
return new Promise((resolve, reject) => {
if (window.echarts) {
resolve(window.echarts)
return
}
const script = document.createElement('script')
script.src = 'https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js'
script.onload = () => resolve(window.echarts)
script.onerror = reject
document.head.appendChild(script)
})
}
export default {
name: 'Dashboard',
setup() {
const overviewData = ref({})
const dailyActiveData = ref([])
const revenueData = ref([])
const orderStatusData = ref([])
const paymentMethodData = ref([])
const recentOrders = ref([])
const dailyActiveChart = ref(null)
const revenueChart = ref(null)
const orderStatusChart = ref(null)
const paymentMethodChart = ref(null)
// 加载仪表盘数据
const loadDashboardData = async () => {
try {
const response = await dashboardApi.getAllData()
const data = response.data
overviewData.value = data.overview
dailyActiveData.value = data.dailyActiveUsers.dailyData || []
revenueData.value = data.revenueTrend.revenueData || []
orderStatusData.value = data.orderStatusDistribution.statusData || []
paymentMethodData.value = data.paymentMethodDistribution.methodData || []
recentOrders.value = data.recentOrders.orders || []
// 等待DOM更新后初始化图表
await nextTick()
await initCharts()
} catch (error) {
console.error('加载仪表盘数据失败:', error)
}
}
// 初始化图表
const initCharts = async () => {
try {
const echarts = await loadECharts()
await initDailyActiveChart(echarts)
await initRevenueChart(echarts)
await initOrderStatusChart(echarts)
await initPaymentMethodChart(echarts)
} catch (error) {
console.error('加载ECharts失败:', error)
}
}
// 日活用户图表
const initDailyActiveChart = async (echarts) => {
if (!dailyActiveChart.value) return
const chart = echarts.init(dailyActiveChart.value)
const option = {
tooltip: {
trigger: 'axis'
},
xAxis: {
type: 'category',
data: dailyActiveData.value.map(item => item.date)
},
yAxis: {
type: 'value'
},
series: [{
data: dailyActiveData.value.map(item => item.activeUsers),
type: 'line',
smooth: true,
areaStyle: {
opacity: 0.3
},
lineStyle: {
color: '#4CAF50'
},
itemStyle: {
color: '#4CAF50'
}
}]
}
chart.setOption(option)
}
// 收入趋势图表
const initRevenueChart = async (echarts) => {
if (!revenueChart.value) return
const chart = echarts.init(revenueChart.value)
const option = {
tooltip: {
trigger: 'axis',
formatter: function(params) {
return `${params[0].axisValue}<br/>收入: ¥${params[0].value}`
}
},
xAxis: {
type: 'category',
data: revenueData.value.map(item => item.date)
},
yAxis: {
type: 'value'
},
series: [{
data: revenueData.value.map(item => item.revenue),
type: 'bar',
itemStyle: {
color: '#2196F3'
}
}]
}
chart.setOption(option)
}
// 订单状态分布图表
const initOrderStatusChart = async (echarts) => {
if (!orderStatusChart.value) return
const chart = echarts.init(orderStatusChart.value)
const option = {
tooltip: {
trigger: 'item'
},
series: [{
type: 'pie',
radius: '50%',
data: orderStatusData.value.map(item => ({
value: item.count,
name: item.status
})),
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}]
}
chart.setOption(option)
}
// 支付方式分布图表
const initPaymentMethodChart = async (echarts) => {
if (!paymentMethodChart.value) return
const chart = echarts.init(paymentMethodChart.value)
const option = {
tooltip: {
trigger: 'item'
},
series: [{
type: 'pie',
radius: '50%',
data: paymentMethodData.value.map(item => ({
value: item.count,
name: item.method
})),
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}]
}
chart.setOption(option)
}
// 格式化数字
const formatNumber = (num) => {
if (typeof num === 'string') {
num = parseFloat(num)
}
return num.toLocaleString()
}
// 格式化日期
const formatDate = (dateString) => {
const date = new Date(dateString)
return date.toLocaleString('zh-CN')
}
// 获取状态样式类
const getStatusClass = (status) => {
const statusMap = {
'PENDING': 'status-pending',
'CONFIRMED': 'status-confirmed',
'PAID': 'status-paid',
'PROCESSING': 'status-processing',
'SHIPPED': 'status-shipped',
'DELIVERED': 'status-delivered',
'COMPLETED': 'status-completed',
'CANCELLED': 'status-cancelled',
'REFUNDED': 'status-refunded'
}
return statusMap[status] || 'status-default'
}
// 获取状态文本
const getStatusText = (status) => {
const statusMap = {
'PENDING': '待支付',
'CONFIRMED': '已确认',
'PAID': '已支付',
'PROCESSING': '处理中',
'SHIPPED': '已发货',
'DELIVERED': '已送达',
'COMPLETED': '已完成',
'CANCELLED': '已取消',
'REFUNDED': '已退款'
}
return statusMap[status] || status
}
onMounted(() => {
loadDashboardData()
})
return {
overviewData,
dailyActiveData,
revenueData,
orderStatusData,
paymentMethodData,
recentOrders,
dailyActiveChart,
revenueChart,
orderStatusChart,
paymentMethodChart,
formatNumber,
formatDate,
getStatusClass,
getStatusText
}
}
}
</script>
<style scoped>
.dashboard {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
position: relative;
overflow-x: hidden;
}
/* 页面特殊效果 */
.dashboard::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background:
radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(255, 119, 198, 0.3) 0%, transparent 50%),
radial-gradient(circle at 40% 40%, rgba(120, 219, 255, 0.3) 0%, transparent 50%);
animation: dashboardGlow 8s ease-in-out infinite alternate;
pointer-events: none;
z-index: 1;
}
@keyframes dashboardGlow {
0% { opacity: 0.3; }
100% { opacity: 0.7; }
}
/* 内容层级 */
.dashboard > * {
position: relative;
z-index: 2;
}
.dashboard-header {
text-align: center;
margin-bottom: 30px;
}
.dashboard-header h1 {
color: #333;
margin-bottom: 10px;
}
.dashboard-subtitle {
color: #666;
font-size: 16px;
}
.overview-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.card {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
transition: transform 0.2s;
}
.card:hover {
transform: translateY(-2px);
}
.card-icon {
width: 60px;
height: 60px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 15px;
font-size: 24px;
color: white;
}
.card-icon.users {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.card-icon.paying {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.card-icon.revenue {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
.card-icon.conversion {
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
}
.card-content h3 {
margin: 0 0 5px 0;
font-size: 28px;
font-weight: bold;
color: #333;
}
.card-content p {
margin: 0;
color: #666;
font-size: 14px;
}
.charts-section {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.distribution-section {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.chart-container {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.chart-container h3 {
margin: 0 0 15px 0;
color: #333;
font-size: 18px;
}
.chart {
height: 300px;
width: 100%;
}
.recent-orders {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.recent-orders h3 {
margin: 0 0 20px 0;
color: #333;
font-size: 18px;
}
.table-container {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #eee;
}
th {
background-color: #f8f9fa;
font-weight: 600;
color: #333;
}
.status-badge {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.status-pending {
background-color: #fff3cd;
color: #856404;
}
.status-confirmed {
background-color: #d1ecf1;
color: #0c5460;
}
.status-paid {
background-color: #d4edda;
color: #155724;
}
.status-processing {
background-color: #cce5ff;
color: #004085;
}
.status-shipped {
background-color: #e2e3e5;
color: #383d41;
}
.status-delivered {
background-color: #d1ecf1;
color: #0c5460;
}
.status-completed {
background-color: #d4edda;
color: #155724;
}
.status-cancelled {
background-color: #f8d7da;
color: #721c24;
}
.status-refunded {
background-color: #f8d7da;
color: #721c24;
}
@media (max-width: 768px) {
.dashboard {
padding: 10px;
}
.overview-cards {
grid-template-columns: 1fr;
}
.charts-section,
.distribution-section {
grid-template-columns: 1fr;
}
.chart {
height: 250px;
}
}
</style>

View File

@@ -0,0 +1,436 @@
<template>
<div class="home">
<!-- 欢迎横幅 -->
<el-row class="welcome-banner">
<el-col :span="24">
<div class="banner-content">
<h1>欢迎使用 AIGC Demo</h1>
<p>现代化的订单管理和支付系统</p>
<div class="banner-actions">
<el-button v-if="!userStore.isAuthenticated" type="primary" size="large" @click="$router.push('/login')">
立即登录
</el-button>
<el-button v-if="!userStore.isAuthenticated" type="success" size="large" @click="$router.push('/register')">
免费注册
</el-button>
<el-button v-if="userStore.isAuthenticated" type="primary" size="large" @click="$router.push('/orders/create')">
创建订单
</el-button>
</div>
</div>
</el-col>
</el-row>
<!-- 功能特色 -->
<el-row :gutter="20" class="features">
<el-col :xs="24" :sm="12" :md="8">
<el-card class="feature-card" @click="goToOrders" style="cursor: pointer;">
<div class="feature-icon">
<el-icon size="48" color="#409EFF"><ShoppingCart /></el-icon>
</div>
<h3>订单管理</h3>
<p>完整的订单生命周期管理从创建到完成的全流程跟踪</p>
</el-card>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-card class="feature-card" @click="goToPayments" style="cursor: pointer;">
<div class="feature-icon">
<el-icon size="48" color="#67C23A"><CreditCard /></el-icon>
</div>
<h3>支付</h3>
<p>支持支付宝PayPal等多种支付方式安全便捷</p>
</el-card>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-card class="feature-card" @click="goToAdmin" style="cursor: pointer;">
<div class="feature-icon">
<el-icon size="48" color="#E6A23C"><Management /></el-icon>
</div>
<h3>管理后台</h3>
<p>强大的管理功能支持用户管理订单统计等</p>
</el-card>
</el-col>
</el-row>
<!-- 统计数据 -->
<el-row v-if="userStore.isAuthenticated" :gutter="20" class="stats">
<el-col :xs="12" :sm="6">
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-number">{{ stats.totalOrders || 0 }}</div>
<div class="stat-label">总订单数</div>
</div>
<el-icon class="stat-icon" color="#409EFF"><List /></el-icon>
</el-card>
</el-col>
<el-col :xs="12" :sm="6">
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-number">{{ stats.pendingOrders || 0 }}</div>
<div class="stat-label">待支付</div>
</div>
<el-icon class="stat-icon" color="#E6A23C"><Clock /></el-icon>
</el-card>
</el-col>
<el-col :xs="12" :sm="6">
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-number">{{ stats.completedOrders || 0 }}</div>
<div class="stat-label">已完成</div>
</div>
<el-icon class="stat-icon" color="#67C23A"><Check /></el-icon>
</el-card>
</el-col>
<el-col :xs="12" :sm="6">
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-number">{{ stats.totalAmount || 0 }}</div>
<div class="stat-label">总金额</div>
</div>
<el-icon class="stat-icon" color="#F56C6C"><Money /></el-icon>
</el-card>
</el-col>
</el-row>
<!-- 最近订单 -->
<el-row v-if="userStore.isAuthenticated" class="recent-orders">
<el-col :span="24">
<el-card>
<template #header>
<div class="card-header">
<span>最近订单</span>
<el-button type="primary" @click="$router.push('/orders')">查看全部</el-button>
</div>
</template>
<el-table :data="recentOrders" v-loading="loading" empty-text="暂无订单">
<el-table-column prop="orderNumber" label="订单号" width="150">
<template #default="{ row }">
<router-link :to="`/orders/${row.id}`" class="order-link">
{{ row.orderNumber }}
</router-link>
</template>
</el-table-column>
<el-table-column prop="totalAmount" label="金额" width="100">
<template #default="{ row }">
{{ row.currency }} {{ row.totalAmount }}
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">
{{ getStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createdAt" label="创建时间" width="150">
<template #default="{ row }">
{{ formatDate(row.createdAt) }}
</template>
</el-table-column>
<el-table-column label="操作" width="120">
<template #default="{ row }">
<el-button size="small" @click="$router.push(`/orders/${row.id}`)">
查看详情
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { useOrderStore } from '@/stores/orders'
import { getOrderStats } from '@/api/orders'
import { ElMessage } from 'element-plus'
const router = useRouter()
const userStore = useUserStore()
const orderStore = useOrderStore()
const stats = ref({})
const recentOrders = ref([])
const loading = ref(false)
// 功能卡片点击事件
const goToOrders = () => {
if (userStore.isAuthenticated) {
router.push('/orders')
} else {
ElMessage.warning('请先登录')
router.push('/login')
}
}
const goToPayments = () => {
router.push('/payments')
}
const goToAdmin = () => {
if (userStore.isAuthenticated) {
if (userStore.isAdmin) {
router.push('/admin/orders')
} else {
ElMessage.warning('需要管理员权限')
}
} else {
ElMessage.warning('请先登录')
router.push('/login')
}
}
// 获取统计数据
const fetchStats = async () => {
try {
const response = await getOrderStats()
if (response.success) {
stats.value = response.data
}
} catch (error) {
console.error('Fetch stats error:', error)
}
}
// 获取最近订单
const fetchRecentOrders = async () => {
try {
loading.value = true
const response = await orderStore.fetchOrders({ page: 0, size: 5 })
if (response.success) {
recentOrders.value = orderStore.orders
}
} catch (error) {
console.error('Fetch recent orders error:', error)
} finally {
loading.value = false
}
}
// 获取状态类型
const getStatusType = (status) => {
const statusMap = {
'PENDING': 'warning',
'CONFIRMED': 'info',
'PAID': 'primary',
'PROCESSING': '',
'SHIPPED': 'success',
'DELIVERED': 'success',
'COMPLETED': 'success',
'CANCELLED': 'danger',
'REFUNDED': 'info'
}
return statusMap[status] || ''
}
// 获取状态文本
const getStatusText = (status) => {
const statusMap = {
'PENDING': '待支付',
'CONFIRMED': '已确认',
'PAID': '已支付',
'PROCESSING': '处理中',
'SHIPPED': '已发货',
'DELIVERED': '已送达',
'COMPLETED': '已完成',
'CANCELLED': '已取消',
'REFUNDED': '已退款'
}
return statusMap[status] || status
}
// 格式化日期
const formatDate = (dateString) => {
const date = new Date(dateString)
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
}
onMounted(() => {
if (userStore.isAuthenticated) {
fetchStats()
fetchRecentOrders()
}
})
</script>
<style scoped>
.home {
max-width: 1200px;
margin: 0 auto;
min-height: 100vh;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
position: relative;
overflow-x: hidden;
padding: 20px;
}
/* 页面特殊效果 */
.home::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(45deg, transparent 30%, rgba(255,255,255,0.1) 50%, transparent 70%);
animation: shimmer 3s infinite;
pointer-events: none;
z-index: 1;
}
@keyframes shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
/* 内容层级 */
.home > * {
position: relative;
z-index: 2;
}
.welcome-banner {
margin-bottom: 40px;
}
.banner-content {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 60px 40px;
border-radius: 12px;
text-align: center;
}
.banner-content h1 {
font-size: 2.5rem;
margin-bottom: 16px;
font-weight: bold;
}
.banner-content p {
font-size: 1.2rem;
margin-bottom: 32px;
opacity: 0.9;
}
.banner-actions .el-button {
margin: 0 8px;
}
.features {
margin-bottom: 40px;
}
.feature-card {
text-align: center;
height: 200px;
display: flex;
flex-direction: column;
justify-content: center;
transition: all 0.3s ease;
}
.feature-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
}
.feature-icon {
margin-bottom: 16px;
}
.feature-card h3 {
margin-bottom: 12px;
color: #303133;
}
.feature-card p {
color: #606266;
line-height: 1.6;
}
.stats {
margin-bottom: 40px;
}
.stat-card {
display: flex;
align-items: center;
justify-content: space-between;
}
.stat-content {
flex: 1;
}
.stat-number {
font-size: 2rem;
font-weight: bold;
color: #303133;
margin-bottom: 4px;
}
.stat-label {
color: #909399;
font-size: 14px;
}
.stat-icon {
font-size: 2rem;
opacity: 0.8;
}
.recent-orders {
margin-bottom: 40px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.order-link {
color: #409EFF;
text-decoration: none;
}
.order-link:hover {
text-decoration: underline;
}
@media (max-width: 768px) {
.banner-content {
padding: 40px 20px;
}
.banner-content h1 {
font-size: 2rem;
}
.banner-content p {
font-size: 1rem;
}
.feature-card {
height: auto;
margin-bottom: 20px;
}
}
</style>

View File

@@ -0,0 +1,749 @@
<template>
<div class="image-to-video-page">
<!-- 左侧导航栏 -->
<aside class="sidebar">
<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" @click="goToSubscription">
<el-icon><Compass /></el-icon>
<span>会员订阅</span>
</div>
<div class="nav-item" @click="goToMyWorks">
<el-icon><Document /></el-icon>
<span>我的作品</span>
</div>
<div class="nav-divider"></div>
<div class="nav-item" @click="goToTextToVideo">
<el-icon><VideoPlay /></el-icon>
<span>文生视频</span>
</div>
<div class="nav-item active">
<el-icon><Picture /></el-icon>
<span>图生视频</span>
</div>
<div class="nav-item storyboard-item" @click="goToStoryboard">
<el-icon><VideoPlay /></el-icon>
<span>分镜视频</span>
<el-tag size="small" type="primary" class="sora-tag">Sora2.0</el-tag>
</div>
</nav>
</aside>
<!-- 主内容区域 -->
<main class="main-content">
<!-- 顶部用户信息卡片 -->
<div class="user-info-card">
<div class="user-avatar">
<div class="avatar-placeholder">||</div>
</div>
<div class="user-details">
<div class="username">mingzi_FBx7foZYDS7inLQb</div>
<div class="profile-prompt">还没有设置个人简介,点击填写</div>
<div class="user-id">ID 2994509784706419</div>
</div>
<div class="edit-profile-btn">
<el-button type="primary">编辑资料</el-button>
</div>
</div>
<!-- 已发布作品区域 -->
<div class="published-works">
<div class="works-tabs">
<div class="tab active">已发布</div>
</div>
<div class="works-grid">
<div class="work-item" v-for="(work, index) in publishedWorks" :key="work.id" @click="openDetail(work)">
<div class="work-thumbnail">
<img :src="work.cover" :alt="work.title" />
<div class="work-overlay">
<div class="overlay-text">{{ work.text }}</div>
</div>
</div>
<div class="work-info">
<div class="work-title">{{ work.title }}</div>
<div class="work-meta">{{ work.id }} · {{ work.size }}</div>
</div>
<div class="work-actions" v-if="index === 0">
<el-button type="primary" class="create-similar-btn" @click.stop="goToCreate(work)">做同款</el-button>
</div>
<div class="work-director" v-else>
<span>DIRECTED BY VANNOCENT</span>
</div>
</div>
</div>
</div>
</main>
<!-- 作品详情模态框 -->
<el-dialog
v-model="detailDialogVisible"
:title="selectedItem?.title"
width="60%"
class="detail-dialog"
:modal="true"
:close-on-click-modal="true"
:close-on-press-escape="true"
@close="handleClose"
>
<div class="detail-content">
<div class="detail-left">
<div class="video-player">
<img :src="selectedItem?.cover" :alt="selectedItem?.title" class="video-thumbnail" />
<div class="play-overlay">
<div class="play-button"></div>
</div>
</div>
</div>
<div class="detail-right">
<div class="metadata-section">
<div class="metadata-item">
<span class="label">作品 ID</span>
<span class="value">{{ selectedItem?.id }}</span>
</div>
<div class="metadata-item">
<span class="label">文件大小</span>
<span class="value">{{ selectedItem?.size }}</span>
</div>
<div class="metadata-item">
<span class="label">创建时间</span>
<span class="value">{{ selectedItem?.createTime }}</span>
</div>
<div class="metadata-item">
<span class="label">分类</span>
<span class="value">{{ selectedItem?.category }}</span>
</div>
</div>
<div class="description-section">
<h3 class="section-title">描述</h3>
<p class="description-text">{{ getDescription(selectedItem) }}</p>
</div>
<!-- 操作按钮 -->
<div class="action-section">
<button class="create-similar-btn" @click="createSimilar">
做同款
</button>
</div>
</div>
</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElIcon, ElButton, ElTag, ElMessage, ElDialog } from 'element-plus'
import { User, Compass, Document, VideoPlay, Picture } from '@element-plus/icons-vue'
const router = useRouter()
// 模态框状态
const detailDialogVisible = ref(false)
const selectedItem = ref(null)
// 已发布作品数据
const publishedWorks = ref([
{
id: '2995000000001',
title: '图生视频作品 #1',
cover: '/images/backgrounds/welcome.jpg',
text: 'What Does it Mean To You',
size: '9 MB',
category: '图生视频',
createTime: '2025/01/15 14:30'
},
{
id: '2995000000002',
title: '图生视频作品 #2',
cover: '/images/backgrounds/welcome.jpg',
text: 'What Does it Mean To You',
size: '9 MB',
category: '图生视频',
createTime: '2025/01/14 16:45'
},
{
id: '2995000000003',
title: '图生视频作品 #3',
cover: '/images/backgrounds/welcome.jpg',
text: 'What Does it Mean To You',
size: '9 MB',
category: '图生视频',
createTime: '2025/01/13 09:20'
}
])
// 导航函数
const goToProfile = () => {
router.push('/profile')
}
const goToSubscription = () => {
router.push('/subscription')
}
const goToMyWorks = () => {
router.push('/works')
}
const goToTextToVideo = () => {
router.push('/text-to-video')
}
const goToStoryboard = () => {
router.push('/storyboard-video')
}
const goToCreate = (work) => {
// 跳转到图生视频创作页面
router.push('/image-to-video/create')
}
// 模态框相关函数
const openDetail = (work) => {
selectedItem.value = work
detailDialogVisible.value = true
}
const handleClose = () => {
detailDialogVisible.value = false
selectedItem.value = null
}
const getDescription = (item) => {
if (!item) return ''
return `这是一个${item.category}作品,展现了"What Does it Mean To You"的主题。作品通过AI技术生成具有独特的视觉风格和创意表达。`
}
const createSimilar = () => {
// 关闭模态框并跳转到创作页面
handleClose()
router.push('/image-to-video/create')
}
onMounted(() => {
// 页面初始化
})
</script>
<style scoped>
.image-to-video-page {
display: flex;
height: 100vh;
background: #0a0a0a;
color: #fff;
margin: 0;
padding: 0;
}
/* 左侧导航栏 */
.sidebar {
width: 280px;
background: #1a1a1a;
border-right: 1px solid #333;
padding: 24px 0;
}
.logo {
font-size: 18px;
font-weight: 600;
color: #fff;
text-align: center;
margin-bottom: 32px;
}
.nav-menu {
display: flex;
flex-direction: column;
gap: 8px;
padding: 0 20px;
}
.nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
color: #d1d5db;
}
.nav-item:hover {
background: #2a2a2a;
color: #fff;
}
.nav-item.active {
background: #3b82f6;
color: #fff;
}
.nav-divider {
height: 1px;
background: #333;
margin: 16px 0;
}
.sora-tag {
margin-left: 8px;
}
/* 分镜视频特殊样式 */
.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);
}
}
/* 主内容区域 */
.main-content {
flex: 1;
padding: 24px;
display: flex;
flex-direction: column;
gap: 24px;
}
/* 用户信息卡片 */
.user-info-card {
background: #1a1a1a;
border: 1px solid #333;
border-radius: 12px;
padding: 24px;
display: flex;
align-items: center;
gap: 20px;
}
.user-avatar {
width: 80px;
height: 80px;
border-radius: 50%;
background: #000;
display: flex;
align-items: center;
justify-content: center;
border: 2px solid #333;
}
.avatar-placeholder {
color: #fff;
font-size: 24px;
font-weight: bold;
letter-spacing: 2px;
}
.user-details {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.username {
font-size: 18px;
font-weight: 600;
color: #fff;
}
.profile-prompt {
font-size: 14px;
color: #9ca3af;
}
.user-id {
font-size: 12px;
color: #6b7280;
}
.edit-profile-btn {
margin-left: auto;
}
/* 已发布作品区域 */
.published-works {
display: flex;
flex-direction: column;
gap: 20px;
}
.works-tabs {
display: flex;
gap: 24px;
}
.tab {
padding: 8px 0;
color: #9ca3af;
cursor: pointer;
position: relative;
}
.tab.active {
color: #fff;
}
.tab.active::after {
content: '';
position: absolute;
bottom: -8px;
left: 0;
right: 0;
height: 2px;
background: #3b82f6;
}
.works-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
.work-item {
background: #1a1a1a;
border: 1px solid #333;
border-radius: 12px;
overflow: hidden;
transition: all 0.2s;
}
.work-item:hover {
border-color: #3b82f6;
transform: translateY(-2px);
}
.work-thumbnail {
position: relative;
aspect-ratio: 16/9;
overflow: hidden;
}
.work-thumbnail img {
width: 100%;
height: 100%;
object-fit: cover;
}
.work-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.8));
padding: 20px;
}
.overlay-text {
font-size: 16px;
font-weight: 600;
color: #fff;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.8);
}
.work-info {
padding: 16px;
display: flex;
flex-direction: column;
gap: 8px;
}
.work-title {
font-size: 16px;
font-weight: 600;
color: #fff;
}
.work-meta {
font-size: 12px;
color: #9ca3af;
}
.work-actions {
padding: 0 16px 16px;
opacity: 0;
transition: opacity 0.2s ease;
}
.work-item:hover .work-actions {
opacity: 1;
}
.create-similar-btn {
width: 100%;
}
.work-director {
padding: 0 16px 16px;
text-align: center;
}
.work-director span {
font-size: 12px;
color: #6b7280;
font-style: italic;
}
/* 响应式设计 */
@media (max-width: 1024px) {
.sidebar {
width: 260px;
}
.works-grid {
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
}
}
@media (max-width: 768px) {
.image-to-video-page {
flex-direction: column;
}
.sidebar {
width: 100%;
height: auto;
}
.nav-menu {
flex-direction: row;
overflow-x: auto;
padding: 0 16px;
}
.nav-item {
white-space: nowrap;
}
.works-grid {
grid-template-columns: 1fr;
}
}
/* 模态框样式 */
:deep(.detail-dialog .el-dialog) {
background: #0a0a0a !important;
border-radius: 12px;
border: 1px solid #333 !important;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.8);
}
:deep(.detail-dialog .el-dialog__wrapper) {
background-color: rgba(0, 0, 0, 0.8) !important;
}
:deep(.detail-dialog .el-dialog__header) {
background: #0a0a0a !important;
padding: 16px 20px;
border-bottom: 1px solid #333;
}
:deep(.detail-dialog .el-dialog__title) {
color: #fff !important;
font-size: 18px;
font-weight: 600;
}
:deep(.detail-dialog .el-dialog__headerbtn) {
color: #fff !important;
}
:deep(.detail-dialog .el-dialog__body) {
background: #0a0a0a !important;
padding: 0 !important;
}
:deep(.detail-dialog .el-overlay) {
background-color: rgba(0, 0, 0, 0.8) !important;
}
/* 全局覆盖Element Plus默认样式 */
:deep(.el-dialog) {
background: #0a0a0a !important;
border: 1px solid #333 !important;
}
:deep(.el-dialog__wrapper) {
background-color: rgba(0, 0, 0, 0.8) !important;
}
:deep(.el-dialog__header) {
background: #0a0a0a !important;
}
:deep(.el-dialog__body) {
background: #0a0a0a !important;
}
:deep(.el-overlay) {
background-color: rgba(0, 0, 0, 0.8) !important;
}
.detail-content {
display: flex;
height: 50vh;
background: #0a0a0a;
}
.detail-left {
flex: 1;
padding: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.video-player {
position: relative;
width: 100%;
max-width: 400px;
aspect-ratio: 16/9;
border-radius: 8px;
overflow: hidden;
cursor: pointer;
}
.video-thumbnail {
width: 100%;
height: 100%;
object-fit: cover;
}
.play-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.2s ease;
}
.video-player:hover .play-overlay {
opacity: 1;
}
.play-button {
width: 60px;
height: 60px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.9);
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
color: #000;
font-weight: bold;
}
.detail-right {
flex: 1;
padding: 20px;
background: #0a0a0a;
display: flex;
flex-direction: column;
gap: 20px;
}
.metadata-section {
display: flex;
flex-direction: column;
gap: 12px;
}
.metadata-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #2a2a2a;
}
.metadata-item:last-child {
border-bottom: none;
}
.label {
font-size: 14px;
color: #9ca3af;
font-weight: 500;
}
.value {
font-size: 14px;
color: #fff;
font-weight: 600;
}
.description-section {
flex: 1;
}
.section-title {
font-size: 16px;
color: #fff;
font-weight: 600;
margin-bottom: 12px;
}
.description-text {
font-size: 14px;
color: #d1d5db;
line-height: 1.6;
margin: 0;
}
.action-section {
margin-top: auto;
}
.create-similar-btn {
width: 100%;
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
color: #fff;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
}
.create-similar-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
}
</style>

View File

@@ -0,0 +1,781 @@
<template>
<div class="image-to-video-create-page">
<!-- 顶部导航栏 -->
<header class="top-header">
<div class="header-left">
<button class="back-btn" @click="goBack">
首页
</button>
</div>
<div class="header-right">
<div class="credits-info">
<div class="credits-circle">25</div>
<span>| 首购优惠</span>
</div>
<div class="notification-icon">
🔔
<div class="notification-badge">5</div>
</div>
<div class="user-avatar">
👤
</div>
</div>
</header>
<!-- 主内容区域 -->
<div class="main-content">
<!-- 左侧设置面板 -->
<div class="left-panel">
<!-- 创作模式标签 -->
<div class="creation-tabs">
<div class="tab" @click="goToTextToVideo">文生视频</div>
<div class="tab active">图生视频</div>
<div class="tab" @click="goToStoryboard">分镜视频</div>
</div>
<!-- 图片输入区域 -->
<div class="image-input-section">
<div class="image-upload-area">
<div class="upload-box" @click="uploadFirstFrame">
<div class="upload-icon">+</div>
<div class="upload-text">首帧</div>
</div>
<div class="arrow-icon"></div>
<div class="upload-box optional" @click="uploadLastFrame">
<div class="upload-icon">+</div>
<div class="upload-text">尾帧 (可选)</div>
</div>
</div>
<!-- 已上传的图片预览 -->
<div class="image-preview" v-if="firstFrameImage || lastFrameImage">
<div class="preview-item" v-if="firstFrameImage">
<img :src="firstFrameImage" alt="首帧" />
<button class="remove-btn" @click="removeFirstFrame">×</button>
</div>
<div class="preview-item" v-if="lastFrameImage">
<img :src="lastFrameImage" alt="尾帧" />
<button class="remove-btn" @click="removeLastFrame">×</button>
</div>
</div>
</div>
<!-- 文本输入区域 -->
<div class="text-input-section">
<textarea
v-model="inputText"
placeholder="结合图片,描述想要生成的内容"
class="text-input"
rows="6"
></textarea>
<div class="optimize-btn">
<button class="optimize-button">
一键优化
</button>
</div>
</div>
<!-- 视频设置 -->
<div class="video-settings">
<div class="setting-item">
<label>比例</label>
<select v-model="aspectRatio" class="setting-select">
<option value="16:9">16:9</option>
<option value="4:3">4:3</option>
<option value="1:1">1:1</option>
<option value="3:4">3:4</option>
<option value="9:16">9:16</option>
</select>
</div>
<div class="setting-item">
<label>时长</label>
<select v-model="duration" class="setting-select">
<option value="5">5s</option>
<option value="10">10s</option>
<option value="15">15s</option>
<option value="30">30s</option>
</select>
</div>
<div class="setting-item">
<label>高清模式 (1080P)</label>
<div class="hd-setting">
<input type="checkbox" v-model="hdMode" class="hd-switch">
<span class="cost-text">开启消耗20积分</span>
</div>
</div>
</div>
<!-- 生成按钮 -->
<div class="generate-section">
<button class="generate-btn" @click="startGenerate">
开始生成
</button>
</div>
</div>
<!-- 右侧预览区域 -->
<div class="right-panel">
<div class="preview-area">
<div class="status-checkbox">
<input type="checkbox" v-model="inProgress" id="progress-checkbox">
<label for="progress-checkbox">进行中</label>
</div>
<div class="preview-content">
<div class="preview-placeholder">
<div class="placeholder-text">开始创作您的第一个作品吧!</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
// 表单数据
const inputText = ref('')
const aspectRatio = ref('16:9')
const duration = ref('5')
const hdMode = ref(false)
const inProgress = ref(false)
// 图片上传
const firstFrameImage = ref('')
const lastFrameImage = ref('')
// 导航函数
const goBack = () => {
router.back()
}
const goToTextToVideo = () => {
router.push('/text-to-video/create')
}
const goToStoryboard = () => {
alert('分镜视频功能开发中')
}
// 图片上传处理
const uploadFirstFrame = () => {
const input = document.createElement('input')
input.type = 'file'
input.accept = 'image/*'
input.onchange = (e) => {
const file = e.target.files[0]
if (file) {
const reader = new FileReader()
reader.onload = (e) => {
firstFrameImage.value = e.target.result
}
reader.readAsDataURL(file)
}
}
input.click()
}
const uploadLastFrame = () => {
const input = document.createElement('input')
input.type = 'file'
input.accept = 'image/*'
input.onchange = (e) => {
const file = e.target.files[0]
if (file) {
const reader = new FileReader()
reader.onload = (e) => {
lastFrameImage.value = e.target.result
}
reader.readAsDataURL(file)
}
}
input.click()
}
const removeFirstFrame = () => {
firstFrameImage.value = ''
}
const removeLastFrame = () => {
lastFrameImage.value = ''
}
const startGenerate = () => {
if (!firstFrameImage.value) {
alert('请上传首帧图片')
return
}
if (!inputText.value.trim()) {
alert('请输入描述文字')
return
}
inProgress.value = true
alert('开始生成视频...')
// 模拟生成过程
setTimeout(() => {
inProgress.value = false
alert('视频生成完成!')
}, 3000)
}
</script>
<style scoped>
.image-to-video-create-page {
height: 100vh;
background: #0a0a0a;
color: #fff;
display: flex;
flex-direction: column;
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
/* 顶部导航栏 */
.top-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 32px;
background: #0a0a0a;
border-bottom: 1px solid #1f1f1f;
min-height: 60px;
}
.header-left {
display: flex;
align-items: center;
}
.back-btn {
background: none;
border: none;
color: #fff;
font-size: 16px;
cursor: pointer;
padding: 10px 20px;
border-radius: 8px;
transition: all 0.2s ease;
font-weight: 500;
}
.back-btn:hover {
background: #1a1a1a;
transform: translateX(-2px);
}
.header-right {
display: flex;
align-items: center;
gap: 24px;
}
.credits-info {
display: flex;
align-items: center;
gap: 12px;
color: #fff;
font-size: 14px;
font-weight: 500;
}
.credits-circle {
width: 36px;
height: 36px;
border-radius: 50%;
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 16px;
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
.notification-icon {
position: relative;
font-size: 24px;
cursor: pointer;
padding: 8px;
border-radius: 8px;
transition: background 0.2s ease;
}
.notification-icon:hover {
background: #1a1a1a;
}
.notification-badge {
position: absolute;
top: 2px;
right: 2px;
background: #ef4444;
color: #fff;
border-radius: 50%;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 700;
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.4);
}
.user-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: linear-gradient(135deg, #374151, #1f2937);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 20px;
transition: transform 0.2s ease;
}
.user-avatar:hover {
transform: scale(1.05);
}
/* 主内容区域 */
.main-content {
flex: 1;
display: grid;
grid-template-columns: 400px 1fr;
gap: 0;
height: calc(100vh - 100px);
}
/* 左侧面板 */
.left-panel {
background: #1a1a1a;
border-right: 1px solid #2a2a2a;
padding: 32px;
display: flex;
flex-direction: column;
gap: 32px;
overflow-y: auto;
}
/* 创作模式标签 */
.creation-tabs {
display: flex;
gap: 4px;
background: #0a0a0a;
padding: 4px;
border-radius: 12px;
}
.tab {
flex: 1;
padding: 12px 16px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
color: #9ca3af;
font-size: 14px;
font-weight: 500;
text-align: center;
}
.tab.active {
background: #3b82f6;
color: #fff;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
}
.tab:hover:not(.active) {
background: #2a2a2a;
color: #fff;
}
/* 图片输入区域 */
.image-input-section {
display: flex;
flex-direction: column;
gap: 16px;
}
.image-upload-area {
display: flex;
align-items: center;
gap: 16px;
}
.upload-box {
flex: 1;
aspect-ratio: 1;
background: #0a0a0a;
border: 2px dashed #2a2a2a;
border-radius: 12px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
}
.upload-box:hover {
border-color: #3b82f6;
background: #1a1a1a;
}
.upload-box.optional {
opacity: 0.7;
}
.upload-icon {
font-size: 32px;
color: #6b7280;
font-weight: 300;
margin-bottom: 8px;
}
.upload-text {
font-size: 14px;
color: #9ca3af;
font-weight: 500;
}
.arrow-icon {
font-size: 20px;
color: #6b7280;
font-weight: bold;
}
.image-preview {
display: flex;
gap: 16px;
}
.preview-item {
flex: 1;
position: relative;
aspect-ratio: 1;
}
.preview-item img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 8px;
}
.remove-btn {
position: absolute;
top: 8px;
right: 8px;
width: 24px;
height: 24px;
border-radius: 50%;
background: rgba(0, 0, 0, 0.7);
color: #fff;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-weight: bold;
}
/* 文本输入区域 */
.text-input-section {
display: flex;
flex-direction: column;
gap: 16px;
}
.text-input {
width: 100%;
min-height: 120px;
padding: 16px;
background: #0a0a0a;
border: 2px solid #2a2a2a;
border-radius: 12px;
color: #fff;
font-size: 15px;
line-height: 1.6;
resize: vertical;
outline: none;
transition: all 0.2s ease;
font-family: inherit;
}
.text-input:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.text-input::placeholder {
color: #6b7280;
}
.optimize-btn {
display: flex;
justify-content: flex-end;
}
.optimize-button {
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
color: #fff;
border: none;
padding: 10px 20px;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
}
.optimize-button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
}
/* 视频设置 */
.video-settings {
display: flex;
flex-direction: column;
gap: 20px;
}
.setting-item {
display: flex;
flex-direction: column;
gap: 10px;
}
.setting-item label {
font-size: 14px;
color: #e5e7eb;
font-weight: 600;
}
.setting-select {
padding: 12px 16px;
background: #0a0a0a;
border: 2px solid #2a2a2a;
border-radius: 8px;
color: #fff;
font-size: 14px;
outline: none;
cursor: pointer;
transition: all 0.2s ease;
font-family: inherit;
}
.setting-select:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.setting-select:hover {
border-color: #374151;
}
.hd-setting {
display: flex;
align-items: center;
gap: 12px;
}
.hd-switch {
width: 20px;
height: 20px;
cursor: pointer;
accent-color: #3b82f6;
}
.cost-text {
font-size: 13px;
color: #9ca3af;
font-weight: 500;
}
/* 生成按钮 */
.generate-section {
margin-top: auto;
}
.generate-btn {
width: 100%;
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
color: #fff;
border: none;
padding: 16px 24px;
border-radius: 12px;
font-size: 16px;
font-weight: 700;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 4px 16px rgba(59, 130, 246, 0.3);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.generate-btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(59, 130, 246, 0.4);
}
.generate-btn:active {
transform: translateY(0);
}
.generate-btn:disabled {
background: #6b7280;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
/* 右侧面板 */
.right-panel {
background: #0a0a0a;
padding: 32px;
display: flex;
flex-direction: column;
}
.preview-area {
flex: 1;
display: flex;
flex-direction: column;
gap: 20px;
}
.status-checkbox {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
}
.status-checkbox input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
accent-color: #3b82f6;
}
.status-checkbox label {
font-size: 14px;
color: #e5e7eb;
cursor: pointer;
font-weight: 500;
}
.preview-content {
flex: 1;
background: #1a1a1a;
border: 2px solid #2a2a2a;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.preview-content:hover {
border-color: #374151;
}
.preview-placeholder {
text-align: center;
padding: 40px;
}
.placeholder-text {
font-size: 18px;
color: #6b7280;
font-weight: 500;
line-height: 1.5;
}
/* 响应式设计 */
@media (max-width: 1200px) {
.main-content {
grid-template-columns: 350px 1fr;
}
.left-panel {
padding: 24px;
}
.right-panel {
padding: 24px;
}
}
@media (max-width: 1024px) {
.main-content {
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
}
.left-panel {
border-right: none;
border-bottom: 1px solid #2a2a2a;
padding: 20px;
}
.right-panel {
padding: 20px;
}
}
@media (max-width: 768px) {
.top-header {
padding: 16px 20px;
}
.header-right {
gap: 16px;
}
.left-panel {
padding: 16px;
gap: 24px;
}
.right-panel {
padding: 16px;
}
.creation-tabs {
flex-direction: column;
gap: 8px;
}
.tab {
text-align: left;
}
.image-upload-area {
flex-direction: column;
gap: 12px;
}
.arrow-icon {
transform: rotate(90deg);
}
}
</style>

View File

@@ -0,0 +1,587 @@
<template>
<div class="video-detail-page">
<!-- 顶部导航栏 -->
<div class="top-bar">
<div class="logo">logo</div>
<div class="top-actions">
<el-icon class="action-icon"><User /></el-icon>
<el-icon class="action-icon"><Setting /></el-icon>
<el-icon class="action-icon"><Bell /></el-icon>
</div>
</div>
<!-- 左侧导航栏 -->
<aside class="sidebar">
<div class="nav-item">
<el-icon><Document /></el-icon>
<span>文件</span>
</div>
<div class="nav-item">
<el-icon><Picture /></el-icon>
<span>图片</span>
</div>
<div class="nav-item">
<el-icon><VideoPlay /></el-icon>
<span>视频</span>
</div>
</aside>
<!-- 主内容区域 -->
<main class="main-content">
<!-- 视频播放器区域 -->
<div class="video-section">
<div class="video-player">
<video
ref="videoRef"
:src="videoData.videoUrl"
@click="togglePlay"
@timeupdate="updateTime"
@loadedmetadata="onLoadedMetadata"
>
您的浏览器不支持视频播放
</video>
<!-- 视频控制栏 -->
<div class="video-controls" v-show="showControls">
<div class="controls-left">
<el-button circle size="small" @click="togglePlay">
<el-icon><VideoPlay v-if="!isPlaying" /><VideoPause v-else /></el-icon>
</el-button>
<span class="time-display">{{ formatTime(currentTime) }} / {{ formatTime(duration) }}</span>
</div>
<div class="controls-right">
<el-button circle size="small" @click="toggleFullscreen">
<el-icon><FullScreen /></el-icon>
</el-button>
</div>
</div>
<!-- 视频操作按钮 -->
<div class="video-actions">
<el-tooltip content="分享" placement="bottom">
<el-button circle size="small" @click="shareVideo">
<el-icon><Share /></el-icon>
</el-button>
</el-tooltip>
<el-tooltip content="下载" placement="bottom">
<el-button circle size="small" @click="downloadVideo">
<el-icon><Download /></el-icon>
</el-button>
</el-tooltip>
<el-tooltip content="删除" placement="bottom">
<el-button circle size="small" @click="deleteVideo">
<el-icon><Delete /></el-icon>
</el-button>
</el-tooltip>
</div>
</div>
</div>
<!-- 右侧详情区域 -->
<div class="detail-section">
<div class="detail-header">
<h3>图片详情</h3>
<p class="subtitle">参考生图</p>
</div>
<div class="detail-content">
<div class="input-section">
<el-input
v-model="detailInput"
placeholder="输入详情"
type="textarea"
:rows="3"
/>
</div>
<div class="thumbnails">
<div class="thumbnail" v-for="(thumb, index) in thumbnails" :key="index">
<img :src="thumb" :alt="`缩略图${index + 1}`" />
</div>
</div>
<div class="description">
<h4>描述</h4>
<p>{{ videoData.description }}</p>
</div>
<div class="metadata">
<div class="meta-item">
<span class="label">创建时间</span>
<span class="value">{{ videoData.createTime }}</span>
</div>
<div class="meta-item">
<span class="label">视频 ID</span>
<span class="value">{{ videoData.id }}</span>
</div>
<div class="meta-item">
<span class="label">时长</span>
<span class="value">{{ videoData.duration }}s</span>
</div>
<div class="meta-item">
<span class="label">清晰度</span>
<span class="value">{{ videoData.resolution }}</span>
</div>
<div class="meta-item">
<span class="label">宽高比</span>
<span class="value">{{ videoData.aspectRatio }}</span>
</div>
</div>
<div class="action-button">
<el-button type="primary" size="large" @click="makeSimilar">
做同款
</el-button>
</div>
</div>
<!-- 滚动指示器 -->
<div class="scroll-indicators">
<el-icon class="scroll-arrow up"><ArrowUp /></el-icon>
<el-icon class="scroll-arrow down"><ArrowDown /></el-icon>
</div>
</div>
</main>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
User, Setting, Bell, Document, Picture, VideoPlay, VideoPause,
FullScreen, Share, Download, Delete, ArrowUp, ArrowDown
} from '@element-plus/icons-vue'
const route = useRoute()
const videoRef = ref(null)
// 视频播放状态
const isPlaying = ref(false)
const currentTime = ref(0)
const duration = ref(0)
const showControls = ref(true)
// 详情数据
const detailInput = ref('')
const videoData = ref({
id: '2995697841305810',
videoUrl: '/images/backgrounds/welcome.jpg', // 临时使用图片实际应该是视频URL
description: '图1在图2中奔跑视频',
createTime: '2025/10/17 13:41',
duration: 5,
resolution: '1080p',
aspectRatio: '16:9'
})
const thumbnails = ref([
'/images/backgrounds/welcome.jpg',
'/images/backgrounds/welcome.jpg'
])
// 视频控制方法
const togglePlay = () => {
if (!videoRef.value) return
if (isPlaying.value) {
videoRef.value.pause()
} else {
videoRef.value.play()
}
isPlaying.value = !isPlaying.value
}
const updateTime = () => {
if (videoRef.value) {
currentTime.value = videoRef.value.currentTime
}
}
const onLoadedMetadata = () => {
if (videoRef.value) {
duration.value = videoRef.value.duration
}
}
const formatTime = (time) => {
const minutes = Math.floor(time / 60)
const seconds = Math.floor(time % 60)
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
}
const toggleFullscreen = () => {
if (!videoRef.value) return
if (document.fullscreenElement) {
document.exitFullscreen()
} else {
videoRef.value.requestFullscreen()
}
}
// 操作按钮方法
const shareVideo = () => {
ElMessage.info('分享功能开发中')
}
const downloadVideo = () => {
ElMessage.success('开始下载视频')
}
const deleteVideo = async () => {
try {
await ElMessageBox.confirm('确定要删除这个视频吗?', '删除确认', {
type: 'warning',
confirmButtonText: '删除',
cancelButtonText: '取消'
})
ElMessage.success('视频已删除')
} catch (_) {}
}
const makeSimilar = () => {
ElMessage.info('做同款功能开发中')
}
// 自动隐藏控制栏
let controlsTimer = null
const resetControlsTimer = () => {
clearTimeout(controlsTimer)
showControls.value = true
controlsTimer = setTimeout(() => {
showControls.value = false
}, 3000)
}
onMounted(() => {
// 监听鼠标移动来显示/隐藏控制栏
document.addEventListener('mousemove', resetControlsTimer)
resetControlsTimer()
})
onUnmounted(() => {
clearTimeout(controlsTimer)
document.removeEventListener('mousemove', resetControlsTimer)
})
</script>
<style scoped>
.video-detail-page {
height: 100vh;
background: #0a0a0a;
color: white;
display: flex;
flex-direction: column;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
/* 顶部导航栏 */
.top-bar {
height: 60px;
background: #1a1a1a;
border-bottom: 1px solid #333;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
z-index: 100;
}
.logo {
font-size: 18px;
font-weight: 500;
color: white;
}
.top-actions {
display: flex;
gap: 16px;
}
.action-icon {
font-size: 20px;
color: #cbd5e1;
cursor: pointer;
transition: color 0.3s;
}
.action-icon:hover {
color: white;
}
/* 左侧导航栏 */
.sidebar {
position: fixed;
left: 0;
top: 60px;
width: 200px;
height: calc(100vh - 60px);
background: #1a1a1a;
border-right: 1px solid #333;
padding: 20px 0;
z-index: 90;
}
.nav-item {
display: flex;
align-items: center;
padding: 12px 20px;
color: #cbd5e1;
cursor: pointer;
transition: all 0.3s;
}
.nav-item:hover {
background: #2a2a2a;
color: white;
}
.nav-item .el-icon {
margin-right: 12px;
font-size: 18px;
}
/* 主内容区域 */
.main-content {
margin-left: 200px;
margin-top: 60px;
height: calc(100vh - 60px);
display: flex;
}
/* 视频播放器区域 */
.video-section {
flex: 2;
padding: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.video-player {
position: relative;
width: 100%;
max-width: 800px;
aspect-ratio: 16/9;
background: #000;
border-radius: 8px;
overflow: hidden;
}
.video-player video {
width: 100%;
height: 100%;
object-fit: cover;
cursor: pointer;
}
.video-controls {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(transparent, rgba(0,0,0,0.8));
padding: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.controls-left {
display: flex;
align-items: center;
gap: 12px;
}
.time-display {
color: white;
font-size: 14px;
font-family: monospace;
}
.video-actions {
position: absolute;
top: 20px;
right: 20px;
display: flex;
gap: 8px;
}
.video-actions .el-button {
background: rgba(0,0,0,0.6);
border: 1px solid rgba(255,255,255,0.2);
color: white;
}
.video-actions .el-button:hover {
background: rgba(0,0,0,0.8);
border-color: rgba(255,255,255,0.4);
}
/* 右侧详情区域 */
.detail-section {
flex: 1;
background: #1a1a1a;
border-left: 1px solid #333;
padding: 20px;
overflow-y: auto;
position: relative;
}
.detail-header h3 {
font-size: 20px;
font-weight: 600;
margin-bottom: 4px;
color: white;
}
.subtitle {
color: #9ca3af;
font-size: 14px;
margin-bottom: 20px;
}
.detail-content {
display: flex;
flex-direction: column;
gap: 20px;
}
.input-section {
margin-bottom: 10px;
}
.thumbnails {
display: flex;
gap: 8px;
}
.thumbnail {
width: 60px;
height: 60px;
border-radius: 6px;
overflow: hidden;
background: #2a2a2a;
}
.thumbnail img {
width: 100%;
height: 100%;
object-fit: cover;
}
.description h4 {
font-size: 16px;
font-weight: 600;
margin-bottom: 8px;
color: white;
}
.description p {
color: #cbd5e1;
font-size: 14px;
line-height: 1.5;
}
.metadata {
display: flex;
flex-direction: column;
gap: 12px;
}
.meta-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #2a2a2a;
}
.meta-item:last-child {
border-bottom: none;
}
.label {
color: #9ca3af;
font-size: 14px;
}
.value {
color: white;
font-size: 14px;
font-weight: 500;
}
.action-button {
margin-top: 20px;
}
.action-button .el-button {
width: 100%;
height: 44px;
font-size: 16px;
font-weight: 600;
}
.scroll-indicators {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
display: flex;
flex-direction: column;
gap: 8px;
}
.scroll-arrow {
font-size: 16px;
color: #6b7280;
cursor: pointer;
transition: color 0.3s;
}
.scroll-arrow:hover {
color: #9ca3af;
}
/* 响应式设计 */
@media (max-width: 1024px) {
.sidebar {
width: 160px;
}
.main-content {
margin-left: 160px;
}
.video-section {
padding: 10px;
}
.detail-section {
padding: 15px;
}
}
@media (max-width: 768px) {
.sidebar {
display: none;
}
.main-content {
margin-left: 0;
flex-direction: column;
}
.video-section {
flex: none;
height: 50vh;
}
.detail-section {
flex: none;
height: 50vh;
}
}
</style>

View File

@@ -0,0 +1,504 @@
<template>
<div class="login-page">
<!-- Logo -->
<div class="logo">Logo</div>
<!-- 登录卡片 -->
<div class="login-card">
<!-- Logo图标 -->
<div class="card-logo">
<div class="logo-icon">Logo</div>
</div>
<!-- 欢迎文字 -->
<div class="welcome-text">
<h1>欢迎来到 Logo</h1>
<p>智创无限,灵感变现</p>
</div>
<!-- 登录表单 -->
<div class="login-form">
<!-- 手机号输入 -->
<div class="phone-input-group">
<div class="country-code">
<span>+86</span>
<el-icon><ArrowDown /></el-icon>
</div>
<el-input
v-model="loginForm.phone"
placeholder="请输入手机号"
class="phone-input"
/>
</div>
<!-- 验证码输入 -->
<div class="code-input-group">
<el-input
v-model="loginForm.code"
placeholder="请输入验证码"
class="code-input"
/>
<el-button
type="primary"
plain
class="get-code-btn"
:disabled="countdown > 0"
@click="getCode"
>
{{ countdown > 0 ? `${countdown}s` : '获取验证码' }}
</el-button>
</div>
<!-- 登录按钮 -->
<el-button
type="primary"
class="login-button"
:loading="userStore.loading"
@click="handleLogin"
>
{{ userStore.loading ? '登录中...' : '登陆/注册' }}
</el-button>
<!-- 协议文字 -->
<p class="agreement-text">
登录即表示您同意遵守用户协议和隐私政策
</p>
<!-- 测试账号提示 -->
<div class="test-accounts">
<el-divider>测试账号</el-divider>
<div class="account-list">
<div class="account-item" @click="fillTestAccount('15538239326', '0627')">
<strong>普通用户:</strong> 15538239326 / 0627
</div>
<div class="account-item" @click="fillTestAccount('15538239327', 'admin123')">
<strong>管理员:</strong> 15538239327 / admin123
</div>
<div class="account-item" @click="fillTestAccount('15538239328', 'test123')">
<strong>测试用户:</strong> 15538239328 / test123
</div>
<div class="account-item" @click="fillTestAccount('15538239329', '123456')">
<strong>个人主页:</strong> 15538239329 / 123456
</div>
</div>
</div>
</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 } from 'element-plus'
import { ArrowDown } from '@element-plus/icons-vue'
const router = useRouter()
const route = useRoute()
const userStore = useUserStore()
const countdown = ref(0)
let countdownTimer = null
const loginForm = reactive({
phone: '',
code: ''
})
// 清除可能的缓存数据
const clearForm = () => {
loginForm.phone = ''
loginForm.code = ''
}
// 快速填充测试账号
const fillTestAccount = (username, password) => {
loginForm.phone = username
loginForm.code = password
}
// 组件挂载时清除表单
onMounted(() => {
clearForm()
})
// 获取验证码
const getCode = () => {
if (!loginForm.phone) {
ElMessage.warning('请先输入手机号')
return
}
if (!/^1[3-9]\d{9}$/.test(loginForm.phone)) {
ElMessage.warning('请输入正确的手机号')
return
}
// 模拟发送验证码
ElMessage.success('验证码已发送')
// 开始倒计时
countdown.value = 60
countdownTimer = setInterval(() => {
countdown.value--
if (countdown.value <= 0) {
clearInterval(countdownTimer)
countdownTimer = null
}
}, 1000)
}
const handleLogin = async () => {
if (!loginForm.phone) {
ElMessage.warning('请输入手机号')
return
}
if (!loginForm.code) {
ElMessage.warning('请输入验证码')
return
}
if (!/^1[3-9]\d{9}$/.test(loginForm.phone)) {
ElMessage.warning('请输入正确的手机号')
return
}
try {
console.log('开始登录...')
// 模拟验证码登录这里可以调用实际的API
// 为了演示,我们使用手机号作为用户名,验证码作为密码
const mockForm = {
username: loginForm.phone,
password: loginForm.code
}
const result = await userStore.loginUser(mockForm)
if (result.success) {
console.log('登录成功,用户信息:', userStore.user)
ElMessage.success('登录成功')
// 等待一下确保状态更新
await new Promise(resolve => setTimeout(resolve, 200))
// 跳转到原始路径或个人主页
const redirectPath = route.query.redirect || '/profile'
console.log('准备跳转到:', redirectPath)
// 使用replace而不是push避免浏览器历史记录问题
await router.replace(redirectPath)
console.log('路由跳转完成')
} else {
ElMessage.error(result.message || '登录失败')
}
} catch (error) {
console.error('Login error:', error)
ElMessage.error('登录失败,请重试')
}
}
</script>
<style scoped>
.login-page {
min-height: 100vh;
background: url('/images/backgrounds/login.png') center/cover no-repeat;
position: relative;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
/* 背景效果已移除,使用图片背景 */
/* 左上角Logo */
.logo {
position: absolute;
top: 30px;
left: 30px;
color: white;
font-size: 18px;
font-weight: 500;
z-index: 10;
}
/* 登录卡片 */
.login-card {
position: absolute;
top: 50%;
right: 8%;
transform: translateY(-50%);
width: 500px;
background: rgba(26, 26, 46, 0.8);
backdrop-filter: blur(20px);
border-radius: 24px;
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 50px;
z-index: 10;
}
/* 卡片内Logo */
.card-logo {
text-align: center;
margin-bottom: 30px;
}
.logo-icon {
width: 80px;
height: 80px;
background: rgba(0, 0, 0, 0.3);
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
color: white;
font-size: 20px;
font-weight: 500;
}
/* 欢迎文字 */
.welcome-text {
text-align: center;
margin-bottom: 50px;
}
.welcome-text h1 {
color: white;
font-size: 28px;
font-weight: 600;
margin: 0 0 12px 0;
}
.welcome-text p {
color: rgba(255, 255, 255, 0.7);
font-size: 16px;
margin: 0;
}
/* 登录表单 */
.login-form {
display: flex;
flex-direction: column;
gap: 25px;
}
/* 手机号输入组 */
.phone-input-group {
display: flex;
gap: 12px;
}
.country-code {
width: 100px;
height: 55px;
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 16px;
cursor: pointer;
transition: all 0.3s ease;
}
.country-code:hover {
background: rgba(0, 0, 0, 0.5);
}
.country-code .el-icon {
margin-left: 6px;
font-size: 14px;
}
.phone-input {
flex: 1;
}
.phone-input :deep(.el-input__wrapper) {
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 10px;
box-shadow: none;
height: 55px;
}
.phone-input :deep(.el-input__inner) {
color: white;
background: transparent;
font-size: 16px;
}
.phone-input :deep(.el-input__inner::placeholder) {
color: rgba(255, 255, 255, 0.5);
}
/* 验证码输入组 */
.code-input-group {
display: flex;
gap: 12px;
}
.code-input {
flex: 1;
}
.code-input :deep(.el-input__wrapper) {
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 10px;
box-shadow: none;
height: 55px;
}
.code-input :deep(.el-input__inner) {
color: white;
background: transparent;
font-size: 16px;
}
.code-input :deep(.el-input__inner::placeholder) {
color: rgba(255, 255, 255, 0.5);
}
.get-code-btn {
background: transparent;
border: 1px solid #409EFF;
color: #409EFF;
border-radius: 10px;
padding: 0 20px;
font-size: 16px;
height: 55px;
transition: all 0.3s ease;
}
.get-code-btn:hover {
background: #409EFF;
color: white;
}
.get-code-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* 登录按钮 */
.login-button {
width: 100%;
height: 50px;
background: #409EFF;
border: none;
border-radius: 8px;
color: white;
font-size: 16px;
font-weight: 500;
margin-top: 15px;
transition: all 0.3s ease;
}
.login-button:hover {
background: #337ecc;
transform: translateY(-2px);
}
.login-button:active {
transform: translateY(0);
}
/* 协议文字 */
.agreement-text {
text-align: center;
color: rgba(255, 255, 255, 0.5);
font-size: 14px;
margin: 25px 0 0 0;
line-height: 1.4;
}
/* 测试账号提示 */
.test-accounts {
margin-top: 30px;
}
.test-accounts :deep(.el-divider__text) {
color: rgba(255, 255, 255, 0.6);
font-size: 12px;
}
.account-list {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 15px;
}
.account-item {
font-size: 12px;
color: rgba(255, 255, 255, 0.7);
padding: 6px 12px;
background: rgba(0, 0, 0, 0.2);
border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.1);
cursor: pointer;
transition: all 0.3s ease;
}
.account-item:hover {
background: rgba(0, 0, 0, 0.4);
border-color: rgba(255, 255, 255, 0.3);
transform: translateY(-1px);
}
.account-item strong {
color: #409EFF;
margin-right: 8px;
}
/* 响应式设计 */
@media (max-width: 1200px) {
.login-card {
right: 5%;
width: 450px;
}
}
@media (max-width: 768px) {
.login-card {
position: relative;
top: auto;
right: auto;
transform: none;
margin: 50px auto;
width: 90%;
max-width: 500px;
}
.logo {
position: relative;
top: auto;
left: auto;
text-align: center;
margin-bottom: 30px;
padding-top: 30px;
}
}
@media (max-width: 480px) {
.login-card {
padding: 40px 25px;
}
.phone-input-group,
.code-input-group {
flex-direction: column;
gap: 15px;
}
.country-code {
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,794 @@
<template>
<div class="works-page">
<div class="toolbar">
<el-radio-group v-model="activeTab" size="small" class="seg-control">
<el-radio-button label="all">全部</el-radio-button>
<el-radio-button label="video">视频</el-radio-button>
<el-radio-button label="image">图片</el-radio-button>
</el-radio-group>
</div>
<div class="filters-bar">
<el-space wrap size="small" class="filters">
<el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" size="small" />
<el-select v-model="category" placeholder="任务类型" size="small" style="width: 120px" @change="onFilterChange">
<el-option label="全部" value="all" />
<el-option label="文生视频" value="text2video" />
<el-option label="图生视频" value="image2video" />
<el-option label="分镜视频" value="storyboard" />
<el-option label="参考图" value="reference" />
</el-select>
<el-select v-model="resolution" placeholder="清晰度" clearable size="small" style="width: 120px">
<el-option label="标清" value="sd" />
<el-option label="高清" value="hd" />
<el-option label="超清" value="uhd" />
</el-select>
<el-select v-model="sortBy" size="small" style="width: 120px">
<el-option label="比例" value="ratio" />
<el-option label="时间" value="date" />
<el-option label="热门" value="hot" />
</el-select>
<el-select v-model="order" size="small" style="width: 100px">
<el-option label="升序" value="asc" />
<el-option label="降序" value="desc" />
</el-select>
</el-space>
<div class="right">
<el-input v-model="keyword" placeholder="名字/提示词/ID" size="small" clearable style="width: 220px" @keyup.enter.native="reload" />
</div>
</div>
<div class="select-row">
<el-checkbox v-model="multiSelect" size="small">选择多个</el-checkbox>
<template v-if="multiSelect && selectedIds.size">
<el-tag type="success" size="small">已选 {{ selectedIds.size }} 个项目</el-tag>
<el-button size="small" type="primary" @click="bulkDownload" plain>下载</el-button>
<el-button size="small" type="danger" @click="bulkDelete" plain>删除</el-button>
</template>
</div>
<el-row :gutter="16" class="works-grid">
<el-col v-for="item in filteredItems" :key="item.id" :xs="24" :sm="12" :md="8" :lg="6">
<el-card class="work-card" :class="{ selected: selectedIds.has(item.id) }" shadow="hover">
<div class="thumb" @click="multiSelect ? toggleSelect(item.id) : openDetail(item)">
<img :src="item.cover" :alt="item.title" />
<div class="checker" v-if="multiSelect">
<el-checkbox :model-value="selectedIds.has(item.id)" @change="() => toggleSelect(item.id)" />
</div>
<div class="actions" @click.stop>
<el-tooltip content="收藏" placement="top"><el-button circle size="small" text><el-icon><Star /></el-icon></el-button></el-tooltip>
<el-dropdown @command="(cmd)=>moreCommand(cmd,item)">
<el-button circle size="small" text><el-icon><MoreFilled /></el-icon></el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="download_with_watermark">带水印下载</el-dropdown-item>
<el-dropdown-item command="download_without_watermark">
不带水印下载
<el-tag type="primary" size="small" style="margin-left: 8px;">会员</el-tag>
</el-dropdown-item>
<el-dropdown-item command="rename" divided>重命名</el-dropdown-item>
<el-dropdown-item command="delete">删除</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
<div class="meta">
<div class="title" :title="item.title">{{ item.title }}</div>
<div class="sub">{{ item.id }} · {{ item.sizeText }}</div>
</div>
<template #footer>
<el-space size="small">
<el-button text size="small" @click.stop="download(item)">下载</el-button>
<el-button text size="small" @click.stop="share(item)">分享</el-button>
</el-space>
</template>
</el-card>
</el-col>
</el-row>
<!-- 作品详情模态框 -->
<el-dialog
v-model="detailDialogVisible"
:title="selectedItem?.title"
width="60%"
:before-close="handleClose"
class="detail-dialog"
:modal="true"
:close-on-click-modal="true"
:close-on-press-escape="true"
>
<div class="detail-content" v-if="selectedItem">
<div class="detail-left">
<div class="video-container">
<video
v-if="selectedItem.type === 'video'"
class="detail-video"
:src="selectedItem.cover"
:poster="selectedItem.cover"
controls
>
您的浏览器不支持视频播放
</video>
<img
v-else
class="detail-image"
:src="selectedItem.cover"
:alt="selectedItem.title"
/>
<!-- 视频文字叠加 -->
<div class="video-overlay" v-if="selectedItem.type === 'video' && selectedItem.overlayText">
<div class="overlay-text">{{ selectedItem.overlayText }}</div>
</div>
</div>
</div>
<div class="detail-right">
<!-- 用户信息头部 -->
<div class="detail-header">
<div class="user-info">
<div class="avatar">
<el-icon><User /></el-icon>
</div>
<div class="username">mingzi_FBx7foZYDS7inL</div>
</div>
</div>
<!-- 标签页 -->
<div class="tabs">
<div class="tab" :class="{ active: activeDetailTab === 'detail' }" @click="activeDetailTab = 'detail'">作品详情</div>
<div class="tab" :class="{ active: activeDetailTab === 'category' }" @click="activeDetailTab = 'category'">{{ selectedItem.category }}</div>
</div>
<!-- 描述区域 -->
<div class="description-section" v-if="activeDetailTab === 'detail'">
<h3 class="section-title">描述</h3>
<p class="description-text">{{ getDescription(selectedItem) }}</p>
</div>
<!-- 参考图特殊内容 -->
<div class="reference-content" v-if="activeDetailTab === 'category' && selectedItem.category === '参考图'">
<div class="input-details-section">
<h3 class="section-title">输入详情</h3>
<div class="input-images">
<div class="input-image-item">
<img :src="selectedItem.cover" :alt="selectedItem.title" class="input-thumbnail" />
</div>
<div class="input-image-item">
<img :src="selectedItem.cover" :alt="selectedItem.title" class="input-thumbnail" />
</div>
</div>
</div>
<div class="description-section">
<h3 class="section-title">描述</h3>
<p class="description-text">图1在图2中奔跑视频</p>
</div>
</div>
<!-- 其他分类的内容 -->
<div class="description-section" v-if="activeDetailTab === 'category' && selectedItem.category !== '参考图'">
<h3 class="section-title">描述</h3>
<p class="description-text">{{ getDescription(selectedItem) }}</p>
</div>
<!-- 元数据区域 -->
<div class="metadata-section">
<div class="metadata-item">
<span class="label">创建时间</span>
<span class="value">{{ selectedItem.createTime }}</span>
</div>
<div class="metadata-item">
<span class="label">作品 ID</span>
<span class="value">{{ selectedItem.id }}</span>
</div>
<div class="metadata-item">
<span class="label">日期</span>
<span class="value">{{ selectedItem.date }}</span>
</div>
<div class="metadata-item" v-if="selectedItem.type === 'video'">
<span class="label">时长</span>
<span class="value">5s</span>
</div>
<div class="metadata-item" v-if="selectedItem.type === 'video'">
<span class="label">清晰度</span>
<span class="value">1080p</span>
</div>
<div class="metadata-item">
<span class="label">分类</span>
<span class="value">{{ selectedItem.category }}</span>
</div>
<div class="metadata-item" v-if="selectedItem.type === 'video'">
<span class="label">宽高比</span>
<span class="value">16:9</span>
</div>
</div>
<!-- 操作按钮 -->
<div class="action-section">
<button class="create-similar-btn" @click="createSimilar">
做同款
</button>
</div>
</div>
</div>
</el-dialog>
<div class="finished" v-if="!hasMore && filteredItems.length>0">已加载全部内容</div>
<el-empty v-if="!loading && filteredItems.length===0" description="没有找到相关内容" />
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Star, MoreFilled, User } from '@element-plus/icons-vue'
const router = useRouter()
const activeTab = ref('all')
const dateRange = ref([])
const category = ref('all')
const resolution = ref('')
const sortBy = ref('date')
const order = ref('desc')
const keyword = ref('')
const multiSelect = ref(false)
const selectedIds = ref(new Set())
// 模态框相关状态
const detailDialogVisible = ref(false)
const selectedItem = ref(null)
const activeDetailTab = ref('detail')
const page = ref(1)
const pageSize = ref(4)
const loading = ref(false)
const hasMore = ref(true)
const items = ref([])
const mockData = (count, startId = 1) => Array.from({ length: count }).map((_, i) => {
const id = startId + i
// 定义不同的分类和类型
const categories = [
{ type: 'image', category: '参考图', title: '图片作品' },
{ type: 'image', category: '参考图', title: '图片作品' },
{ type: 'video', category: '文生视频', title: '视频作品' },
{ type: 'video', category: '图生视频', title: '视频作品' }
]
const itemConfig = categories[i] || categories[0]
// 生成不同的日期
const dates = ['2025/01/15', '2025/01/14', '2025/01/13', '2025/01/12']
const createTimes = ['2025/01/15 14:30', '2025/01/14 16:45', '2025/01/13 09:20', '2025/01/12 11:15']
return {
id: `2995${id.toString().padStart(9,'0')}`,
title: `${itemConfig.title} #${id}`,
type: itemConfig.type,
category: itemConfig.category,
sizeText: itemConfig.type === 'video' ? '9 MB' : '6 MB',
cover: itemConfig.type === 'video'
? '/images/backgrounds/welcome.jpg'
: '/images/backgrounds/login.png',
createTime: createTimes[i] || createTimes[0],
date: dates[i] || dates[0]
}
})
const loadList = async () => {
loading.value = true
// TODO: 替换为真实接口
await new Promise(r => setTimeout(r, 400))
const data = mockData(pageSize.value, (page.value - 1) * pageSize.value + 1)
if (page.value === 1) items.value = []
items.value = items.value.concat(data)
hasMore.value = false
loading.value = false
}
// 筛选后的作品列表
const filteredItems = computed(() => {
let filtered = [...items.value]
// 按类型筛选(全部/视频/图片)
if (activeTab.value === 'video') {
filtered = filtered.filter(item => item.type === 'video')
} else if (activeTab.value === 'image') {
filtered = filtered.filter(item => item.type === 'image')
}
// 按分类筛选
if (category.value !== 'all') {
const categoryMap = {
'text2video': '文生视频',
'image2video': '图生视频',
'storyboard': '分镜视频',
'reference': '参考图'
}
const targetCategory = categoryMap[category.value]
if (targetCategory) {
filtered = filtered.filter(item => item.category === targetCategory)
}
}
// 按关键词筛选
if (keyword.value) {
const keywordLower = keyword.value.toLowerCase()
filtered = filtered.filter(item =>
item.title.toLowerCase().includes(keywordLower) ||
item.id.includes(keywordLower)
)
}
return filtered
})
const reload = () => {
page.value = 1
hasMore.value = true
loadList()
}
// 筛选变化时的处理
const onFilterChange = () => {
// 筛选是响应式的,不需要额外处理
console.log('筛选条件变化:', { category: category.value, activeTab: activeTab.value })
}
const loadMore = () => {
if (loading.value || !hasMore.value) return
page.value += 1
loadList()
}
const openDetail = (item) => {
selectedItem.value = item
detailDialogVisible.value = true
}
// 获取作品描述
const getDescription = (item) => {
if (item.type === 'video') {
return '影片捕捉了暴风雪中的午夜时分,坐落在积雪覆盖的悬崖顶上的孤立灯塔。相机逐渐放大灯塔的灯光,穿透飞舞的雪花,投射出幽幽的光芒。在白茫茫的环境中,灯塔的黑色轮廓显得格外醒目,呼啸的风声和远处海浪的撞击声增强了孤独的氛围。这一场景展示了灯塔的孤独力量。'
} else {
return '这是一张精美的参考图片,展现了独特的艺术风格和创意构思。图片构图优美,色彩搭配和谐,具有很高的艺术价值和参考意义。'
}
}
// 关闭模态框
const handleClose = () => {
detailDialogVisible.value = false
selectedItem.value = null
activeDetailTab.value = 'detail'
}
// 创建同款
const createSimilar = () => {
ElMessage.info('跳转到创作页面')
}
const download = (item) => {
ElMessage.success(`开始下载:${item.title}`)
}
const share = (item) => {
ElMessageBox.alert('分享链接功能即将上线', '提示')
}
const moreCommand = async (cmd, item) => {
if (cmd === 'download_with_watermark') {
ElMessage.success('开始下载带水印版本')
} else if (cmd === 'download_without_watermark') {
ElMessage.success('开始下载不带水印版本(会员专享)')
} else if (cmd === 'rename') {
ElMessage.info('重命名功能开发中')
} else if (cmd === 'delete') {
try {
await ElMessageBox.confirm('确定删除该作品吗?', '删除确认', {
type: 'warning',
confirmButtonText: '删除',
cancelButtonText: '取消'
})
ElMessage.success('已删除')
} catch (_) {}
}
}
const toggleSelect = (id) => {
const next = new Set(selectedIds.value)
if (next.has(id)) next.delete(id)
else next.add(id)
selectedIds.value = next
}
const bulkDownload = () => {
ElMessage.success(`开始下载 ${selectedIds.value.size} 个文件`)
}
const bulkDelete = async () => {
try {
await ElMessageBox.confirm(`确定删除选中的 ${selectedIds.value.size} 个项目吗?`, '删除确认', {
type: 'warning',
confirmButtonText: '删除',
cancelButtonText: '取消'
})
ElMessage.success('已删除选中项目')
selectedIds.value = new Set()
} catch (_) {}
}
onMounted(() => {
loadList()
})
</script>
<style scoped>
.works-page {
padding: 16px 20px;
}
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 0 4px;
}
.seg-control { margin-left: 2px; }
:deep(.seg-control .el-radio-button__inner) {
height: 36px;
line-height: 36px;
padding: 0 18px;
font-size: 14px;
background-color: #0a0a0a; /* 与页面背景相同 */
color: #cbd5e1;
border-color: #2a2a2a;
}
:deep(.seg-control .el-radio-button:first-child .el-radio-button__inner) { border-top-left-radius: 8px; border-bottom-left-radius: 8px; }
:deep(.seg-control .el-radio-button:last-child .el-radio-button__inner) { border-top-right-radius: 8px; border-bottom-right-radius: 8px; }
:deep(.seg-control .el-radio-button.is-active .el-radio-button__inner) {
background-color: #23262b; /* 比背景略亮,保持可区分 */
color: #ffffff;
border-color: #3a3a3a;
}
.filters { margin-left: 10px; }
.filters-bar {
display:flex;
align-items:center;
justify-content: space-between;
padding: 4px 0 2px;
/* 覆盖 Element Plus 变量,确保与页面背景一致 */
--el-input-bg-color: #0a0a0a;
--el-fill-color-blank: #0a0a0a;
--el-border-color: #2a2a2a;
--el-text-color-regular: #cbd5e1;
}
:deep(.filters .el-select .el-input__wrapper),
:deep(.filters .el-date-editor.el-input__wrapper),
:deep(.filters .el-input__wrapper) {
background-color: #0a0a0a; /* 与页面背景相同 */
border-color: #2a2a2a;
box-shadow: none;
}
:deep(.filters .el-input__wrapper.is-focus) { border-color: #3a3a3a; box-shadow: none; }
:deep(.filters .el-input__inner) { color: #cbd5e1; }
:deep(.filters .el-input__suffix) { color: #cbd5e1; }
.select-row { padding: 4px 0 8px; }
.works-grid { margin-top: 12px; }
.work-card { margin-bottom: 14px; }
.thumb { position: relative; width: 100%; padding-top: 56.25%; overflow: hidden; border-radius: 6px; cursor: pointer; }
.thumb img { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; }
.checker { position: absolute; left: 6px; top: 6px; }
.actions { position: absolute; right: 6px; top: 6px; display: flex; gap: 4px; opacity: 0; transition: opacity .2s ease; }
.thumb:hover .actions { opacity: 1; }
.work-card.selected .thumb::after {
content: '';
position: absolute;
inset: 0;
border: 2px solid #409eff;
border-radius: 6px;
box-shadow: 0 0 0 2px rgba(64,158,255,0.15) inset;
}
.meta { margin-top: 10px; }
.title { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sub { color: #909399; font-size: 12px; margin-top: 4px; }
.finished { text-align: center; color: #909399; margin: 14px 0 4px; font-size: 12px; }
/* 让卡片与页面背景一致 */
:deep(.work-card.el-card) {
background-color: #0a0a0a;
border-color: #1f2937;
color: #e5e7eb;
}
:deep(.work-card .el-card__body) {
background-color: #0a0a0a;
}
:deep(.work-card .el-card__footer) {
background-color: #0a0a0a;
border-top: 1px solid #1f2937;
}
/* 模态框样式 */
:deep(.detail-dialog .el-dialog) {
background: #0a0a0a !important;
border-radius: 12px;
border: 1px solid #333;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.8);
}
:deep(.detail-dialog .el-dialog__header) {
background: #0a0a0a !important;
border-bottom: 1px solid #333;
padding: 16px 20px;
}
:deep(.detail-dialog .el-dialog__title) {
color: #fff !important;
font-size: 18px;
font-weight: 600;
}
:deep(.detail-dialog .el-dialog__headerbtn) {
color: #fff !important;
}
:deep(.detail-dialog .el-dialog__body) {
background: #0a0a0a !important;
padding: 0 !important;
}
:deep(.detail-dialog .el-overlay) {
background-color: rgba(0, 0, 0, 0.8) !important;
}
/* 强制覆盖Element Plus默认样式 */
:deep(.el-dialog) {
background: #0a0a0a !important;
}
:deep(.el-dialog__wrapper) {
background-color: rgba(0, 0, 0, 0.8) !important;
}
.detail-content {
display: flex;
height: 50vh;
background: #0a0a0a;
}
.detail-left {
flex: 2;
display: flex;
align-items: center;
justify-content: center;
background: #000;
position: relative;
}
.video-container {
position: relative;
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
}
.detail-video, .detail-image {
width: 100%;
height: 100%;
object-fit: contain;
background: #000;
}
.video-overlay {
position: absolute;
bottom: 80px;
left: 20px;
z-index: 10;
}
.overlay-text {
font-family: 'Brush Script MT', cursive;
font-size: 24px;
color: #8b5cf6;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
font-weight: bold;
}
.detail-right {
flex: 1;
background: #0a0a0a;
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
overflow-y: auto;
}
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.user-info {
display: flex;
align-items: center;
gap: 12px;
}
.avatar {
width: 40px;
height: 40px;
background: #409eff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 18px;
}
.username {
font-size: 16px;
font-weight: 500;
color: #fff;
}
.tabs {
display: flex;
gap: 0;
}
.tab {
padding: 8px 16px;
background: transparent;
color: #9ca3af;
cursor: pointer;
border-radius: 6px;
transition: all 0.3s;
font-size: 14px;
}
.tab.active {
background: #409eff;
color: #fff;
}
.tab:hover:not(.active) {
background: rgba(255, 255, 255, 0.1);
color: #fff;
}
.description-section {
display: flex;
flex-direction: column;
gap: 12px;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #fff;
margin: 0;
}
.description-text {
font-size: 14px;
line-height: 1.6;
color: #d1d5db;
margin: 0;
}
/* 参考图特殊内容样式 */
.reference-content {
display: flex;
flex-direction: column;
gap: 16px;
}
.input-details-section {
display: flex;
flex-direction: column;
gap: 12px;
}
.input-images {
display: flex;
gap: 12px;
}
.input-image-item {
flex: 1;
}
.input-thumbnail {
width: 100%;
height: 80px;
object-fit: cover;
border-radius: 6px;
border: 1px solid #333;
}
.metadata-section {
display: flex;
flex-direction: column;
gap: 12px;
}
.metadata-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
}
.label {
font-size: 14px;
color: #9ca3af;
}
.value {
font-size: 14px;
color: #fff;
font-weight: 500;
}
.action-section {
margin-top: auto;
padding-top: 20px;
}
.create-similar-btn {
width: 100%;
background: #409eff;
color: #fff;
border: none;
border-radius: 8px;
padding: 12px 24px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
}
.create-similar-btn:hover {
background: #337ecc;
transform: translateY(-2px);
}
.create-similar-btn:active {
transform: translateY(0);
}
/* 更强制性的样式覆盖 */
:deep(.detail-dialog) {
background: #0a0a0a !important;
}
:deep(.detail-dialog .el-dialog__wrapper) {
background-color: rgba(0, 0, 0, 0.8) !important;
}
:deep(.detail-dialog .el-overlay-dialog) {
background: #0a0a0a !important;
border: none !important;
box-shadow: none !important;
}
/* 全局模态框样式覆盖 */
:deep(.el-dialog__wrapper) {
background-color: rgba(0, 0, 0, 0.8) !important;
}
:deep(.el-overlay) {
background-color: rgba(0, 0, 0, 0.8) !important;
}
</style>

View File

@@ -0,0 +1,381 @@
<template>
<div class="order-create">
<el-page-header @back="$router.go(-1)" content="创建订单">
<template #extra>
<el-button type="primary" @click="handleSubmit" :loading="loading">
<el-icon><Check /></el-icon>
创建订单
</el-button>
</template>
</el-page-header>
<el-card class="form-card">
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="100px"
@submit.prevent="handleSubmit"
>
<el-form-item label="订单类型" prop="orderType">
<el-select v-model="form.orderType" placeholder="请选择订单类型">
<el-option label="AI服务" value="SERVICE" />
<el-option label="AI订阅" value="SUBSCRIPTION" />
<el-option label="数字商品" value="DIGITAL" />
<el-option label="虚拟商品" value="VIRTUAL" />
</el-select>
<div class="field-description">
<el-icon><InfoFilled /></el-icon>
<span>选择您要购买的虚拟商品类型AI服务如AI绘画AI写作AI订阅按月/年付费数字商品如软件电子书虚拟商品如游戏道具虚拟货币</span>
</div>
</el-form-item>
<el-form-item label="货币" prop="currency">
<el-select v-model="form.currency" placeholder="请选择货币">
<el-option label="人民币 (CNY)" value="CNY" />
<el-option label="美元 (USD)" value="USD" />
<el-option label="欧元 (EUR)" value="EUR" />
</el-select>
<div class="field-description">
<el-icon><InfoFilled /></el-icon>
<span>选择支付货币类型系统会根据您选择的货币进行计费和结算</span>
</div>
</el-form-item>
<el-form-item label="订单描述" prop="description">
<el-input
v-model="form.description"
type="textarea"
:rows="3"
placeholder="请详细描述您的订单需求需要AI绘画服务风格为动漫风格尺寸为1024x1024像素"
/>
<div class="field-description">
<el-icon><InfoFilled /></el-icon>
<span>详细描述您的订单需求包括服务要求特殊需求等这将帮助服务提供方更好地理解您的需求</span>
</div>
</el-form-item>
<el-form-item label="联系邮箱" prop="contactEmail">
<el-input
v-model="form.contactEmail"
type="email"
placeholder="请输入联系邮箱(用于接收虚拟商品)"
/>
<div class="field-description">
<el-icon><InfoFilled /></el-icon>
<span>必填项虚拟商品将通过此邮箱发送给您请确保邮箱地址正确且可正常接收邮件</span>
</div>
</el-form-item>
<el-form-item label="联系电话" prop="contactPhone">
<el-input
v-model="form.contactPhone"
placeholder="请输入联系电话(可选)"
/>
<div class="field-description">
<el-icon><InfoFilled /></el-icon>
<span>可选填写用于紧急情况联系或重要通知建议填写以便服务提供方在需要时联系您</span>
</div>
</el-form-item>
<!-- 虚拟商品不需要收货地址 -->
<template v-if="isPhysicalOrder">
<el-form-item label="收货地址" prop="shippingAddress">
<el-input
v-model="form.shippingAddress"
type="textarea"
:rows="3"
placeholder="请输入收货地址"
/>
</el-form-item>
<el-form-item label="账单地址" prop="billingAddress">
<el-input
v-model="form.billingAddress"
type="textarea"
:rows="3"
placeholder="请输入账单地址"
/>
</el-form-item>
</template>
<!-- 订单项 -->
<el-form-item label="虚拟商品">
<div class="field-description">
<el-icon><InfoFilled /></el-icon>
<span>添加您要购买的虚拟商品包括商品名称单价和数量支持添加多个商品</span>
</div>
<div class="order-items">
<div
v-for="(item, index) in form.orderItems"
:key="index"
class="order-item"
>
<el-row :gutter="20">
<el-col :span="8">
<el-input
v-model="item.productName"
placeholder="商品名称AI绘画服务、AI写作助手、AI翻译服务等"
@input="calculateSubtotal(index)"
/>
</el-col>
<el-col :span="4">
<el-input-number
v-model="item.unitPrice"
:precision="2"
:min="0"
placeholder="单价"
@change="calculateSubtotal(index)"
/>
</el-col>
<el-col :span="4">
<el-input-number
v-model="item.quantity"
:min="1"
placeholder="数量"
@change="calculateSubtotal(index)"
/>
</el-col>
<el-col :span="4">
<el-input
v-model="item.subtotal"
readonly
placeholder="小计"
/>
</el-col>
<el-col :span="4">
<el-button
type="danger"
:icon="Delete"
circle
@click="removeItem(index)"
v-if="form.orderItems.length > 1"
/>
</el-col>
</el-row>
</div>
<el-button
type="primary"
:icon="Plus"
@click="addItem"
class="add-item-btn"
>
添加虚拟商品
</el-button>
</div>
</el-form-item>
<el-form-item>
<div class="total-amount">
<span class="total-label">订单总计</span>
<span class="total-value">{{ form.currency }} {{ totalAmount }}</span>
</div>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useOrderStore } from '@/stores/orders'
import { ElMessage } from 'element-plus'
import { Plus, Delete, Check, InfoFilled } from '@element-plus/icons-vue'
const router = useRouter()
const orderStore = useOrderStore()
const formRef = ref()
const loading = ref(false)
const form = reactive({
orderType: 'SERVICE',
currency: 'CNY',
description: '',
contactEmail: '',
contactPhone: '',
shippingAddress: '',
billingAddress: '',
orderItems: [
{
productName: '',
unitPrice: 0,
quantity: 1,
subtotal: 0
}
]
})
const rules = {
orderType: [
{ required: true, message: '请选择订单类型', trigger: 'change' }
],
currency: [
{ required: true, message: '请选择货币', trigger: 'change' }
],
contactEmail: [
{ required: true, message: '请输入联系邮箱', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
]
}
// 检查是否为实体商品订单
const isPhysicalOrder = computed(() => {
return form.orderType === 'PHYSICAL'
})
// 计算总金额
const totalAmount = computed(() => {
return form.orderItems.reduce((total, item) => {
return total + parseFloat(item.subtotal || 0)
}, 0).toFixed(2)
})
// 计算小计
const calculateSubtotal = (index) => {
const item = form.orderItems[index]
if (item.unitPrice && item.quantity) {
item.subtotal = parseFloat((item.unitPrice * item.quantity).toFixed(2))
} else {
item.subtotal = 0
}
}
// 添加商品项
const addItem = () => {
form.orderItems.push({
productName: '',
unitPrice: 0,
quantity: 1,
subtotal: 0
})
}
// 删除商品项
const removeItem = (index) => {
if (form.orderItems.length > 1) {
form.orderItems.splice(index, 1)
}
}
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
try {
const valid = await formRef.value.validate()
if (!valid) return
// 验证订单项
const validItems = form.orderItems.filter(item =>
item.productName && item.unitPrice > 0 && item.quantity > 0
)
if (validItems.length === 0) {
ElMessage.error('请至少添加一个有效虚拟商品')
return
}
loading.value = true
// 准备提交数据
const orderData = {
orderType: form.orderType,
currency: form.currency,
description: form.description,
contactEmail: form.contactEmail,
contactPhone: form.contactPhone,
shippingAddress: form.shippingAddress,
billingAddress: form.billingAddress,
totalAmount: parseFloat(totalAmount.value),
status: 'PENDING', // 新创建的订单状态为待支付
orderItems: validItems.map(item => ({
productName: item.productName,
unitPrice: parseFloat(item.unitPrice),
quantity: parseInt(item.quantity),
subtotal: parseFloat(item.subtotal)
}))
}
const response = await orderStore.createNewOrder(orderData)
if (response.success) {
ElMessage.success('虚拟商品订单创建成功!商品将发送到您的邮箱')
router.push('/orders')
} else {
ElMessage.error(response.message || '创建订单失败')
}
} catch (error) {
console.error('Create order error:', error)
ElMessage.error('创建订单失败')
} finally {
loading.value = false
}
}
</script>
<style scoped>
.order-create {
max-width: 1200px;
margin: 0 auto;
}
.form-card {
margin-top: 20px;
}
.order-items {
width: 100%;
}
.order-item {
margin-bottom: 16px;
padding: 16px;
border: 1px solid #e4e7ed;
border-radius: 8px;
background-color: #fafafa;
}
.add-item-btn {
margin-top: 16px;
}
.total-amount {
text-align: right;
padding: 16px;
background-color: #f5f7fa;
border-radius: 8px;
}
.total-label {
font-size: 16px;
color: #606266;
}
.field-description {
display: flex;
align-items: flex-start;
margin-top: 8px;
padding: 8px 12px;
background-color: #f0f9ff;
border: 1px solid #b3d8ff;
border-radius: 6px;
font-size: 13px;
color: #409eff;
line-height: 1.4;
}
.field-description .el-icon {
margin-right: 6px;
margin-top: 1px;
flex-shrink: 0;
}
.field-description span {
flex: 1;
}
</style>

View File

@@ -0,0 +1,201 @@
<template>
<div class="order-detail">
<el-page-header @back="$router.go(-1)" content="订单详情">
<template #extra>
<el-button-group>
<el-button v-if="order?.canPay()" type="success" @click="handlePayment">
<el-icon><CreditCard /></el-icon>
立即支付
</el-button>
<el-button v-if="order?.canCancel()" type="danger" @click="handleCancel">
<el-icon><Close /></el-icon>
取消订单
</el-button>
</el-button-group>
</template>
</el-page-header>
<el-card v-if="order" class="order-card">
<template #header>
<div class="order-header">
<h3>订单信息</h3>
<el-tag :type="getStatusType(order.status)">
{{ getStatusText(order.status) }}
</el-tag>
</div>
</template>
<el-descriptions :column="2" border>
<el-descriptions-item label="订单号">{{ order.orderNumber }}</el-descriptions-item>
<el-descriptions-item label="订单类型">{{ getOrderTypeText(order.orderType) }}</el-descriptions-item>
<el-descriptions-item label="订单金额">
<span class="amount">{{ order.currency }} {{ order.totalAmount }}</span>
</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ formatDate(order.createdAt) }}</el-descriptions-item>
<el-descriptions-item label="联系邮箱" v-if="order.contactEmail">{{ order.contactEmail }}</el-descriptions-item>
<el-descriptions-item label="联系电话" v-if="order.contactPhone">{{ order.contactPhone }}</el-descriptions-item>
</el-descriptions>
<div v-if="order.description" class="order-description">
<h4>订单描述</h4>
<p>{{ order.description }}</p>
</div>
<div v-if="order.orderItems && order.orderItems.length > 0" class="order-items">
<h4>订单商品</h4>
<el-table :data="order.orderItems" border>
<el-table-column prop="productName" label="商品名称" />
<el-table-column prop="unitPrice" label="单价" width="120">
<template #default="{ row }">
{{ order.currency }} {{ row.unitPrice }}
</template>
</el-table-column>
<el-table-column prop="quantity" label="数量" width="80" />
<el-table-column prop="subtotal" label="小计" width="120">
<template #default="{ row }">
{{ order.currency }} {{ row.subtotal }}
</template>
</el-table-column>
</el-table>
</div>
</el-card>
<el-empty v-else description="订单不存在" />
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { useOrderStore } from '@/stores/orders'
import { ElMessage } from 'element-plus'
const route = useRoute()
const orderStore = useOrderStore()
const order = ref(null)
// 获取状态类型
const getStatusType = (status) => {
const statusMap = {
'PENDING': 'warning',
'CONFIRMED': 'info',
'PAID': 'primary',
'PROCESSING': '',
'SHIPPED': 'success',
'DELIVERED': 'success',
'COMPLETED': 'success',
'CANCELLED': 'danger',
'REFUNDED': 'info'
}
return statusMap[status] || ''
}
// 获取状态文本
const getStatusText = (status) => {
const statusMap = {
'PENDING': '待支付',
'CONFIRMED': '已确认',
'PAID': '已支付',
'PROCESSING': '处理中',
'SHIPPED': '已发货',
'DELIVERED': '已送达',
'COMPLETED': '已完成',
'CANCELLED': '已取消',
'REFUNDED': '已退款'
}
return statusMap[status] || status
}
// 获取订单类型文本
const getOrderTypeText = (orderType) => {
const typeMap = {
'PRODUCT': '商品订单',
'SERVICE': '服务订单',
'SUBSCRIPTION': '订阅订单',
'DIGITAL': '数字商品',
'PHYSICAL': '实体商品'
}
return typeMap[orderType] || orderType
}
// 格式化日期
const formatDate = (dateString) => {
const date = new Date(dateString)
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
}
// 处理支付
const handlePayment = () => {
ElMessage.info('支付功能开发中')
}
// 处理取消
const handleCancel = () => {
ElMessage.info('取消订单功能开发中')
}
onMounted(async () => {
const orderId = route.params.id
if (orderId) {
const response = await orderStore.fetchOrderById(orderId)
if (response.success) {
order.value = orderStore.currentOrder
} else {
ElMessage.error(response.message || '获取订单详情失败')
}
}
})
</script>
<style scoped>
.order-detail {
max-width: 1200px;
margin: 0 auto;
}
.order-card {
margin-top: 20px;
}
.order-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.order-header h3 {
margin: 0;
}
.amount {
font-weight: 600;
color: #E6A23C;
}
.order-description,
.order-items {
margin-top: 20px;
}
.order-description h4,
.order-items h4 {
margin-bottom: 12px;
color: #303133;
}
</style>

View File

@@ -0,0 +1,525 @@
<template>
<div class="orders">
<!-- 页面标题和操作 -->
<div class="page-header">
<div class="page-title">
<h2>
<el-icon><List /></el-icon>
订单管理
</h2>
</div>
<div class="page-actions">
<el-button type="primary" @click="$router.push('/orders/create')">
<el-icon><Plus /></el-icon>
创建订单
</el-button>
</div>
</div>
<!-- 筛选和搜索 -->
<el-card class="filter-card">
<el-row :gutter="20">
<el-col :xs="24" :sm="12" :md="8">
<el-select
v-model="filters.status"
placeholder="选择订单状态"
clearable
@change="handleFilterChange"
>
<el-option
v-for="status in orderStatuses"
:key="status.value"
:label="status.label"
:value="status.value"
/>
</el-select>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-input
v-model="filters.search"
placeholder="搜索订单号"
clearable
@input="handleSearch"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-button @click="resetFilters">重置筛选</el-button>
</el-col>
</el-row>
</el-card>
<!-- 订单列表 -->
<el-card class="orders-card">
<el-table
:data="orderStore.orders"
v-loading="orderStore.loading"
empty-text="暂无订单"
@sort-change="handleSortChange"
>
<el-table-column prop="orderNumber" label="订单号" width="150" sortable="custom">
<template #default="{ row }">
<router-link :to="`/orders/${row.id}`" class="order-link">
{{ row.orderNumber }}
</router-link>
</template>
</el-table-column>
<el-table-column prop="totalAmount" label="金额" width="120" sortable="custom">
<template #default="{ row }">
<span class="amount">{{ row.currency }} {{ row.totalAmount }}</span>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="120">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">
{{ getStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="orderType" label="类型" width="120">
<template #default="{ row }">
{{ getOrderTypeText(row.orderType) }}
</template>
</el-table-column>
<el-table-column prop="createdAt" label="创建时间" width="160" sortable="custom">
<template #default="{ row }">
{{ formatDate(row.createdAt) }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button-group>
<el-button size="small" @click="$router.push(`/orders/${row.id}`)">
查看
</el-button>
<el-dropdown v-if="canPay(row)" trigger="click" :teleported="true" popper-class="table-dropdown" @command="(command) => handlePayment(row, command)">
<el-button size="small" type="success">
支付<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="ALIPAY">
<el-icon><CreditCard /></el-icon>
支付宝支付
</el-dropdown-item>
<el-dropdown-item command="PAYPAL">
<el-icon><CreditCard /></el-icon>
PayPal支付
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-button
v-if="canCancel(row)"
size="small"
type="danger"
@click="handleCancel(row)"
>
取消
</el-button>
</el-button-group>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.size"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
<!-- 取消订单对话框 -->
<el-dialog
v-model="cancelDialogVisible"
title="取消订单"
width="400px"
>
<el-form :model="cancelForm" label-width="80px">
<el-form-item label="取消原因">
<el-input
v-model="cancelForm.reason"
type="textarea"
:rows="3"
placeholder="请输入取消原因(可选)"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="cancelDialogVisible = false">取消</el-button>
<el-button type="danger" @click="confirmCancel">确认取消</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { useOrderStore } from '@/stores/orders'
import { createOrderPayment } from '@/api/orders'
import { ElMessage, ElMessageBox } from 'element-plus'
const orderStore = useOrderStore()
// 筛选条件
const filters = reactive({
status: '',
search: ''
})
// 分页信息
const pagination = reactive({
page: 1,
size: 10,
total: 0
})
// 排序
const sortBy = ref('createdAt')
const sortDir = ref('desc')
// 取消订单对话框
const cancelDialogVisible = ref(false)
const cancelForm = reactive({
reason: ''
})
const currentCancelOrder = ref(null)
// 订单状态选项
const orderStatuses = [
{ value: '', label: '全部状态' },
{ value: 'PENDING', label: '待支付' },
{ value: 'CONFIRMED', label: '已确认' },
{ value: 'PAID', label: '已支付' },
{ value: 'PROCESSING', label: '处理中' },
{ value: 'SHIPPED', label: '已发货' },
{ value: 'DELIVERED', label: '已送达' },
{ value: 'COMPLETED', label: '已完成' },
{ value: 'CANCELLED', label: '已取消' },
{ value: 'REFUNDED', label: '已退款' }
]
// 获取状态类型
const getStatusType = (status) => {
const statusMap = {
'PENDING': 'warning',
'CONFIRMED': 'info',
'PAID': 'primary',
'PROCESSING': '',
'SHIPPED': 'success',
'DELIVERED': 'success',
'COMPLETED': 'success',
'CANCELLED': 'danger',
'REFUNDED': 'info'
}
return statusMap[status] || ''
}
// 获取状态文本
const getStatusText = (status) => {
const statusMap = {
'PENDING': '待支付',
'CONFIRMED': '已确认',
'PAID': '已支付',
'PROCESSING': '处理中',
'SHIPPED': '已发货',
'DELIVERED': '已送达',
'COMPLETED': '已完成',
'CANCELLED': '已取消',
'REFUNDED': '已退款'
}
return statusMap[status] || status
}
// 获取订单类型文本
const getOrderTypeText = (orderType) => {
const typeMap = {
'PRODUCT': '商品订单',
'SERVICE': '服务订单',
'SUBSCRIPTION': '订阅订单',
'DIGITAL': '数字商品',
'PHYSICAL': '实体商品'
}
return typeMap[orderType] || orderType
}
// 格式化日期
const formatDate = (dateString) => {
const date = new Date(dateString)
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
}
// 检查是否可以支付
const canPay = (order) => {
return order.status === 'PENDING' || order.status === 'CONFIRMED'
}
// 检查是否可以取消
const canCancel = (order) => {
return order.status === 'PENDING' || order.status === 'CONFIRMED'
}
// 获取订单列表
const fetchOrders = async () => {
console.log('=== 开始获取订单列表 ===')
console.log('当前用户:', userStore.user)
console.log('认证状态:', userStore.isAuthenticated)
console.log('Token:', sessionStorage.getItem('token'))
const params = {
page: pagination.page - 1,
size: pagination.size,
sortBy: sortBy.value,
sortDir: sortDir.value
}
if (filters.status) {
params.status = filters.status
}
if (filters.search) {
params.search = filters.search
}
console.log('请求参数:', params)
try {
const response = await orderStore.fetchOrders(params)
console.log('API响应:', response)
if (response.success) {
pagination.total = orderStore.pagination.total
console.log('订单数据:', orderStore.orders)
console.log('分页信息:', orderStore.pagination)
} else {
console.error('获取订单失败:', response.message)
}
} catch (error) {
console.error('获取订单异常:', error)
}
}
// 筛选变化
const handleFilterChange = () => {
pagination.page = 1
fetchOrders()
}
// 搜索
const handleSearch = () => {
pagination.page = 1
fetchOrders()
}
// 重置筛选
const resetFilters = () => {
filters.status = ''
filters.search = ''
pagination.page = 1
fetchOrders()
}
// 排序变化
const handleSortChange = ({ prop, order }) => {
if (prop) {
sortBy.value = prop
sortDir.value = order === 'ascending' ? 'asc' : 'desc'
fetchOrders()
}
}
// 分页大小变化
const handleSizeChange = (size) => {
pagination.size = size
pagination.page = 1
fetchOrders()
}
// 当前页变化
const handleCurrentChange = (page) => {
pagination.page = page
fetchOrders()
}
// 处理支付
const handlePayment = async (order, paymentMethod) => {
try {
const response = await createOrderPayment(order.id, paymentMethod)
if (response.success) {
ElMessage.success('正在跳转到支付页面...')
// 这里应该跳转到支付页面
window.open(response.data.paymentUrl, '_blank')
} else {
ElMessage.error(response.message || '创建支付失败')
}
} catch (error) {
console.error('Payment error:', error)
ElMessage.error('创建支付失败')
}
}
// 处理取消
const handleCancel = (order) => {
currentCancelOrder.value = order
cancelForm.reason = ''
cancelDialogVisible.value = true
}
// 确认取消
const confirmCancel = async () => {
if (!currentCancelOrder.value) return
try {
const response = await orderStore.cancelOrderById(
currentCancelOrder.value.id,
cancelForm.reason
)
if (response.success) {
ElMessage.success('订单取消成功')
cancelDialogVisible.value = false
fetchOrders()
} else {
ElMessage.error(response.message || '取消订单失败')
}
} catch (error) {
console.error('Cancel order error:', error)
ElMessage.error('取消订单失败')
}
}
onMounted(() => {
fetchOrders()
})
</script>
<style scoped>
.orders {
max-width: 1200px;
margin: 0 auto;
min-height: 100vh;
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
position: relative;
overflow-x: hidden;
padding: 20px;
}
/* 页面特殊效果 */
.orders::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background:
radial-gradient(circle at 30% 20%, rgba(255, 255, 255, 0.1) 0%, transparent 50%),
radial-gradient(circle at 70% 80%, rgba(255, 255, 255, 0.05) 0%, transparent 50%);
animation: ordersPulse 5s ease-in-out infinite alternate;
pointer-events: none;
z-index: 1;
}
@keyframes ordersPulse {
0% { opacity: 0.3; transform: scale(1); }
100% { opacity: 0.6; transform: scale(1.02); }
}
/* 内容层级 */
.orders > * {
position: relative;
z-index: 2;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.page-title h2 {
margin: 0;
color: #303133;
display: flex;
align-items: center;
gap: 8px;
}
.page-actions {
display: flex;
gap: 12px;
}
.filter-card {
margin-bottom: 20px;
}
.orders-card {
margin-bottom: 20px;
}
.order-link {
color: #409EFF;
text-decoration: none;
font-weight: 500;
}
.order-link:hover {
text-decoration: underline;
}
.amount {
font-weight: 600;
color: #E6A23C;
}
.pagination-container {
display: flex;
justify-content: center;
margin-top: 20px;
}
/* 确保表格内下拉菜单不被裁剪/遮挡 */
:deep(.table-dropdown) {
z-index: 3000 !important;
}
@media (max-width: 768px) {
.page-header {
flex-direction: column;
gap: 16px;
align-items: stretch;
}
.page-actions {
justify-content: center;
}
}
</style>

View File

@@ -0,0 +1,254 @@
<template>
<div class="payment-create">
<el-page-header @back="$router.go(-1)" content="创建支付">
<template #extra>
<el-button type="primary" @click="handleSubmit" :loading="loading">
<el-icon><CreditCard /></el-icon>
创建支付
</el-button>
</template>
</el-page-header>
<el-card class="form-card">
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="100px"
@submit.prevent="handleSubmit"
>
<el-form-item label="订单号" prop="orderId">
<el-input
v-model="form.orderId"
placeholder="请输入订单号"
clearable
/>
</el-form-item>
<el-form-item label="支付金额" prop="amount">
<el-input-number
v-model="form.amount"
:precision="2"
:min="0.01"
placeholder="请输入支付金额"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="货币" prop="currency">
<el-select v-model="form.currency" placeholder="请选择货币">
<el-option label="人民币 (CNY)" value="CNY" />
<el-option label="美元 (USD)" value="USD" />
<el-option label="欧元 (EUR)" value="EUR" />
</el-select>
</el-form-item>
<el-form-item label="支付方式" prop="paymentMethod">
<el-radio-group v-model="form.paymentMethod">
<el-radio value="ALIPAY">
<el-icon><CreditCard /></el-icon>
支付宝
</el-radio>
<el-radio value="PAYPAL">
<el-icon><CreditCard /></el-icon>
PayPal
</el-radio>
<el-radio value="WECHAT">
<el-icon><CreditCard /></el-icon>
微信支付
</el-radio>
<el-radio value="UNIONPAY">
<el-icon><CreditCard /></el-icon>
银联支付
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="支付描述" prop="description">
<el-input
v-model="form.description"
type="textarea"
:rows="3"
placeholder="请输入支付描述"
/>
</el-form-item>
<el-form-item label="回调URL" prop="callbackUrl">
<el-input
v-model="form.callbackUrl"
placeholder="请输入回调URL可选"
/>
</el-form-item>
<el-form-item label="返回URL" prop="returnUrl">
<el-input
v-model="form.returnUrl"
placeholder="请输入返回URL可选"
/>
</el-form-item>
</el-form>
</el-card>
<!-- 支付方式说明 -->
<el-card class="info-card">
<template #header>
<h4>支付方式说明</h4>
</template>
<el-row :gutter="20">
<el-col :xs="24" :sm="12" :md="6">
<div class="payment-method-info">
<el-icon size="32" color="#1677FF"><CreditCard /></el-icon>
<h5>支付宝</h5>
<p>支持支付宝扫码支付和网页支付</p>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<div class="payment-method-info">
<el-icon size="32" color="#0070BA"><CreditCard /></el-icon>
<h5>PayPal</h5>
<p>支持PayPal账户支付和信用卡支付</p>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<div class="payment-method-info">
<el-icon size="32" color="#07C160"><CreditCard /></el-icon>
<h5>微信支付</h5>
<p>支持微信扫码支付和H5支付</p>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<div class="payment-method-info">
<el-icon size="32" color="#E6A23C"><CreditCard /></el-icon>
<h5>银联支付</h5>
<p>支持银联卡支付和网银支付</p>
</div>
</el-col>
</el-row>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
const router = useRouter()
const formRef = ref()
const loading = ref(false)
const form = reactive({
orderId: '',
amount: 0,
currency: 'CNY',
paymentMethod: 'ALIPAY',
description: '',
callbackUrl: '',
returnUrl: ''
})
const rules = {
orderId: [
{ required: true, message: '请输入订单号', trigger: 'blur' }
],
amount: [
{ required: true, message: '请输入支付金额', trigger: 'blur' },
{ type: 'number', min: 0.01, message: '支付金额必须大于0', trigger: 'blur' }
],
currency: [
{ required: true, message: '请选择货币', trigger: 'change' }
],
paymentMethod: [
{ required: true, message: '请选择支付方式', trigger: 'change' }
]
}
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
try {
const valid = await formRef.value.validate()
if (!valid) return
loading.value = true
// 模拟创建支付
await new Promise(resolve => setTimeout(resolve, 1000))
ElMessage.success('支付创建成功')
router.push('/payments')
} catch (error) {
console.error('Create payment error:', error)
ElMessage.error('创建支付失败')
} finally {
loading.value = false
}
}
</script>
<style scoped>
.payment-create {
max-width: 1200px;
margin: 0 auto;
}
.form-card {
margin-top: 20px;
}
.info-card {
margin-top: 20px;
}
.info-card h4 {
margin: 0;
color: #303133;
}
.payment-method-info {
text-align: center;
padding: 20px;
border: 1px solid #e4e7ed;
border-radius: 8px;
background-color: #fafafa;
transition: all 0.3s;
}
.payment-method-info:hover {
background-color: #f5f7fa;
border-color: #409EFF;
}
.payment-method-info h5 {
margin: 12px 0 8px 0;
color: #303133;
}
.payment-method-info p {
margin: 0;
color: #606266;
font-size: 14px;
line-height: 1.5;
}
@media (max-width: 768px) {
.payment-method-info {
margin-bottom: 16px;
}
}
</style>

View File

@@ -0,0 +1,693 @@
<template>
<div class="payments">
<!-- 页面标题 -->
<div class="page-header">
<h2>
<el-icon><CreditCard /></el-icon>
支付记录
</h2>
</div>
<!-- 筛选和搜索 -->
<el-card class="filter-card">
<el-row :gutter="20">
<el-col :xs="24" :sm="12" :md="8">
<el-select
v-model="filters.status"
placeholder="选择支付状态"
clearable
@change="handleFilterChange"
>
<el-option label="全部状态" value="" />
<el-option label="待支付" value="PENDING" />
<el-option label="支付成功" value="SUCCESS" />
<el-option label="支付失败" value="FAILED" />
<el-option label="已取消" value="CANCELLED" />
</el-select>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-input
v-model="filters.search"
placeholder="搜索订单号"
clearable
@input="handleSearch"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-button @click="resetFilters">重置筛选</el-button>
<el-button type="success" @click="showSubscriptionDialog('standard')">标准版订阅</el-button>
<el-button type="warning" @click="showSubscriptionDialog('professional')">专业版订阅</el-button>
</el-col>
</el-row>
</el-card>
<!-- 支付记录列表 -->
<el-card class="payments-card">
<el-table
:data="payments"
v-loading="loading"
empty-text="暂无支付记录"
>
<el-table-column prop="orderId" label="订单号" width="150">
<template #default="{ row }">
<router-link :to="`/orders/${row.orderId}`" class="order-link">
{{ row.orderId }}
</router-link>
</template>
</el-table-column>
<el-table-column prop="amount" label="金额" width="120">
<template #default="{ row }">
<span class="amount">{{ row.currency }} {{ row.amount }}</span>
</template>
</el-table-column>
<el-table-column prop="paymentMethod" label="支付方式" width="120">
<template #default="{ row }">
<el-tag :type="getPaymentMethodType(row.paymentMethod)">
{{ getPaymentMethodText(row.paymentMethod) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="120">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">
{{ getStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="description" label="描述" min-width="200">
<template #default="{ row }">
<span class="description">{{ row.description }}</span>
</template>
</el-table-column>
<el-table-column prop="createdAt" label="创建时间" width="160">
<template #default="{ row }">
{{ formatDate(row.createdAt) }}
</template>
</el-table-column>
<el-table-column prop="paidAt" label="支付时间" width="160">
<template #default="{ row }">
{{ row.paidAt ? formatDate(row.paidAt) : '-' }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button
size="small"
@click="viewPaymentDetail(row)"
>
查看详情
</el-button>
<el-button
v-if="row.status === 'PENDING'"
size="small"
type="success"
@click="testPaymentComplete(row)"
>
测试完成
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.size"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
<!-- 支付详情对话框 -->
<el-dialog
v-model="detailDialogVisible"
title="支付详情"
width="600px"
>
<div v-if="currentPayment">
<el-descriptions :column="2" border>
<el-descriptions-item label="订单号">{{ currentPayment.orderId }}</el-descriptions-item>
<el-descriptions-item label="支付方式">
<el-tag :type="getPaymentMethodType(currentPayment.paymentMethod)">
{{ getPaymentMethodText(currentPayment.paymentMethod) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="支付金额">
<span class="amount">{{ currentPayment.currency }} {{ currentPayment.amount }}</span>
</el-descriptions-item>
<el-descriptions-item label="支付状态">
<el-tag :type="getStatusType(currentPayment.status)">
{{ getStatusText(currentPayment.status) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="外部交易ID" v-if="currentPayment.externalTransactionId">
{{ currentPayment.externalTransactionId }}
</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ formatDate(currentPayment.createdAt) }}</el-descriptions-item>
<el-descriptions-item label="支付时间" v-if="currentPayment.paidAt">
{{ formatDate(currentPayment.paidAt) }}
</el-descriptions-item>
<el-descriptions-item label="更新时间">{{ formatDate(currentPayment.updatedAt) }}</el-descriptions-item>
</el-descriptions>
<div v-if="currentPayment.description" class="payment-description">
<h4>支付描述</h4>
<p>{{ currentPayment.description }}</p>
</div>
</div>
</el-dialog>
<!-- 订阅对话框 -->
<el-dialog
v-model="subscriptionDialogVisible"
:title="subscriptionDialogTitle"
width="500px"
>
<div class="subscription-info">
<h3>{{ subscriptionInfo.title }}</h3>
<p class="price">${{ subscriptionInfo.price }}</p>
<p class="description">{{ subscriptionInfo.description }}</p>
<div class="benefits">
<h4>包含功能</h4>
<ul>
<li v-for="benefit in subscriptionInfo.benefits" :key="benefit">
{{ benefit }}
</li>
</ul>
</div>
<div class="points-info">
<el-tag type="success">支付完成后可获得 {{ subscriptionInfo.points }} 积分</el-tag>
</div>
<div class="payment-method">
<h4>选择支付方式</h4>
<el-radio-group v-model="selectedPaymentMethod" @change="updatePrice">
<el-radio label="ALIPAY">支付宝</el-radio>
<el-radio label="PAYPAL">PayPal</el-radio>
</el-radio-group>
<div class="converted-price" v-if="convertedPrice">
<p>支付金额<span class="price-display">{{ convertedPrice }}</span></p>
</div>
</div>
</div>
<template #footer>
<el-button @click="subscriptionDialogVisible = false">取消</el-button>
<el-button type="primary" @click="createSubscription" :loading="subscriptionLoading">
立即订阅
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getPayments, testPaymentComplete as testPaymentCompleteApi, createTestPayment } from '@/api/payments'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const loading = ref(false)
const payments = ref([])
// 筛选条件
const filters = reactive({
status: '',
search: ''
})
// 分页信息
const pagination = reactive({
page: 1,
size: 10,
total: 0
})
// 支付详情对话框
const detailDialogVisible = ref(false)
const currentPayment = ref(null)
// 订阅对话框
const subscriptionDialogVisible = ref(false)
const subscriptionLoading = ref(false)
const subscriptionType = ref('')
const selectedPaymentMethod = ref('ALIPAY')
const convertedPrice = ref('')
const exchangeRate = ref(7.2) // 美元对人民币汇率,可以根据实际情况调整
const subscriptionInfo = reactive({
title: '',
price: 0,
description: '',
benefits: [],
points: 0
})
// 计算属性
const subscriptionDialogTitle = computed(() => {
return subscriptionType.value === 'standard' ? '标准版订阅' : '专业版订阅'
})
// 获取支付方式类型
const getPaymentMethodType = (method) => {
const methodMap = {
'ALIPAY': 'primary',
'PAYPAL': 'success',
'WECHAT': 'success',
'UNIONPAY': 'warning'
}
return methodMap[method] || ''
}
// 获取支付方式文本
const getPaymentMethodText = (method) => {
const methodMap = {
'ALIPAY': '支付宝',
'PAYPAL': 'PayPal',
'WECHAT': '微信支付',
'UNIONPAY': '银联支付'
}
return methodMap[method] || method
}
// 获取状态类型
const getStatusType = (status) => {
const statusMap = {
'PENDING': 'warning',
'SUCCESS': 'success',
'FAILED': 'danger',
'CANCELLED': 'info'
}
return statusMap[status] || ''
}
// 获取状态文本
const getStatusText = (status) => {
const statusMap = {
'PENDING': '待支付',
'SUCCESS': '支付成功',
'FAILED': '支付失败',
'CANCELLED': '已取消'
}
return statusMap[status] || status
}
// 格式化日期
const formatDate = (dateString) => {
const date = new Date(dateString)
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
}
// 获取支付记录列表
const fetchPayments = async () => {
try {
loading.value = true
const response = await getPayments({
page: pagination.page - 1,
size: pagination.size,
status: filters.status,
search: filters.search
})
if (response.success) {
payments.value = response.data
pagination.total = response.total || response.data.length
} else {
ElMessage.error(response.message || '获取支付记录失败')
}
} catch (error) {
console.error('Fetch payments error:', error)
ElMessage.error('获取支付记录失败')
} finally {
loading.value = false
}
}
// 筛选变化
const handleFilterChange = () => {
pagination.page = 1
fetchPayments()
}
// 搜索
const handleSearch = () => {
pagination.page = 1
fetchPayments()
}
// 重置筛选
const resetFilters = () => {
filters.status = ''
filters.search = ''
pagination.page = 1
fetchPayments()
}
// 分页大小变化
const handleSizeChange = (size) => {
pagination.size = size
pagination.page = 1
fetchPayments()
}
// 当前页变化
const handleCurrentChange = (page) => {
pagination.page = page
fetchPayments()
}
// 查看支付详情
const viewPaymentDetail = (payment) => {
currentPayment.value = payment
detailDialogVisible.value = true
}
// 更新价格显示
const updatePrice = () => {
if (selectedPaymentMethod.value === 'ALIPAY') {
// 支付宝使用人民币
const cnyPrice = (subscriptionInfo.price * exchangeRate.value).toFixed(2)
convertedPrice.value = `¥${cnyPrice}`
} else if (selectedPaymentMethod.value === 'PAYPAL') {
// PayPal使用美元
convertedPrice.value = `$${subscriptionInfo.price}`
}
}
// 显示订阅对话框
const showSubscriptionDialog = (type) => {
if (!userStore.isAuthenticated) {
ElMessage.warning('请先登录后再订阅')
return
}
subscriptionType.value = type
if (type === 'standard') {
subscriptionInfo.title = '标准版订阅'
subscriptionInfo.price = 59
subscriptionInfo.description = '适合个人用户的基础功能订阅'
subscriptionInfo.benefits = [
'基础AI功能使用',
'每月100次API调用',
'邮件技术支持',
'基础模板库访问'
]
subscriptionInfo.points = 200
} else if (type === 'professional') {
subscriptionInfo.title = '专业版订阅'
subscriptionInfo.price = 259
subscriptionInfo.description = '适合企业用户的高级功能订阅'
subscriptionInfo.benefits = [
'高级AI功能使用',
'每月1000次API调用',
'优先技术支持',
'完整模板库访问',
'API接口集成',
'数据分析报告'
]
subscriptionInfo.points = 1000
}
subscriptionDialogVisible.value = true
// 初始化价格显示
updatePrice()
}
// 创建订阅支付
const createSubscription = async () => {
try {
subscriptionLoading.value = true
// 根据支付方式确定实际支付金额
let actualAmount
if (selectedPaymentMethod.value === 'ALIPAY') {
// 支付宝使用人民币
actualAmount = (subscriptionInfo.price * exchangeRate.value).toFixed(2)
} else {
// PayPal使用美元
actualAmount = subscriptionInfo.price.toString()
}
const response = await createTestPayment({
amount: actualAmount,
method: selectedPaymentMethod.value
})
if (response.success) {
ElMessage.success(`${subscriptionInfo.title}支付记录创建成功`)
// 根据支付方式调用相应的支付接口
if (selectedPaymentMethod.value === 'ALIPAY') {
try {
const alipayResponse = await createAlipayPayment({
paymentId: response.data.id
})
if (alipayResponse.success) {
// 跳转到支付宝支付页面
window.open(alipayResponse.data.paymentUrl, '_blank')
ElMessage.success('正在跳转到支付宝支付页面')
} else {
ElMessage.error(alipayResponse.message || '创建支付宝支付失败')
}
} catch (error) {
console.error('创建支付宝支付失败:', error)
ElMessage.error('创建支付宝支付失败')
}
} else if (selectedPaymentMethod.value === 'PAYPAL') {
try {
const paypalResponse = await createPayPalPayment({
paymentId: response.data.id
})
if (paypalResponse.success) {
// 跳转到PayPal支付页面
window.open(paypalResponse.data.paymentUrl, '_blank')
ElMessage.success('正在跳转到PayPal支付页面')
} else {
ElMessage.error(paypalResponse.message || '创建PayPal支付失败')
}
} catch (error) {
console.error('创建PayPal支付失败:', error)
ElMessage.error('创建PayPal支付失败')
}
}
subscriptionDialogVisible.value = false
// 刷新支付记录列表
fetchPayments()
} else {
ElMessage.error(response.message || '创建订阅支付记录失败')
}
} catch (error) {
console.error('Create subscription error:', error)
ElMessage.error('创建订阅支付记录失败')
} finally {
subscriptionLoading.value = false
}
}
// 测试支付完成
const testPaymentComplete = async (payment) => {
try {
await ElMessageBox.confirm(
`确定要测试完成支付 ${payment.orderId} 吗?这将自动创建订单。`,
'确认测试',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
const response = await testPaymentCompleteApi(payment.id)
if (response.success) {
ElMessage.success('支付完成测试成功,订单已自动创建')
// 刷新支付记录列表
fetchPayments()
} else {
ElMessage.error(response.message || '测试支付完成失败')
}
} catch (error) {
if (error !== 'cancel') {
console.error('Test payment complete error:', error)
ElMessage.error('测试支付完成失败')
}
}
}
onMounted(() => {
fetchPayments()
})
</script>
<style scoped>
.payments {
max-width: 1200px;
margin: 0 auto;
}
.page-header {
margin-bottom: 20px;
}
.page-header h2 {
margin: 0;
color: #303133;
display: flex;
align-items: center;
gap: 8px;
}
.filter-card {
margin-bottom: 20px;
}
.payments-card {
margin-bottom: 20px;
}
.order-link {
color: #409EFF;
text-decoration: none;
font-weight: 500;
}
.order-link:hover {
text-decoration: underline;
}
.amount {
font-weight: 600;
color: #E6A23C;
}
.description {
color: #606266;
}
.pagination-container {
display: flex;
justify-content: center;
margin-top: 20px;
}
.payment-description {
margin-top: 20px;
}
.payment-description h4 {
margin-bottom: 12px;
color: #303133;
}
.payment-description p {
color: #606266;
line-height: 1.6;
}
.subscription-info {
text-align: center;
}
.subscription-info h3 {
color: #409eff;
margin-bottom: 0.5rem;
}
.subscription-info .price {
font-size: 2rem;
font-weight: bold;
color: #f56c6c;
margin: 1rem 0;
}
.subscription-info .description {
color: #666;
margin-bottom: 1rem;
}
.subscription-info .benefits {
text-align: left;
margin: 1rem 0;
}
.subscription-info .benefits h4 {
color: #333;
margin-bottom: 0.5rem;
}
.subscription-info .benefits ul {
list-style: none;
padding: 0;
}
.subscription-info .benefits li {
padding: 0.25rem 0;
color: #666;
}
.subscription-info .benefits li:before {
content: "✓ ";
color: #67c23a;
font-weight: bold;
}
.subscription-info .points-info {
margin-top: 1rem;
}
.subscription-info .payment-method {
margin-top: 1.5rem;
padding-top: 1rem;
border-top: 1px solid #e4e7ed;
}
.subscription-info .payment-method h4 {
color: #333;
margin-bottom: 0.5rem;
}
.subscription-info .converted-price {
margin-top: 0.5rem;
padding: 0.5rem;
background-color: #f0f9ff;
border-radius: 4px;
border: 1px solid #b3d8ff;
}
.subscription-info .price-display {
font-size: 1.2rem;
font-weight: bold;
color: #409eff;
}
</style>

View File

@@ -0,0 +1,533 @@
<template>
<div class="profile-page">
<!-- 左侧导航栏 -->
<aside class="sidebar">
<!-- Logo -->
<div class="logo">logo</div>
<!-- 导航菜单 -->
<nav class="nav-menu">
<div class="nav-item active">
<el-icon><User /></el-icon>
<span>个人主页</span>
</div>
<div class="nav-item">
<el-icon><Compass /></el-icon>
<span @click="goToSubscription">会员订阅</span>
</div>
<div class="nav-item">
<el-icon><Document /></el-icon>
<span>我的作品</span>
</div>
</nav>
<!-- 工具分隔线 -->
<div class="divider">
<span>工具</span>
</div>
<!-- 工具菜单 -->
<nav class="tools-menu">
<div class="nav-item">
<el-icon><Document /></el-icon>
<span>文生视频</span>
</div>
<div class="nav-item">
<el-icon><Picture /></el-icon>
<span>图生视频</span>
</div>
<div class="nav-item">
<el-icon><VideoPlay /></el-icon>
<span>分镜视频</span>
<el-tag size="small" type="primary" class="sora-tag">Sora2.0</el-tag>
</div>
</nav>
</aside>
<!-- 主内容区域 -->
<main class="main-content">
<!-- 顶部栏 -->
<header class="top-header">
<div class="header-right">
<div class="points">
<el-icon><Star /></el-icon>
<span>25 | 首购优惠</span>
</div>
<div class="notifications">
<el-icon><Bell /></el-icon>
<div class="notification-dot"></div>
</div>
<div class="user-status">
<div class="status-icon"></div>
</div>
</div>
</header>
<!-- 用户资料区域 -->
<section class="profile-section">
<div class="profile-info">
<div class="avatar">
<div class="avatar-icon"></div>
</div>
<div class="user-details">
<h2 class="username">mingzi_FBx7foZYDS7inLQb</h2>
<p class="profile-status">还没有设置个人简介,点击填写</p>
<p class="user-id">ID 2994509784706419</p>
</div>
<el-button class="edit-btn">编辑资料</el-button>
</div>
</section>
<!-- 已发布内容 -->
<section class="published-section">
<h3 class="section-title">已发布</h3>
<div class="video-grid">
<div class="video-item" v-for="(video, index) in videos" :key="index">
<div class="video-thumbnail">
<div class="thumbnail-image">
<div class="figure"></div>
<div class="text-overlay">What Does it Mean To You</div>
</div>
<div class="video-action">
<el-button v-if="index === 0" type="primary" size="small">做同款</el-button>
<span v-else class="director-text">DIRECTED BY VANNOCENT</span>
</div>
</div>
</div>
</div>
</section>
</main>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import {
User,
Compass,
Document,
Picture,
VideoPlay,
Star,
Bell
} from '@element-plus/icons-vue'
const router = useRouter()
// 模拟视频数据
const videos = ref(Array(6).fill({}))
// 跳转到会员订阅页面
const goToSubscription = () => {
router.push('/subscription')
}
</script>
<style scoped>
.profile-page {
min-height: 100vh;
background: #0a0a0a;
color: white;
display: flex;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 0;
width: 100vw;
height: 100vh;
overflow: hidden;
position: relative;
}
/* 页面特殊效果 */
.profile-page::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background:
radial-gradient(circle at 10% 20%, rgba(64, 158, 255, 0.1) 0%, transparent 50%),
radial-gradient(circle at 90% 80%, rgba(103, 194, 58, 0.1) 0%, transparent 50%),
radial-gradient(circle at 50% 50%, rgba(230, 162, 60, 0.05) 0%, transparent 50%);
animation: profileGlow 6s ease-in-out infinite alternate;
pointer-events: none;
z-index: 1;
}
@keyframes profileGlow {
0% { opacity: 0.3; }
100% { opacity: 0.6; }
}
/* 内容层级 */
.profile-page > * {
position: relative;
z-index: 2;
}
/* 左侧导航栏 */
.sidebar {
width: 240px;
background: #1a1a1a;
padding: 20px 0;
border-right: 1px solid #333;
}
.logo {
padding: 0 20px 30px;
font-size: 18px;
font-weight: 500;
color: white;
}
.nav-menu, .tools-menu {
padding: 0 20px;
}
.nav-item {
display: flex;
align-items: center;
padding: 12px 16px;
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: 12px;
font-size: 18px;
}
.nav-item span {
font-size: 14px;
flex: 1;
}
.sora-tag {
margin-left: 8px;
font-size: 10px;
padding: 2px 6px;
}
.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;
}
/* 顶部栏 */
.top-header {
padding: 20px 30px;
border-bottom: 1px solid #333;
display: flex;
justify-content: flex-end;
}
.header-right {
display: flex;
align-items: center;
gap: 20px;
}
.points {
display: flex;
align-items: center;
gap: 8px;
color: #409EFF;
font-size: 14px;
font-weight: 500;
}
.notifications {
position: relative;
cursor: pointer;
}
.notification-dot {
position: absolute;
top: -2px;
right: -2px;
width: 8px;
height: 8px;
background: #ff4757;
border-radius: 50%;
}
.user-status {
width: 32px;
height: 32px;
border: 2px solid #409EFF;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.status-icon {
width: 12px;
height: 12px;
background: white;
border-radius: 2px;
}
/* 用户资料区域 */
.profile-section {
padding: 30px;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
margin: 20px 30px;
border-radius: 12px;
border: 1px solid #333;
position: relative;
}
.profile-section::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: radial-gradient(circle at 50% 50%, rgba(64, 158, 255, 0.1) 0%, transparent 70%);
border-radius: 12px;
pointer-events: none;
}
.profile-info {
display: flex;
align-items: center;
gap: 20px;
position: relative;
z-index: 1;
}
.avatar {
width: 80px;
height: 80px;
border-radius: 50%;
background: #409EFF;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.avatar-icon {
width: 40px;
height: 40px;
background: #1a1a2e;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.avatar-icon::before {
content: '';
width: 16px;
height: 16px;
background: white;
border-radius: 2px;
}
.user-details {
flex: 1;
}
.username {
font-size: 24px;
font-weight: 600;
margin: 0 0 8px 0;
color: white;
}
.profile-status {
font-size: 14px;
color: #999;
margin: 0 0 4px 0;
}
.user-id {
font-size: 12px;
color: #666;
margin: 0;
}
.edit-btn {
background: #2a2a2a;
border: 1px solid #444;
color: white;
padding: 10px 20px;
border-radius: 6px;
font-size: 14px;
}
.edit-btn:hover {
background: #3a3a3a;
border-color: #555;
}
/* 已发布内容 */
.published-section {
padding: 0 30px 30px;
}
.section-title {
font-size: 20px;
font-weight: 600;
margin: 0 0 20px 0;
color: white;
position: relative;
padding-bottom: 8px;
}
.section-title::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 40px;
height: 3px;
background: #1e3a8a;
border-radius: 2px;
}
.video-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
.video-item {
background: #1a1a1a;
border-radius: 8px;
overflow: hidden;
border: 1px solid #333;
transition: all 0.3s ease;
}
.video-item:hover {
transform: translateY(-2px);
border-color: #444;
}
.video-thumbnail {
position: relative;
}
.thumbnail-image {
height: 200px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.figure {
width: 80px;
height: 80px;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
position: relative;
}
.figure::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 40px;
height: 40px;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
}
.text-overlay {
position: absolute;
top: 20px;
left: 20px;
right: 20px;
color: white;
font-size: 14px;
font-weight: 500;
text-align: center;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
}
.video-action {
padding: 15px;
text-align: center;
opacity: 0;
transition: opacity 0.2s ease;
}
.video-item:hover .video-action {
opacity: 1;
}
.director-text {
font-size: 12px;
color: #999;
}
/* 响应式设计 */
@media (max-width: 1200px) {
.video-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.profile-page {
flex-direction: column;
}
.sidebar {
width: 100%;
height: auto;
}
.nav-menu, .tools-menu {
display: flex;
overflow-x: auto;
padding: 0 20px;
}
.nav-item {
white-space: nowrap;
margin-right: 10px;
}
.video-grid {
grid-template-columns: 1fr;
}
.profile-info {
flex-direction: column;
text-align: center;
}
}
</style>

View File

@@ -0,0 +1,502 @@
<template>
<div class="register">
<el-row justify="center" align="middle" class="register-container">
<el-col :xs="22" :sm="16" :md="12" :lg="8" :xl="6">
<el-card class="register-card">
<template #header>
<div class="register-header">
<el-icon size="32" color="#67C23A"><UserFilled /></el-icon>
<h2>用户注册</h2>
</div>
</template>
<el-form
ref="registerFormRef"
:model="registerForm"
:rules="registerRules"
label-width="80px"
@submit.prevent="handleRegister"
>
<el-form-item label="用户名" prop="username">
<el-input
v-model="registerForm.username"
placeholder="请输入用户名"
prefix-icon="User"
clearable
@blur="checkUsername"
/>
<div v-if="usernameChecking" class="checking-text">
<el-icon class="is-loading"><Loading /></el-icon>
检查中...
</div>
<div v-if="usernameExists" class="error-text">
<el-icon><CircleCloseFilled /></el-icon>
用户名已存在
</div>
<div v-if="usernameAvailable" class="success-text">
<el-icon><CircleCheckFilled /></el-icon>
用户名可用
</div>
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input
v-model="registerForm.email"
placeholder="请输入邮箱"
prefix-icon="Message"
clearable
@blur="checkEmail"
/>
<div v-if="emailChecking" class="checking-text">
<el-icon class="is-loading"><Loading /></el-icon>
检查中...
</div>
<div v-if="emailExists" class="error-text">
<el-icon><CircleCloseFilled /></el-icon>
邮箱已存在
</div>
<div v-if="emailAvailable" class="success-text">
<el-icon><CircleCheckFilled /></el-icon>
邮箱可用
</div>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input
v-model="registerForm.password"
type="password"
placeholder="请输入密码"
prefix-icon="Lock"
show-password
clearable
@input="checkPasswordStrength"
/>
<div v-if="passwordStrength" class="password-strength">
<div class="strength-bar">
<div
class="strength-fill"
:class="strengthClass"
:style="{ width: strengthWidth }"
></div>
</div>
<span class="strength-text">{{ strengthText }}</span>
</div>
</el-form-item>
<el-form-item label="确认密码" prop="confirmPassword">
<el-input
v-model="registerForm.confirmPassword"
type="password"
placeholder="请再次输入密码"
prefix-icon="Lock"
show-password
clearable
/>
</el-form-item>
<el-form-item>
<el-checkbox v-model="agreeTerms">
我已阅读并同意
<a href="#" class="terms-link">用户协议</a>
<a href="#" class="terms-link">隐私政策</a>
</el-checkbox>
</el-form-item>
<el-form-item>
<el-button
type="success"
size="large"
:loading="userStore.loading"
:disabled="!canRegister"
@click="handleRegister"
class="register-button"
>
{{ userStore.loading ? '注册中...' : '注册' }}
</el-button>
</el-form-item>
</el-form>
<div class="register-footer">
<p>已有账号<router-link to="/login" class="login-link">立即登录</router-link></p>
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup>
import { ref, reactive, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { checkUsernameExists, checkEmailExists } from '@/api/auth'
import { ElMessage } from 'element-plus'
const router = useRouter()
const userStore = useUserStore()
const registerFormRef = ref()
const agreeTerms = ref(false)
// 用户名检查状态
const usernameChecking = ref(false)
const usernameExists = ref(false)
const usernameAvailable = ref(false)
// 邮箱检查状态
const emailChecking = ref(false)
const emailExists = ref(false)
const emailAvailable = ref(false)
// 密码强度
const passwordStrength = ref(false)
const strengthLevel = ref(0)
const registerForm = reactive({
username: '',
email: '',
password: '',
confirmPassword: ''
})
const registerRules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 20, message: '用户名长度在 3 到 20 个字符', trigger: 'blur' },
{ pattern: /^[a-zA-Z0-9_]+$/, message: '用户名只能包含字母、数字和下划线', trigger: 'blur' }
],
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 20, message: '密码长度在 6 到 20 个字符', trigger: 'blur' }
],
confirmPassword: [
{ required: true, message: '请确认密码', trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (value !== registerForm.password) {
callback(new Error('两次输入密码不一致'))
} else {
callback()
}
},
trigger: 'blur'
}
]
}
// 检查用户名是否存在
const checkUsername = async () => {
if (!registerForm.username || registerForm.username.length < 3) {
resetUsernameCheck()
return
}
try {
usernameChecking.value = true
usernameExists.value = false
usernameAvailable.value = false
const response = await checkUsernameExists(registerForm.username)
if (response.success) {
usernameExists.value = response.data.exists
usernameAvailable.value = !response.data.exists
}
} catch (error) {
console.error('Check username error:', error)
} finally {
usernameChecking.value = false
}
}
// 检查邮箱是否存在
const checkEmail = async () => {
if (!registerForm.email || !isValidEmail(registerForm.email)) {
resetEmailCheck()
return
}
try {
emailChecking.value = true
emailExists.value = false
emailAvailable.value = false
const response = await checkEmailExists(registerForm.email)
if (response.success) {
emailExists.value = response.data.exists
emailAvailable.value = !response.data.exists
}
} catch (error) {
console.error('Check email error:', error)
} finally {
emailChecking.value = false
}
}
// 检查密码强度
const checkPasswordStrength = () => {
const password = registerForm.password
if (!password) {
passwordStrength.value = false
return
}
passwordStrength.value = true
let score = 0
if (password.length >= 6) score++
if (password.length >= 8) score++
if (/[a-z]/.test(password)) score++
if (/[A-Z]/.test(password)) score++
if (/[0-9]/.test(password)) score++
if (/[^A-Za-z0-9]/.test(password)) score++
strengthLevel.value = Math.min(score, 4)
}
// 重置用户名检查状态
const resetUsernameCheck = () => {
usernameChecking.value = false
usernameExists.value = false
usernameAvailable.value = false
}
// 重置邮箱检查状态
const resetEmailCheck = () => {
emailChecking.value = false
emailExists.value = false
emailAvailable.value = false
}
// 验证邮箱格式
const isValidEmail = (email) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return emailRegex.test(email)
}
// 计算属性
const strengthClass = computed(() => {
const classes = ['weak', 'fair', 'good', 'strong']
return classes[strengthLevel.value - 1] || 'weak'
})
const strengthWidth = computed(() => {
return `${(strengthLevel.value / 4) * 100}%`
})
const strengthText = computed(() => {
const texts = ['弱', '一般', '良好', '强']
return texts[strengthLevel.value - 1] || '弱'
})
const canRegister = computed(() => {
return agreeTerms.value &&
usernameAvailable.value &&
emailAvailable.value &&
registerForm.password &&
registerForm.confirmPassword &&
registerForm.password === registerForm.confirmPassword
})
const handleRegister = async () => {
if (!registerFormRef.value) return
try {
const valid = await registerFormRef.value.validate()
if (!valid) return
if (!agreeTerms.value) {
ElMessage.warning('请先同意用户协议和隐私政策')
return
}
const result = await userStore.registerUser(registerForm)
if (result.success) {
ElMessage.success(result.message || '注册成功')
router.push('/login')
} else {
ElMessage.error(result.message || '注册失败')
}
} catch (error) {
console.error('Register error:', error)
ElMessage.error('注册失败,请重试')
}
}
</script>
<style scoped>
.register {
min-height: calc(100vh - 120px);
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 40px 0;
position: relative;
overflow-x: hidden;
}
/* 页面特殊效果 */
.register::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background:
radial-gradient(circle at 25% 25%, rgba(255, 255, 255, 0.1) 0%, transparent 50%),
radial-gradient(circle at 75% 75%, rgba(255, 255, 255, 0.05) 0%, transparent 50%);
animation: registerFloat 4s ease-in-out infinite alternate;
pointer-events: none;
z-index: 1;
}
@keyframes registerFloat {
0% { transform: translateY(0px) rotate(0deg); }
100% { transform: translateY(-10px) rotate(1deg); }
}
/* 内容层级 */
.register > * {
position: relative;
z-index: 2;
}
.register-container {
min-height: calc(100vh - 200px);
}
.register-card {
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
border-radius: 12px;
backdrop-filter: blur(10px);
background: rgba(255, 255, 255, 0.95);
}
.register-header {
text-align: center;
margin-bottom: 20px;
}
.register-header h2 {
margin: 12px 0 0 0;
color: #303133;
font-weight: 600;
}
.register-button {
width: 100%;
height: 45px;
font-size: 16px;
}
.register-footer {
text-align: center;
margin-top: 20px;
}
.register-footer p {
margin: 0;
color: #606266;
}
.login-link {
color: #67C23A;
text-decoration: none;
font-weight: 500;
}
.login-link:hover {
text-decoration: underline;
}
.terms-link {
color: #409EFF;
text-decoration: none;
}
.terms-link:hover {
text-decoration: underline;
}
.checking-text, .error-text, .success-text {
font-size: 12px;
margin-top: 4px;
display: flex;
align-items: center;
gap: 4px;
}
.checking-text {
color: #909399;
}
.error-text {
color: #F56C6C;
}
.success-text {
color: #67C23A;
}
.password-strength {
margin-top: 8px;
}
.strength-bar {
height: 4px;
background-color: #EBEEF5;
border-radius: 2px;
overflow: hidden;
margin-bottom: 4px;
}
.strength-fill {
height: 100%;
transition: all 0.3s;
}
.strength-fill.weak {
background-color: #F56C6C;
}
.strength-fill.fair {
background-color: #E6A23C;
}
.strength-fill.good {
background-color: #409EFF;
}
.strength-fill.strong {
background-color: #67C23A;
}
.strength-text {
font-size: 12px;
color: #606266;
}
@media (max-width: 768px) {
.register {
padding: 20px 0;
}
.register-container {
min-height: calc(100vh - 160px);
}
}
</style>

View File

@@ -0,0 +1,13 @@
<template>
<div>
<h1>测试页面</h1>
<p>如果您能看到这个页面说明Vue应用正常工作</p>
</div>
</template>
<script setup>
console.log('测试页面加载成功')
</script>

View File

@@ -0,0 +1,752 @@
<template>
<div class="storyboard-video-page">
<!-- 左侧导航栏 -->
<aside class="sidebar">
<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" @click="goToSubscription">
<el-icon><Compass /></el-icon>
<span>会员订阅</span>
</div>
<div class="nav-item" @click="goToMyWorks">
<el-icon><Document /></el-icon>
<span>我的作品</span>
</div>
<div class="nav-divider"></div>
<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 active storyboard-item">
<el-icon><VideoPlay /></el-icon>
<span>分镜视频</span>
<el-tag size="small" type="primary" class="sora-tag">Sora2.0</el-tag>
</div>
</nav>
</aside>
<!-- 主内容区域 -->
<main class="main-content">
<!-- 顶部用户信息卡片 -->
<div class="user-info-card">
<div class="user-avatar">
<div class="avatar-placeholder">👁👃</div>
</div>
<div class="user-details">
<div class="username">mingzi_FBx7foZYDS7inLQb</div>
<div class="profile-prompt">还没有设置个人简介,点击填写</div>
<div class="user-id">ID 2994509784706419</div>
</div>
<div class="edit-profile-btn">
<el-button type="primary">编辑资料</el-button>
</div>
</div>
<!-- 已发布作品区域 -->
<div class="published-works">
<div class="works-tabs">
<div class="tab active">已发布</div>
</div>
<div class="works-grid">
<div class="work-item" v-for="(work, index) in publishedWorks" :key="work.id" @click="openDetail(work)">
<div class="work-thumbnail">
<img :src="work.cover" :alt="work.title" />
<div class="work-overlay">
<div class="overlay-text">{{ work.text }}</div>
</div>
</div>
<div class="work-info">
<div class="work-title">{{ work.title }}</div>
<div class="work-meta">{{ work.id }} · {{ work.size }}</div>
</div>
<div class="work-actions" v-if="index === 0">
<el-button type="primary" class="create-similar-btn" @click.stop="goToCreate(work)">做同款</el-button>
</div>
<div class="work-director" v-else>
<span>DIRECTED BY VANNOCENT</span>
</div>
</div>
</div>
</div>
</main>
<!-- 作品详情模态框 -->
<el-dialog
v-model="detailDialogVisible"
:title="selectedItem?.title"
width="60%"
class="detail-dialog"
:modal="true"
:close-on-click-modal="true"
:close-on-press-escape="true"
@close="handleClose"
>
<div class="detail-content">
<div class="detail-left">
<div class="video-player">
<img :src="selectedItem?.cover" :alt="selectedItem?.title" class="video-thumbnail" />
<div class="play-overlay">
<div class="play-button"></div>
</div>
</div>
</div>
<div class="detail-right">
<div class="metadata-section">
<div class="metadata-item">
<span class="label">作品 ID</span>
<span class="value">{{ selectedItem?.id }}</span>
</div>
<div class="metadata-item">
<span class="label">文件大小</span>
<span class="value">{{ selectedItem?.size }}</span>
</div>
<div class="metadata-item">
<span class="label">创建时间</span>
<span class="value">{{ selectedItem?.createTime }}</span>
</div>
<div class="metadata-item">
<span class="label">分类</span>
<span class="value">{{ selectedItem?.category }}</span>
</div>
</div>
<div class="description-section">
<h3 class="section-title">描述</h3>
<p class="description-text">{{ getDescription(selectedItem) }}</p>
</div>
<!-- 操作按钮 -->
<div class="action-section">
<button class="create-similar-btn" @click="createSimilar">
做同款
</button>
</div>
</div>
</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElIcon, ElButton, ElTag, ElMessage, ElDialog } from 'element-plus'
import { User, Compass, Document, VideoPlay, Picture } from '@element-plus/icons-vue'
const router = useRouter()
// 模态框状态
const detailDialogVisible = ref(false)
const selectedItem = ref(null)
// 已发布作品数据
const publishedWorks = ref([
{
id: '2995000000001',
title: '分镜视频作品 #1',
cover: '/images/backgrounds/welcome.jpg',
text: 'What Does it Mean To You',
size: '9 MB',
category: '分镜视频',
createTime: '2025/01/15 14:30'
},
{
id: '2995000000002',
title: '分镜视频作品 #2',
cover: '/images/backgrounds/welcome.jpg',
text: 'What Does it Mean To You',
size: '9 MB',
category: '分镜视频',
createTime: '2025/01/14 16:45'
},
{
id: '2995000000003',
title: '分镜视频作品 #3',
cover: '/images/backgrounds/welcome.jpg',
text: 'What Does it Mean To You',
size: '9 MB',
category: '分镜视频',
createTime: '2025/01/13 09:20'
}
])
// 导航函数
const goToProfile = () => {
router.push('/profile')
}
const goToSubscription = () => {
router.push('/subscription')
}
const goToMyWorks = () => {
router.push('/works')
}
const goToTextToVideo = () => {
router.push('/text-to-video')
}
const goToImageToVideo = () => {
router.push('/image-to-video')
}
const goToCreate = (work) => {
// 跳转到分镜视频创作页面
router.push('/storyboard-video/create')
}
// 模态框相关函数
const openDetail = (work) => {
selectedItem.value = work
detailDialogVisible.value = true
}
const handleClose = () => {
detailDialogVisible.value = false
selectedItem.value = null
}
const getDescription = (item) => {
if (!item) return ''
return `这是一个${item.category}作品,展现了"What Does it Mean To You"的主题。作品通过AI技术生成具有独特的视觉风格和创意表达。`
}
const createSimilar = () => {
// 关闭模态框并跳转到创作页面
handleClose()
router.push('/storyboard-video/create')
}
onMounted(() => {
// 页面初始化
})
</script>
<style scoped>
.storyboard-video-page {
display: flex;
height: 100vh;
background: #0a0a0a;
color: #fff;
margin: 0;
padding: 0;
}
/* 左侧导航栏 */
.sidebar {
width: 280px;
background: #1a1a1a;
border-right: 1px solid #333;
padding: 24px 0;
}
.logo {
font-size: 18px;
font-weight: 600;
color: #fff;
text-align: center;
margin-bottom: 32px;
}
.nav-menu {
display: flex;
flex-direction: column;
gap: 8px;
padding: 0 20px;
}
.nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
color: #d1d5db;
}
.nav-item:hover {
background: #2a2a2a;
color: #fff;
}
.nav-item.active {
background: #3b82f6;
color: #fff;
}
.nav-divider {
height: 1px;
background: #333;
margin: 16px 0;
}
.sora-tag {
margin-left: 8px;
}
/* 分镜视频特殊样式 */
.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);
}
}
/* 主内容区域 */
.main-content {
flex: 1;
padding: 24px;
display: flex;
flex-direction: column;
gap: 24px;
}
/* 用户信息卡片 */
.user-info-card {
background: #1a1a1a;
border: 1px solid #333;
border-radius: 12px;
padding: 24px;
display: flex;
align-items: center;
gap: 20px;
}
.user-avatar {
width: 80px;
height: 80px;
border-radius: 50%;
background: #000;
display: flex;
align-items: center;
justify-content: center;
border: 2px solid #333;
}
.avatar-placeholder {
color: #fff;
font-size: 24px;
font-weight: bold;
}
.user-details {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.username {
font-size: 18px;
font-weight: 600;
color: #fff;
}
.profile-prompt {
font-size: 14px;
color: #9ca3af;
}
.user-id {
font-size: 12px;
color: #6b7280;
}
.edit-profile-btn {
margin-left: auto;
}
/* 已发布作品区域 */
.published-works {
display: flex;
flex-direction: column;
gap: 20px;
}
.works-tabs {
display: flex;
gap: 24px;
}
.tab {
padding: 8px 0;
color: #9ca3af;
cursor: pointer;
position: relative;
}
.tab.active {
color: #fff;
}
.tab.active::after {
content: '';
position: absolute;
bottom: -8px;
left: 0;
right: 0;
height: 2px;
background: #3b82f6;
}
.works-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
.work-item {
background: #1a1a1a;
border: 1px solid #333;
border-radius: 12px;
overflow: hidden;
transition: all 0.2s;
cursor: pointer;
}
.work-item:hover {
border-color: #3b82f6;
transform: translateY(-2px);
}
.work-thumbnail {
position: relative;
aspect-ratio: 16/9;
overflow: hidden;
}
.work-thumbnail img {
width: 100%;
height: 100%;
object-fit: cover;
}
.work-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.8));
padding: 20px;
}
.overlay-text {
font-size: 16px;
font-weight: 600;
color: #fff;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.8);
}
.work-info {
padding: 16px;
display: flex;
flex-direction: column;
gap: 8px;
}
.work-title {
font-size: 16px;
font-weight: 600;
color: #fff;
}
.work-meta {
font-size: 12px;
color: #9ca3af;
}
.work-actions {
padding: 0 16px 16px;
opacity: 0;
transition: opacity 0.2s ease;
}
.work-item:hover .work-actions {
opacity: 1;
}
.create-similar-btn {
width: 100%;
}
.work-director {
padding: 0 16px 16px;
text-align: center;
}
.work-director span {
font-size: 12px;
color: #6b7280;
font-style: italic;
}
/* 响应式设计 */
@media (max-width: 1024px) {
.sidebar {
width: 260px;
}
.works-grid {
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
}
}
@media (max-width: 768px) {
.storyboard-video-page {
flex-direction: column;
}
.sidebar {
width: 100%;
height: auto;
}
.nav-menu {
flex-direction: row;
overflow-x: auto;
padding: 0 16px;
}
.nav-item {
white-space: nowrap;
}
.works-grid {
grid-template-columns: 1fr;
}
}
/* 模态框样式 */
:deep(.detail-dialog .el-dialog) {
background: #0a0a0a !important;
border-radius: 12px;
border: 1px solid #333 !important;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.8);
}
:deep(.detail-dialog .el-dialog__wrapper) {
background-color: rgba(0, 0, 0, 0.8) !important;
}
:deep(.detail-dialog .el-dialog__header) {
background: #0a0a0a !important;
padding: 16px 20px;
border-bottom: 1px solid #333;
}
:deep(.detail-dialog .el-dialog__title) {
color: #fff !important;
font-size: 18px;
font-weight: 600;
}
:deep(.detail-dialog .el-dialog__headerbtn) {
color: #fff !important;
}
:deep(.detail-dialog .el-dialog__body) {
background: #0a0a0a !important;
padding: 0 !important;
}
:deep(.detail-dialog .el-overlay) {
background-color: rgba(0, 0, 0, 0.8) !important;
}
/* 全局覆盖Element Plus默认样式 */
:deep(.el-dialog) {
background: #0a0a0a !important;
border: 1px solid #333 !important;
}
:deep(.el-dialog__wrapper) {
background-color: rgba(0, 0, 0, 0.8) !important;
}
:deep(.el-dialog__header) {
background: #0a0a0a !important;
}
:deep(.el-dialog__body) {
background: #0a0a0a !important;
}
:deep(.el-overlay) {
background-color: rgba(0, 0, 0, 0.8) !important;
}
.detail-content {
display: flex;
height: 50vh;
background: #0a0a0a;
}
.detail-left {
flex: 1;
padding: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.video-player {
position: relative;
width: 100%;
max-width: 400px;
aspect-ratio: 16/9;
border-radius: 8px;
overflow: hidden;
cursor: pointer;
}
.video-thumbnail {
width: 100%;
height: 100%;
object-fit: cover;
}
.play-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.2s ease;
}
.video-player:hover .play-overlay {
opacity: 1;
}
.play-button {
width: 60px;
height: 60px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.9);
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
color: #000;
font-weight: bold;
}
.detail-right {
flex: 1;
padding: 20px;
background: #0a0a0a;
display: flex;
flex-direction: column;
gap: 20px;
}
.metadata-section {
display: flex;
flex-direction: column;
gap: 12px;
}
.metadata-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #2a2a2a;
}
.metadata-item:last-child {
border-bottom: none;
}
.label {
font-size: 14px;
color: #9ca3af;
font-weight: 500;
}
.value {
font-size: 14px;
color: #fff;
font-weight: 600;
}
.description-section {
flex: 1;
}
.section-title {
font-size: 16px;
color: #fff;
font-weight: 600;
margin-bottom: 12px;
}
.description-text {
font-size: 14px;
color: #d1d5db;
line-height: 1.6;
margin: 0;
}
.action-section {
margin-top: auto;
}
.create-similar-btn {
width: 100%;
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
color: #fff;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
}
.create-similar-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
}
</style>

View File

@@ -0,0 +1,732 @@
<template>
<div class="storyboard-video-create-page">
<!-- 顶部导航栏 -->
<header class="top-header">
<div class="header-left">
<button class="back-btn" @click="goBack">
首页
</button>
</div>
<div class="header-right">
<div class="credits-info">
<div class="credits-circle">25</div>
<span>| 首购优惠</span>
</div>
<div class="notification-icon">
🔔
<div class="notification-badge">5</div>
</div>
<div class="user-avatar">
👤
</div>
</div>
</header>
<!-- 主内容区域 -->
<div class="main-content">
<!-- 左侧设置面板 -->
<div class="left-panel">
<!-- 创作模式标签 -->
<div class="creation-tabs">
<div class="tab" @click="goToTextToVideo">文生视频</div>
<div class="tab" @click="goToImageToVideo">图生视频</div>
<div class="tab active">分镜视频</div>
</div>
<!-- 分镜步骤标签 -->
<div class="storyboard-steps">
<div class="step active">生成分镜图</div>
<div class="step">生成视频</div>
</div>
<!-- 生成分镜图区域 -->
<div class="storyboard-section">
<div class="image-upload-btn" @click="uploadImage">
<span>+ 图片 (可选)</span>
</div>
<!-- 已上传的图片预览 -->
<div class="image-preview" v-if="uploadedImage">
<img :src="uploadedImage" alt="上传的图片" />
<button class="remove-btn" @click="removeImage">×</button>
</div>
<div class="text-input-section">
<textarea
v-model="inputText"
placeholder="例如:一个咖啡的广告提示:简单描述即可,AI会自动优化成专业的12格黑白分镜图"
class="text-input"
rows="6"
></textarea>
<div class="optimize-btn">
<button class="optimize-button">
一键优化
</button>
</div>
</div>
</div>
<!-- 视频设置 -->
<div class="video-settings">
<div class="setting-item">
<label>比例</label>
<select v-model="aspectRatio" class="setting-select">
<option value="16:9">16:9</option>
<option value="4:3">4:3</option>
<option value="1:1">1:1</option>
<option value="3:4">3:4</option>
<option value="9:16">9:16</option>
</select>
</div>
<div class="setting-item">
<label>高清模式 (1080P)</label>
<div class="hd-setting">
<input type="checkbox" v-model="hdMode" class="hd-switch">
<span class="cost-text">开启消耗20积分</span>
</div>
</div>
</div>
<!-- 生成按钮 -->
<div class="generate-section">
<button class="generate-btn" @click="startGenerate">
开始生成
</button>
</div>
</div>
<!-- 右侧预览区域 -->
<div class="right-panel">
<div class="preview-area">
<div class="status-checkbox">
<input type="checkbox" v-model="inProgress" id="progress-checkbox">
<label for="progress-checkbox">进行中</label>
</div>
<div class="preview-content">
<div class="preview-placeholder">
<div class="placeholder-text">开始创作您的第一个作品吧!</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
// 表单数据
const inputText = ref('')
const aspectRatio = ref('16:9')
const hdMode = ref(false)
const inProgress = ref(false)
// 图片上传
const uploadedImage = ref('')
// 导航函数
const goBack = () => {
router.back()
}
const goToTextToVideo = () => {
router.push('/text-to-video/create')
}
const goToImageToVideo = () => {
router.push('/image-to-video/create')
}
// 图片上传处理
const uploadImage = () => {
const input = document.createElement('input')
input.type = 'file'
input.accept = 'image/*'
input.onchange = (e) => {
const file = e.target.files[0]
if (file) {
const reader = new FileReader()
reader.onload = (e) => {
uploadedImage.value = e.target.result
}
reader.readAsDataURL(file)
}
}
input.click()
}
const removeImage = () => {
uploadedImage.value = ''
}
const startGenerate = () => {
if (!inputText.value.trim()) {
alert('请输入描述文字')
return
}
inProgress.value = true
alert('开始生成分镜图...')
// 模拟生成过程
setTimeout(() => {
inProgress.value = false
alert('分镜图生成完成!')
}, 3000)
}
</script>
<style scoped>
.storyboard-video-create-page {
height: 100vh;
background: #0a0a0a;
color: #fff;
display: flex;
flex-direction: column;
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
/* 顶部导航栏 */
.top-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 32px;
background: #0a0a0a;
border-bottom: 1px solid #1f1f1f;
min-height: 60px;
}
.header-left {
display: flex;
align-items: center;
}
.back-btn {
background: none;
border: none;
color: #fff;
font-size: 16px;
cursor: pointer;
padding: 10px 20px;
border-radius: 8px;
transition: all 0.2s ease;
font-weight: 500;
}
.back-btn:hover {
background: #1a1a1a;
transform: translateX(-2px);
}
.header-right {
display: flex;
align-items: center;
gap: 24px;
}
.credits-info {
display: flex;
align-items: center;
gap: 12px;
color: #fff;
font-size: 14px;
font-weight: 500;
}
.credits-circle {
width: 36px;
height: 36px;
border-radius: 50%;
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 16px;
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
.notification-icon {
position: relative;
font-size: 24px;
cursor: pointer;
padding: 8px;
border-radius: 8px;
transition: background 0.2s ease;
}
.notification-icon:hover {
background: #1a1a1a;
}
.notification-badge {
position: absolute;
top: 2px;
right: 2px;
background: #ef4444;
color: #fff;
border-radius: 50%;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 700;
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.4);
}
.user-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: linear-gradient(135deg, #374151, #1f2937);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 20px;
transition: transform 0.2s ease;
}
.user-avatar:hover {
transform: scale(1.05);
}
/* 主内容区域 */
.main-content {
flex: 1;
display: grid;
grid-template-columns: 400px 1fr;
gap: 0;
height: calc(100vh - 100px);
}
/* 左侧面板 */
.left-panel {
background: #1a1a1a;
border-right: 1px solid #2a2a2a;
padding: 32px;
display: flex;
flex-direction: column;
gap: 32px;
overflow-y: auto;
}
/* 创作模式标签 */
.creation-tabs {
display: flex;
gap: 4px;
background: #0a0a0a;
padding: 4px;
border-radius: 12px;
}
.tab {
flex: 1;
padding: 12px 16px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
color: #9ca3af;
font-size: 14px;
font-weight: 500;
text-align: center;
}
.tab.active {
background: #3b82f6;
color: #fff;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
}
.tab:hover:not(.active) {
background: #2a2a2a;
color: #fff;
}
/* 分镜步骤标签 */
.storyboard-steps {
display: flex;
gap: 4px;
background: #0a0a0a;
padding: 4px;
border-radius: 12px;
}
.step {
flex: 1;
padding: 10px 16px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
color: #9ca3af;
font-size: 13px;
font-weight: 500;
text-align: center;
}
.step.active {
background: #3b82f6;
color: #fff;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
}
.step:hover:not(.active) {
background: #2a2a2a;
color: #fff;
}
/* 分镜图区域 */
.storyboard-section {
display: flex;
flex-direction: column;
gap: 16px;
}
.image-upload-btn {
width: 100%;
padding: 12px 16px;
background: #0a0a0a;
border: 2px dashed #2a2a2a;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
text-align: center;
color: #9ca3af;
font-size: 14px;
}
.image-upload-btn:hover {
border-color: #3b82f6;
background: #1a1a1a;
color: #fff;
}
.image-preview {
position: relative;
width: 100%;
aspect-ratio: 16/9;
}
.image-preview img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 8px;
}
.remove-btn {
position: absolute;
top: 8px;
right: 8px;
width: 24px;
height: 24px;
border-radius: 50%;
background: rgba(0, 0, 0, 0.7);
color: #fff;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-weight: bold;
}
/* 文本输入区域 */
.text-input-section {
display: flex;
flex-direction: column;
gap: 16px;
}
.text-input {
width: 100%;
min-height: 120px;
padding: 16px;
background: #0a0a0a;
border: 2px solid #2a2a2a;
border-radius: 12px;
color: #fff;
font-size: 15px;
line-height: 1.6;
resize: vertical;
outline: none;
transition: all 0.2s ease;
font-family: inherit;
}
.text-input:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.text-input::placeholder {
color: #6b7280;
}
.optimize-btn {
display: flex;
justify-content: flex-end;
}
.optimize-button {
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
color: #fff;
border: none;
padding: 10px 20px;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
}
.optimize-button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
}
/* 视频设置 */
.video-settings {
display: flex;
flex-direction: column;
gap: 20px;
}
.setting-item {
display: flex;
flex-direction: column;
gap: 10px;
}
.setting-item label {
font-size: 14px;
color: #e5e7eb;
font-weight: 600;
}
.setting-select {
padding: 12px 16px;
background: #0a0a0a;
border: 2px solid #2a2a2a;
border-radius: 8px;
color: #fff;
font-size: 14px;
outline: none;
cursor: pointer;
transition: all 0.2s ease;
font-family: inherit;
}
.setting-select:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.setting-select:hover {
border-color: #374151;
}
.hd-setting {
display: flex;
align-items: center;
gap: 12px;
}
.hd-switch {
width: 20px;
height: 20px;
cursor: pointer;
accent-color: #3b82f6;
}
.cost-text {
font-size: 13px;
color: #9ca3af;
font-weight: 500;
}
/* 生成按钮 */
.generate-section {
margin-top: auto;
}
.generate-btn {
width: 100%;
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
color: #fff;
border: none;
padding: 16px 24px;
border-radius: 12px;
font-size: 16px;
font-weight: 700;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 4px 16px rgba(59, 130, 246, 0.3);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.generate-btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(59, 130, 246, 0.4);
}
.generate-btn:active {
transform: translateY(0);
}
.generate-btn:disabled {
background: #6b7280;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
/* 右侧面板 */
.right-panel {
background: #0a0a0a;
padding: 32px;
display: flex;
flex-direction: column;
}
.preview-area {
flex: 1;
display: flex;
flex-direction: column;
gap: 20px;
}
.status-checkbox {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
}
.status-checkbox input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
accent-color: #3b82f6;
}
.status-checkbox label {
font-size: 14px;
color: #e5e7eb;
cursor: pointer;
font-weight: 500;
}
.preview-content {
flex: 1;
background: #1a1a1a;
border: 2px solid #2a2a2a;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.preview-content:hover {
border-color: #374151;
}
.preview-placeholder {
text-align: center;
padding: 40px;
}
.placeholder-text {
font-size: 18px;
color: #6b7280;
font-weight: 500;
line-height: 1.5;
}
/* 响应式设计 */
@media (max-width: 1200px) {
.main-content {
grid-template-columns: 350px 1fr;
}
.left-panel {
padding: 24px;
}
.right-panel {
padding: 24px;
}
}
@media (max-width: 1024px) {
.main-content {
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
}
.left-panel {
border-right: none;
border-bottom: 1px solid #2a2a2a;
padding: 20px;
}
.right-panel {
padding: 20px;
}
}
@media (max-width: 768px) {
.top-header {
padding: 16px 20px;
}
.header-right {
gap: 16px;
}
.left-panel {
padding: 16px;
gap: 24px;
}
.right-panel {
padding: 16px;
}
.creation-tabs {
flex-direction: column;
gap: 8px;
}
.tab {
text-align: left;
}
.storyboard-steps {
flex-direction: column;
gap: 8px;
}
.step {
text-align: left;
}
}
</style>

View File

@@ -0,0 +1,557 @@
<template>
<div class="subscription-page">
<!-- 左侧导航栏 -->
<aside class="sidebar">
<!-- Logo -->
<div class="logo">logo</div>
<!-- 导航菜单 -->
<nav class="nav-menu">
<div class="nav-item" @click="goToProfile" @mousedown="console.log('mousedown 个人主页')">
<el-icon><User /></el-icon>
<span>个人主页</span>
</div>
<div class="nav-item" :class="{ active: currentSection === 'subscription' }" @click="setSection('subscription')" @mousedown="console.log('mousedown 会员订阅')">
<el-icon><Compass /></el-icon>
<span>会员订阅</span>
</div>
<div class="nav-item" :class="{ active: currentSection === 'works' }" @click="setSection('works')" @mousedown="console.log('mousedown 我的作品')">
<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><Document /></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">
<el-icon><VideoPlay /></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">
<el-icon><Plus /></el-icon>
<span>50</span>
</div>
<button class="mini-btn">积分详情</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">
<el-icon class="plus-icon"><Plus /></el-icon>
<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>
</template>
<script setup>
import { ref } from 'vue'
import MyWorks from '@/views/MyWorks.vue'
import { useRouter } from 'vue-router'
import {
User,
Compass,
Document,
Picture,
VideoPlay,
Plus,
Bell,
Check
} from '@element-plus/icons-vue'
const router = useRouter()
// 跳转到个人主页
const goToProfile = () => {
console.log('点击个人主页')
router.push('/profile')
}
const goToTextToVideo = () => {
router.push('/text-to-video')
}
const goToImageToVideo = () => {
router.push('/image-to-video')
}
// 当前主区块subscription | works
const currentSection = ref('subscription')
const setSection = (section) => {
console.log('切换区块到:', section)
currentSection.value = section
}
// 选中套餐(紫色边框)
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 40px 30px; /* 与顶部盒子保持一致的左右留白 */
}
.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 40px 0 0; /* 右侧留白与左侧 padding(40px) 保持一致 */
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: none; /* 取消限制,铺满内容区域 */
width: 100%;
margin: 32px 40px 10px 40px; /* 左右各 40px 留白,与套餐区对齐 */
background: #1a1a1a; /* 与卡片、页面风格保持一致 */
border: 1px solid #333;
border-radius: 10px;
}
.top-merged-card .row-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 26px;
padding: 22px 26px; /* 比例放大 */
border-bottom: 1px solid #1f2937;
}
.top-merged-card .row-bottom {
display: grid;
grid-template-columns: 1fr auto 1fr auto 1fr;
gap: 30px;
padding: 22px 26px 24px; /* 比例放大 */
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; }
.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; }
}
</style>

View File

@@ -0,0 +1,750 @@
<template>
<div class="text-to-video-page">
<!-- 左侧导航栏 -->
<aside class="sidebar">
<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" @click="goToSubscription">
<el-icon><Compass /></el-icon>
<span>会员订阅</span>
</div>
<div class="nav-item" @click="goToMyWorks">
<el-icon><Document /></el-icon>
<span>我的作品</span>
</div>
<div class="nav-divider"></div>
<div class="nav-item active">
<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="goToStoryboard">
<el-icon><VideoPlay /></el-icon>
<span>分镜视频</span>
<el-tag size="small" type="primary" class="sora-tag">Sora2.0</el-tag>
</div>
</nav>
</aside>
<!-- 主内容区域 -->
<main class="main-content">
<!-- 顶部用户信息卡片 -->
<div class="user-info-card">
<div class="user-avatar">
<div class="avatar-placeholder">||</div>
</div>
<div class="user-details">
<div class="username">mingzi_FBx7foZYDS7inLQb</div>
<div class="profile-prompt">还没有设置个人简介,点击填写</div>
<div class="user-id">ID 2994509784706419</div>
</div>
<div class="edit-profile-btn">
<el-button type="primary">编辑资料</el-button>
</div>
</div>
<!-- 已发布作品区域 -->
<div class="published-works">
<div class="works-tabs">
<div class="tab active">已发布</div>
</div>
<div class="works-grid">
<div class="work-item" v-for="(work, index) in publishedWorks" :key="work.id" @click="openDetail(work)">
<div class="work-thumbnail">
<img :src="work.cover" :alt="work.title" />
<div class="work-overlay">
<div class="overlay-text">{{ work.text }}</div>
</div>
</div>
<div class="work-info">
<div class="work-title">{{ work.title }}</div>
<div class="work-meta">{{ work.id }} · {{ work.size }}</div>
</div>
<div class="work-actions" v-if="index === 0">
<el-button type="primary" class="create-similar-btn" @click.stop="goToCreate(work)">做同款</el-button>
</div>
<div class="work-director" v-else>
<span>DIRECTED BY VANNOCENT</span>
</div>
</div>
</div>
</div>
</main>
<!-- 作品详情模态框 -->
<el-dialog
v-model="detailDialogVisible"
:title="selectedItem?.title"
width="60%"
class="detail-dialog"
:modal="true"
:close-on-click-modal="true"
:close-on-press-escape="true"
@close="handleClose"
>
<div class="detail-content">
<div class="detail-left">
<div class="video-player">
<img :src="selectedItem?.cover" :alt="selectedItem?.title" class="video-thumbnail" />
<div class="play-overlay">
<div class="play-button"></div>
</div>
</div>
</div>
<div class="detail-right">
<div class="metadata-section">
<div class="metadata-item">
<span class="label">作品 ID</span>
<span class="value">{{ selectedItem?.id }}</span>
</div>
<div class="metadata-item">
<span class="label">文件大小</span>
<span class="value">{{ selectedItem?.size }}</span>
</div>
<div class="metadata-item">
<span class="label">创建时间</span>
<span class="value">{{ selectedItem?.createTime }}</span>
</div>
<div class="metadata-item">
<span class="label">分类</span>
<span class="value">{{ selectedItem?.category }}</span>
</div>
</div>
<div class="description-section">
<h3 class="section-title">描述</h3>
<p class="description-text">{{ getDescription(selectedItem) }}</p>
</div>
<!-- 操作按钮 -->
<div class="action-section">
<button class="create-similar-btn" @click="createSimilar">
做同款
</button>
</div>
</div>
</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElIcon, ElButton, ElTag, ElMessage, ElDialog } from 'element-plus'
import { User, Compass, Document, VideoPlay, Picture } from '@element-plus/icons-vue'
const router = useRouter()
// 模态框状态
const detailDialogVisible = ref(false)
const selectedItem = ref(null)
// 已发布作品数据
const publishedWorks = ref([
{
id: '2995000000001',
title: '文生视频作品 #1',
cover: '/images/backgrounds/welcome.jpg',
text: 'What Does it Mean To You',
size: '9 MB',
category: '文生视频',
createTime: '2025/01/15 14:30'
},
{
id: '2995000000002',
title: '文生视频作品 #2',
cover: '/images/backgrounds/welcome.jpg',
text: 'What Does it Mean To You',
size: '9 MB',
category: '文生视频',
createTime: '2025/01/14 16:45'
},
{
id: '2995000000003',
title: '文生视频作品 #3',
cover: '/images/backgrounds/welcome.jpg',
text: 'What Does it Mean To You',
size: '9 MB',
category: '文生视频',
createTime: '2025/01/13 09:20'
}
])
// 导航函数
const goToProfile = () => {
router.push('/profile')
}
const goToSubscription = () => {
router.push('/subscription')
}
const goToMyWorks = () => {
router.push('/works')
}
const goToImageToVideo = () => {
router.push('/image-to-video')
}
const goToStoryboard = () => {
router.push('/storyboard-video')
}
const goToCreate = (work) => {
// 跳转到文生视频创作页面
router.push('/text-to-video/create')
}
// 模态框相关函数
const openDetail = (work) => {
selectedItem.value = work
detailDialogVisible.value = true
}
const handleClose = () => {
detailDialogVisible.value = false
selectedItem.value = null
}
const getDescription = (item) => {
if (!item) return ''
return `这是一个${item.category}作品,展现了"What Does it Mean To You"的主题。作品通过AI技术生成具有独特的视觉风格和创意表达。`
}
const createSimilar = () => {
// 关闭模态框并跳转到创作页面
handleClose()
router.push('/text-to-video/create')
}
onMounted(() => {
// 页面初始化
})
</script>
<style scoped>
.text-to-video-page {
display: flex;
height: 100vh;
background: #0a0a0a;
color: #fff;
margin: 0;
padding: 0;
}
/* 左侧导航栏 */
.sidebar {
width: 280px;
background: #1a1a1a;
border-right: 1px solid #333;
padding: 24px 0;
}
.logo {
font-size: 18px;
font-weight: 600;
color: #fff;
text-align: center;
margin-bottom: 32px;
}
.nav-menu {
display: flex;
flex-direction: column;
gap: 8px;
padding: 0 20px;
}
.nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
color: #d1d5db;
}
.nav-item:hover {
background: #2a2a2a;
color: #fff;
}
.nav-item.active {
background: #3b82f6;
color: #fff;
}
.nav-divider {
height: 1px;
background: #333;
margin: 16px 0;
}
.sora-tag {
margin-left: 8px;
}
/* 分镜视频特殊样式 */
.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);
}
}
/* 主内容区域 */
.main-content {
flex: 1;
padding: 24px;
display: flex;
flex-direction: column;
gap: 24px;
}
/* 用户信息卡片 */
.user-info-card {
background: #1a1a1a;
border: 1px solid #333;
border-radius: 12px;
padding: 24px;
display: flex;
align-items: center;
gap: 20px;
}
.user-avatar {
width: 80px;
height: 80px;
border-radius: 50%;
background: #000;
display: flex;
align-items: center;
justify-content: center;
border: 2px solid #333;
}
.avatar-placeholder {
color: #fff;
font-size: 24px;
font-weight: bold;
letter-spacing: 2px;
}
.user-details {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.username {
font-size: 18px;
font-weight: 600;
color: #fff;
}
.profile-prompt {
font-size: 14px;
color: #9ca3af;
}
.user-id {
font-size: 12px;
color: #6b7280;
}
.edit-profile-btn {
margin-left: auto;
}
/* 已发布作品区域 */
.published-works {
display: flex;
flex-direction: column;
gap: 20px;
}
.works-tabs {
display: flex;
gap: 24px;
}
.tab {
padding: 8px 0;
color: #9ca3af;
cursor: pointer;
position: relative;
}
.tab.active {
color: #fff;
}
.tab.active::after {
content: '';
position: absolute;
bottom: -8px;
left: 0;
right: 0;
height: 2px;
background: #3b82f6;
}
.works-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
.work-item {
background: #1a1a1a;
border: 1px solid #333;
border-radius: 12px;
overflow: hidden;
transition: all 0.2s;
cursor: pointer;
}
.work-item:hover {
border-color: #3b82f6;
transform: translateY(-2px);
}
.work-thumbnail {
position: relative;
aspect-ratio: 16/9;
overflow: hidden;
}
.work-thumbnail img {
width: 100%;
height: 100%;
object-fit: cover;
}
.work-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.8));
padding: 20px;
}
.overlay-text {
font-size: 16px;
font-weight: 600;
color: #fff;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.8);
}
.work-info {
padding: 16px;
display: flex;
flex-direction: column;
gap: 8px;
}
.work-title {
font-size: 16px;
font-weight: 600;
color: #fff;
}
.work-meta {
font-size: 12px;
color: #9ca3af;
}
.work-actions {
padding: 0 16px 16px;
opacity: 0;
transition: opacity 0.2s ease;
}
.work-item:hover .work-actions {
opacity: 1;
}
.create-similar-btn {
width: 100%;
}
.work-director {
padding: 0 16px 16px;
text-align: center;
}
.work-director span {
font-size: 12px;
color: #6b7280;
font-style: italic;
}
/* 响应式设计 */
@media (max-width: 1024px) {
.sidebar {
width: 260px;
}
.works-grid {
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
}
}
@media (max-width: 768px) {
.text-to-video-page {
flex-direction: column;
}
.sidebar {
width: 100%;
height: auto;
}
.nav-menu {
flex-direction: row;
overflow-x: auto;
padding: 0 16px;
}
.nav-item {
white-space: nowrap;
}
.works-grid {
grid-template-columns: 1fr;
}
}
/* 模态框样式 */
:deep(.detail-dialog .el-dialog) {
background: #0a0a0a !important;
border-radius: 12px;
border: 1px solid #333 !important;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.8);
}
:deep(.detail-dialog .el-dialog__wrapper) {
background-color: rgba(0, 0, 0, 0.8) !important;
}
:deep(.detail-dialog .el-dialog__header) {
background: #0a0a0a !important;
padding: 16px 20px;
border-bottom: 1px solid #333;
}
:deep(.detail-dialog .el-dialog__title) {
color: #fff !important;
font-size: 18px;
font-weight: 600;
}
:deep(.detail-dialog .el-dialog__headerbtn) {
color: #fff !important;
}
:deep(.detail-dialog .el-dialog__body) {
background: #0a0a0a !important;
padding: 0 !important;
}
:deep(.detail-dialog .el-overlay) {
background-color: rgba(0, 0, 0, 0.8) !important;
}
/* 全局覆盖Element Plus默认样式 */
:deep(.el-dialog) {
background: #0a0a0a !important;
border: 1px solid #333 !important;
}
:deep(.el-dialog__wrapper) {
background-color: rgba(0, 0, 0, 0.8) !important;
}
:deep(.el-dialog__header) {
background: #0a0a0a !important;
}
:deep(.el-dialog__body) {
background: #0a0a0a !important;
}
:deep(.el-overlay) {
background-color: rgba(0, 0, 0, 0.8) !important;
}
.detail-content {
display: flex;
height: 50vh;
background: #0a0a0a;
}
.detail-left {
flex: 1;
padding: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.video-player {
position: relative;
width: 100%;
max-width: 400px;
aspect-ratio: 16/9;
border-radius: 8px;
overflow: hidden;
cursor: pointer;
}
.video-thumbnail {
width: 100%;
height: 100%;
object-fit: cover;
}
.play-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.2s ease;
}
.video-player:hover .play-overlay {
opacity: 1;
}
.play-button {
width: 60px;
height: 60px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.9);
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
color: #000;
font-weight: bold;
}
.detail-right {
flex: 1;
padding: 20px;
background: #0a0a0a;
display: flex;
flex-direction: column;
gap: 20px;
}
.metadata-section {
display: flex;
flex-direction: column;
gap: 12px;
}
.metadata-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #2a2a2a;
}
.metadata-item:last-child {
border-bottom: none;
}
.label {
font-size: 14px;
color: #9ca3af;
font-weight: 500;
}
.value {
font-size: 14px;
color: #fff;
font-weight: 600;
}
.description-section {
flex: 1;
}
.section-title {
font-size: 16px;
color: #fff;
font-weight: 600;
margin-bottom: 12px;
}
.description-text {
font-size: 14px;
color: #d1d5db;
line-height: 1.6;
margin: 0;
}
.action-section {
margin-top: auto;
}
.create-similar-btn {
width: 100%;
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
color: #fff;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
}
.create-similar-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
}
</style>

View File

@@ -0,0 +1,596 @@
<template>
<div class="text-to-video-create-page">
<!-- 顶部导航栏 -->
<header class="top-header">
<div class="header-left">
<button class="back-btn" @click="goBack">
首页
</button>
</div>
<div class="header-right">
<div class="credits-info">
<div class="credits-circle">25</div>
<span>| 首购优惠</span>
</div>
<div class="notification-icon">
🔔
<div class="notification-badge">5</div>
</div>
<div class="user-avatar">
👤
</div>
</div>
</header>
<!-- 主内容区域 -->
<div class="main-content">
<!-- 左侧设置面板 -->
<div class="left-panel">
<!-- 创作模式标签 -->
<div class="creation-tabs">
<div class="tab active">文生视频</div>
<div class="tab" @click="goToImageToVideo">图生视频</div>
<div class="tab" @click="goToStoryboardVideo">分镜视频</div>
</div>
<!-- 文本输入区域 -->
<div class="text-input-section">
<textarea
v-model="inputText"
placeholder="输入文字,描述想要生成的内容"
class="text-input"
rows="8"
></textarea>
<div class="optimize-btn">
<button class="optimize-button">
一键优化
</button>
</div>
</div>
<!-- 视频设置 -->
<div class="video-settings">
<div class="setting-item">
<label>比例</label>
<select v-model="aspectRatio" class="setting-select">
<option value="16:9">16:9</option>
<option value="9:16">9:16</option>
<option value="1:1">1:1</option>
</select>
</div>
<div class="setting-item">
<label>时长</label>
<select v-model="duration" class="setting-select">
<option value="5">5s</option>
<option value="10">10s</option>
<option value="15">15s</option>
</select>
</div>
<div class="setting-item">
<label>高清模式 (1080P)</label>
<div class="hd-setting">
<input type="checkbox" v-model="hdMode" class="hd-switch">
<span class="cost-text">开启消耗20积分</span>
</div>
</div>
</div>
<!-- 生成按钮 -->
<div class="generate-section">
<button class="generate-btn" @click="startGenerate">
开始生成
</button>
</div>
</div>
<!-- 右侧预览区域 -->
<div class="right-panel">
<div class="preview-area">
<div class="status-checkbox">
<input type="checkbox" v-model="inProgress" id="progress-checkbox">
<label for="progress-checkbox">进行中</label>
</div>
<div class="preview-content">
<div class="preview-placeholder">
<div class="placeholder-text">开始创作您的第一个作品吧!</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
// 表单数据
const inputText = ref('')
const aspectRatio = ref('16:9')
const duration = ref('5')
const hdMode = ref(false)
const inProgress = ref(false)
// 导航函数
const goBack = () => {
router.back()
}
const goToImageToVideo = () => {
router.push('/image-to-video/create')
}
const goToStoryboardVideo = () => {
router.push('/storyboard-video/create')
}
const startGenerate = () => {
if (!inputText.value.trim()) {
alert('请输入描述文字')
return
}
inProgress.value = true
alert('开始生成视频...')
// 模拟生成过程
setTimeout(() => {
inProgress.value = false
alert('视频生成完成!')
}, 3000)
}
</script>
<style scoped>
.text-to-video-create-page {
height: 100vh;
background: #0a0a0a;
color: #fff;
display: flex;
flex-direction: column;
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
/* 顶部导航栏 */
.top-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 32px;
background: #0a0a0a;
border-bottom: 1px solid #1f1f1f;
min-height: 60px;
}
.header-left {
display: flex;
align-items: center;
}
.back-btn {
background: none;
border: none;
color: #fff;
font-size: 16px;
cursor: pointer;
padding: 10px 20px;
border-radius: 8px;
transition: all 0.2s ease;
font-weight: 500;
}
.back-btn:hover {
background: #1a1a1a;
transform: translateX(-2px);
}
.header-right {
display: flex;
align-items: center;
gap: 24px;
}
.credits-info {
display: flex;
align-items: center;
gap: 12px;
color: #fff;
font-size: 14px;
font-weight: 500;
}
.credits-circle {
width: 36px;
height: 36px;
border-radius: 50%;
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 16px;
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
.notification-icon {
position: relative;
font-size: 24px;
cursor: pointer;
padding: 8px;
border-radius: 8px;
transition: background 0.2s ease;
}
.notification-icon:hover {
background: #1a1a1a;
}
.notification-badge {
position: absolute;
top: 2px;
right: 2px;
background: #ef4444;
color: #fff;
border-radius: 50%;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 700;
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.4);
}
.user-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: linear-gradient(135deg, #374151, #1f2937);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 20px;
transition: transform 0.2s ease;
}
.user-avatar:hover {
transform: scale(1.05);
}
/* 主内容区域 */
.main-content {
flex: 1;
display: grid;
grid-template-columns: 400px 1fr;
gap: 0;
height: calc(100vh - 100px);
}
/* 左侧面板 */
.left-panel {
background: #1a1a1a;
border-right: 1px solid #2a2a2a;
padding: 32px;
display: flex;
flex-direction: column;
gap: 32px;
overflow-y: auto;
}
/* 创作模式标签 */
.creation-tabs {
display: flex;
gap: 4px;
background: #0a0a0a;
padding: 4px;
border-radius: 12px;
}
.tab {
flex: 1;
padding: 12px 16px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
color: #9ca3af;
font-size: 14px;
font-weight: 500;
text-align: center;
}
.tab.active {
background: #3b82f6;
color: #fff;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
}
.tab:hover:not(.active) {
background: #2a2a2a;
color: #fff;
}
/* 文本输入区域 */
.text-input-section {
display: flex;
flex-direction: column;
gap: 16px;
}
.text-input {
width: 100%;
min-height: 140px;
padding: 16px;
background: #0a0a0a;
border: 2px solid #2a2a2a;
border-radius: 12px;
color: #fff;
font-size: 15px;
line-height: 1.6;
resize: vertical;
outline: none;
transition: all 0.2s ease;
font-family: inherit;
}
.text-input:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.text-input::placeholder {
color: #6b7280;
}
.optimize-btn {
display: flex;
justify-content: flex-end;
}
.optimize-button {
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
color: #fff;
border: none;
padding: 10px 20px;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
}
.optimize-button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
}
/* 视频设置 */
.video-settings {
display: flex;
flex-direction: column;
gap: 20px;
}
.setting-item {
display: flex;
flex-direction: column;
gap: 10px;
}
.setting-item label {
font-size: 14px;
color: #e5e7eb;
font-weight: 600;
}
.setting-select {
padding: 12px 16px;
background: #0a0a0a;
border: 2px solid #2a2a2a;
border-radius: 8px;
color: #fff;
font-size: 14px;
outline: none;
cursor: pointer;
transition: all 0.2s ease;
font-family: inherit;
}
.setting-select:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.setting-select:hover {
border-color: #374151;
}
.hd-setting {
display: flex;
align-items: center;
gap: 12px;
}
.hd-switch {
width: 20px;
height: 20px;
cursor: pointer;
accent-color: #3b82f6;
}
.cost-text {
font-size: 13px;
color: #9ca3af;
font-weight: 500;
}
/* 生成按钮 */
.generate-section {
margin-top: auto;
}
.generate-btn {
width: 100%;
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
color: #fff;
border: none;
padding: 16px 24px;
border-radius: 12px;
font-size: 16px;
font-weight: 700;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 4px 16px rgba(59, 130, 246, 0.3);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.generate-btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(59, 130, 246, 0.4);
}
.generate-btn:active {
transform: translateY(0);
}
.generate-btn:disabled {
background: #6b7280;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
/* 右侧面板 */
.right-panel {
background: #0a0a0a;
padding: 32px;
display: flex;
flex-direction: column;
}
.preview-area {
flex: 1;
display: flex;
flex-direction: column;
gap: 20px;
}
.status-checkbox {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
}
.status-checkbox input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
accent-color: #3b82f6;
}
.status-checkbox label {
font-size: 14px;
color: #e5e7eb;
cursor: pointer;
font-weight: 500;
}
.preview-content {
flex: 1;
background: #1a1a1a;
border: 2px solid #2a2a2a;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.preview-content:hover {
border-color: #374151;
}
.preview-placeholder {
text-align: center;
padding: 40px;
}
.placeholder-text {
font-size: 18px;
color: #6b7280;
font-weight: 500;
line-height: 1.5;
}
/* 响应式设计 */
@media (max-width: 1200px) {
.main-content {
grid-template-columns: 350px 1fr;
}
.left-panel {
padding: 24px;
}
.right-panel {
padding: 24px;
}
}
@media (max-width: 1024px) {
.main-content {
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
}
.left-panel {
border-right: none;
border-bottom: 1px solid #2a2a2a;
padding: 20px;
}
.right-panel {
padding: 20px;
}
}
@media (max-width: 768px) {
.top-header {
padding: 16px 20px;
}
.header-right {
gap: 16px;
}
.left-panel {
padding: 16px;
gap: 24px;
}
.right-panel {
padding: 16px;
}
.creation-tabs {
flex-direction: column;
gap: 8px;
}
.tab {
text-align: left;
}
}
</style>

View File

@@ -0,0 +1,756 @@
<template>
<div class="video-detail-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" @click="goToSubscription">
<el-icon><Compass /></el-icon>
<span>会员订阅</span>
</div>
<div class="nav-item active">
<el-icon><Document /></el-icon>
<span>我的作品</span>
</div>
</nav>
</aside>
<!-- 主内容区域 -->
<main class="main-content">
<!-- 左侧视频播放器区域 -->
<div class="video-player-section">
<div class="video-container">
<video
ref="videoPlayer"
class="video-player"
:src="videoData.videoUrl"
:poster="videoData.cover"
@loadedmetadata="onVideoLoaded"
@timeupdate="onTimeUpdate"
@ended="onVideoEnded"
>
您的浏览器不支持视频播放
</video>
<!-- 视频文字叠加 -->
<div class="video-overlay" v-if="videoData.overlayText">
<div class="overlay-text">{{ videoData.overlayText }}</div>
</div>
<!-- 播放控制栏 -->
<div class="video-controls">
<div class="control-left">
<button class="play-btn" @click="togglePlay">
<el-icon v-if="!isPlaying"><VideoPlay /></el-icon>
<el-icon v-else><Pause /></el-icon>
</button>
<div class="progress-container">
<div class="progress-bar" @click="seekTo">
<div class="progress-fill" :style="{ width: progressPercent + '%' }"></div>
</div>
<div class="time-display">{{ formatTime(currentTime) }} / {{ formatTime(duration) }}</div>
</div>
</div>
<div class="control-right">
<button class="control-btn" @click="toggleFullscreen">
<el-icon><FullScreen /></el-icon>
</button>
</div>
</div>
<!-- 右上角操作按钮 -->
<div class="video-actions">
<el-tooltip content="分享" placement="bottom">
<button class="action-btn" @click="shareVideo">
<el-icon><Share /></el-icon>
</button>
</el-tooltip>
<el-tooltip content="下载" placement="bottom">
<button class="action-btn" @click="downloadVideo">
<el-icon><Download /></el-icon>
</button>
</el-tooltip>
<el-tooltip content="删除" placement="bottom">
<button class="action-btn delete-btn" @click="deleteVideo">
<el-icon><Delete /></el-icon>
</button>
</el-tooltip>
</div>
</div>
</div>
<!-- 右侧详情侧边栏 -->
<div class="detail-sidebar">
<!-- 用户信息头部 -->
<div class="sidebar-header">
<div class="user-info">
<div class="avatar">
<el-icon><User /></el-icon>
</div>
<div class="username">{{ videoData.username }}</div>
</div>
<button class="close-btn" @click="goBack">
<el-icon><Close /></el-icon>
</button>
</div>
<!-- 标签页 -->
<div class="tabs">
<div class="tab active">视频详情</div>
<div class="tab">文生视频</div>
</div>
<!-- 描述区域 -->
<div class="description-section">
<h3 class="section-title">描述</h3>
<p class="description-text">{{ videoData.description }}</p>
</div>
<!-- 元数据区域 -->
<div class="metadata-section">
<div class="metadata-item">
<span class="label">创建时间</span>
<span class="value">{{ videoData.createTime }}</span>
</div>
<div class="metadata-item">
<span class="label">视频 ID</span>
<span class="value">{{ videoData.id }}</span>
</div>
<div class="metadata-item">
<span class="label">时长</span>
<span class="value">{{ videoData.duration }}s</span>
</div>
<div class="metadata-item">
<span class="label">清晰度</span>
<span class="value">{{ videoData.resolution }}</span>
</div>
<div class="metadata-item">
<span class="label">分类</span>
<span class="value">{{ videoData.category }}</span>
</div>
<div class="metadata-item">
<span class="label">宽高比</span>
<span class="value">{{ videoData.aspectRatio }}</span>
</div>
</div>
<!-- 操作按钮 -->
<div class="action-section">
<button class="create-similar-btn" @click="createSimilar">
做同款
</button>
</div>
</div>
</main>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
VideoPlay,
VideoPause as Pause,
FullScreen,
Share,
Download,
Delete,
User,
Compass,
Document,
Close
} from '@element-plus/icons-vue'
const route = useRoute()
const router = useRouter()
// 视频播放器相关
const videoPlayer = ref(null)
const isPlaying = ref(false)
const currentTime = ref(0)
const duration = ref(0)
const progressPercent = ref(0)
// 视频数据
const videoData = ref({
id: '2995697841305810',
username: 'mingzi_FBx7foZYDS7inL',
title: 'What Does it Mean To You',
overlayText: 'What Does it Mean To You',
description: '影片捕捉了暴风雪中的午夜时分,坐落在积雪覆盖的悬崖顶上的孤立灯塔。相机逐渐放大灯塔的灯光,穿透飞舞的雪花,投射出幽幽的光芒。在白茫茫的环境中,灯塔的黑色轮廓显得格外醒目,呼啸的风声和远处海浪的撞击声增强了孤独的氛围。这一场景展示了灯塔的孤独力量。',
createTime: '2025/10/17 13:41',
duration: 5,
resolution: '1080p',
category: '文生视频',
aspectRatio: '16:9',
videoUrl: '/images/backgrounds/welcome.jpg', // 临时使用图片作为视频
cover: '/images/backgrounds/welcome.jpg'
})
// 根据ID获取视频数据
const getVideoData = (id) => {
// 模拟不同ID对应不同的分类
const videoConfigs = {
'2995000000001': { category: '参考图', title: '图片作品 #1' },
'2995000000002': { category: '参考图', title: '图片作品 #2' },
'2995000000003': { category: '文生视频', title: '视频作品 #3' },
'2995000000004': { category: '图生视频', title: '视频作品 #4' }
}
const config = videoConfigs[id] || videoConfigs['2995000000003']
return {
id: id,
username: 'mingzi_FBx7foZYDS7inL',
title: config.title,
overlayText: config.title,
description: '影片捕捉了暴风雪中的午夜时分,坐落在积雪覆盖的悬崖顶上的孤立灯塔。相机逐渐放大灯塔的灯光,穿透飞舞的雪花,投射出幽幽的光芒。在白茫茫的环境中,灯塔的黑色轮廓显得格外醒目,呼啸的风声和远处海浪的撞击声增强了孤独的氛围。这一场景展示了灯塔的孤独力量。',
createTime: '2025/10/17 13:41',
duration: 5,
resolution: '1080p',
category: config.category,
aspectRatio: '16:9',
videoUrl: '/images/backgrounds/welcome.jpg',
cover: '/images/backgrounds/welcome.jpg'
}
}
// 视频播放控制
const togglePlay = () => {
if (!videoPlayer.value) return
if (isPlaying.value) {
videoPlayer.value.pause()
} else {
videoPlayer.value.play()
}
}
const onVideoLoaded = () => {
duration.value = videoPlayer.value.duration
}
const onTimeUpdate = () => {
currentTime.value = videoPlayer.value.currentTime
progressPercent.value = (currentTime.value / duration.value) * 100
}
const onVideoEnded = () => {
isPlaying.value = false
}
const seekTo = (event) => {
if (!videoPlayer.value) return
const rect = event.currentTarget.getBoundingClientRect()
const clickX = event.clientX - rect.left
const percentage = clickX / rect.width
const newTime = percentage * duration.value
videoPlayer.value.currentTime = newTime
}
const formatTime = (time) => {
const minutes = Math.floor(time / 60)
const seconds = Math.floor(time % 60)
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
}
const toggleFullscreen = () => {
if (!videoPlayer.value) return
if (document.fullscreenElement) {
document.exitFullscreen()
} else {
videoPlayer.value.requestFullscreen()
}
}
// 操作功能
const shareVideo = () => {
ElMessage.info('分享功能开发中')
}
const downloadVideo = () => {
ElMessage.success('开始下载视频')
}
const deleteVideo = async () => {
try {
await ElMessageBox.confirm('确定删除这个视频吗?', '删除确认', {
type: 'warning',
confirmButtonText: '删除',
cancelButtonText: '取消'
})
ElMessage.success('视频已删除')
router.back()
} catch (_) {}
}
const createSimilar = () => {
ElMessage.info('跳转到文生视频创作页面')
// router.push('/create-video')
}
// 导航函数
const goToProfile = () => {
router.push('/profile')
}
const goToSubscription = () => {
router.push('/subscription')
}
const goBack = () => {
router.back()
}
// 监听视频播放状态变化
const handlePlay = () => {
isPlaying.value = true
}
const handlePause = () => {
isPlaying.value = false
}
onMounted(() => {
// 根据路由参数更新视频数据
const videoId = route.params.id
if (videoId) {
videoData.value = getVideoData(videoId)
}
if (videoPlayer.value) {
videoPlayer.value.addEventListener('play', handlePlay)
videoPlayer.value.addEventListener('pause', handlePause)
}
})
onUnmounted(() => {
if (videoPlayer.value) {
videoPlayer.value.removeEventListener('play', handlePlay)
videoPlayer.value.removeEventListener('pause', handlePause)
}
})
</script>
<style scoped>
.video-detail-page {
display: flex;
height: 100vh;
background: #0a0a0a;
color: #fff;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
/* 左侧导航栏 */
.sidebar {
width: 280px;
background: #1a1a1a;
padding: 24px 0;
border-right: 1px solid #1a1a1a;
flex-shrink: 0;
display: block;
position: relative;
}
.logo {
padding: 0 24px 32px;
font-size: 20px;
font-weight: 500;
color: white;
}
.nav-menu {
padding: 0 20px;
}
.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;
}
/* 主内容区域 */
.main-content {
flex: 1;
display: flex;
background: #0a0a0a;
overflow: hidden;
}
/* 左侧视频播放器区域 */
.video-player-section {
flex: 2;
display: flex;
align-items: center;
justify-content: center;
background: #000;
position: relative;
}
.video-container {
position: relative;
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
}
.video-player {
width: 100%;
height: 100%;
object-fit: contain;
background: #000;
}
.video-overlay {
position: absolute;
bottom: 80px;
left: 20px;
z-index: 10;
}
.overlay-text {
font-family: 'Brush Script MT', cursive;
font-size: 24px;
color: #8b5cf6;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
font-weight: bold;
}
/* 视频控制栏 */
.video-controls {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.8));
padding: 20px;
display: flex;
align-items: center;
justify-content: space-between;
}
.control-left {
display: flex;
align-items: center;
gap: 15px;
flex: 1;
}
.play-btn {
background: none;
border: none;
color: #fff;
font-size: 20px;
cursor: pointer;
padding: 8px;
border-radius: 50%;
transition: background-color 0.3s;
}
.play-btn:hover {
background: rgba(255, 255, 255, 0.1);
}
.progress-container {
flex: 1;
display: flex;
align-items: center;
gap: 10px;
}
.progress-bar {
flex: 1;
height: 4px;
background: rgba(255, 255, 255, 0.3);
border-radius: 2px;
cursor: pointer;
position: relative;
}
.progress-fill {
height: 100%;
background: #409eff;
border-radius: 2px;
transition: width 0.1s;
}
.time-display {
color: #fff;
font-size: 14px;
white-space: nowrap;
}
.control-right {
display: flex;
align-items: center;
}
.control-btn {
background: none;
border: none;
color: #fff;
font-size: 18px;
cursor: pointer;
padding: 8px;
border-radius: 4px;
transition: background-color 0.3s;
}
.control-btn:hover {
background: rgba(255, 255, 255, 0.1);
}
/* 右上角操作按钮 */
.video-actions {
position: absolute;
top: 20px;
right: 20px;
display: flex;
gap: 10px;
z-index: 10;
}
.action-btn {
background: rgba(0, 0, 0, 0.6);
border: none;
color: #fff;
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s;
}
.action-btn:hover {
background: rgba(0, 0, 0, 0.8);
transform: scale(1.1);
}
.delete-btn:hover {
background: rgba(220, 38, 38, 0.8);
}
/* 右侧详情侧边栏 */
.detail-sidebar {
flex: 1;
background: #1a1a1a;
border-radius: 12px 0 0 0;
padding: 24px;
display: flex;
flex-direction: column;
gap: 24px;
overflow-y: auto;
}
.sidebar-header {
display: flex;
flex-direction: column;
gap: 16px;
}
.user-info {
display: flex;
align-items: center;
gap: 12px;
}
.avatar {
width: 40px;
height: 40px;
background: #409eff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 18px;
}
.username {
font-size: 16px;
font-weight: 500;
color: #fff;
}
.tabs {
display: flex;
gap: 0;
}
.tab {
padding: 8px 16px;
background: transparent;
color: #9ca3af;
cursor: pointer;
border-radius: 6px;
transition: all 0.3s;
font-size: 14px;
}
.tab.active {
background: #409eff;
color: #fff;
}
.tab:hover:not(.active) {
background: rgba(255, 255, 255, 0.1);
color: #fff;
}
.description-section {
display: flex;
flex-direction: column;
gap: 12px;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #fff;
margin: 0;
}
.description-text {
font-size: 14px;
line-height: 1.6;
color: #d1d5db;
margin: 0;
}
.metadata-section {
display: flex;
flex-direction: column;
gap: 12px;
}
.metadata-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
}
.label {
font-size: 14px;
color: #9ca3af;
}
.value {
font-size: 14px;
color: #fff;
font-weight: 500;
}
.action-section {
margin-top: auto;
padding-top: 20px;
}
.create-similar-btn {
width: 100%;
background: #409eff;
color: #fff;
border: none;
border-radius: 8px;
padding: 12px 24px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
}
.create-similar-btn:hover {
background: #337ecc;
transform: translateY(-2px);
}
.create-similar-btn:active {
transform: translateY(0);
}
/* 响应式设计 */
@media (max-width: 1024px) {
.video-detail-page {
flex-direction: column;
}
.video-player-section {
flex: 1;
min-height: 50vh;
}
.detail-sidebar {
flex: 1;
border-radius: 0;
}
}
@media (max-width: 768px) {
.video-overlay {
bottom: 60px;
left: 10px;
}
.overlay-text {
font-size: 18px;
}
.video-controls {
padding: 15px;
}
.video-actions {
top: 15px;
right: 15px;
gap: 8px;
}
.action-btn {
width: 35px;
height: 35px;
}
.detail-sidebar {
padding: 16px;
gap: 16px;
}
}
</style>

View File

@@ -0,0 +1,223 @@
<template>
<div class="welcome-page">
<!-- 导航栏 -->
<header class="navbar">
<div class="navbar-content">
<div class="logo">Logo</div>
<nav class="nav-links">
<a href="#" class="nav-link">文生视频</a>
<a href="#" class="nav-link">图生视频</a>
<a href="#" class="nav-link">分镜视频</a>
<a href="#" class="nav-link">订阅套餐</a>
</nav>
<button class="nav-button">开始体验</button>
</div>
</header>
<!-- 主要内容 -->
<main class="content">
<h1 class="title">
<span class="title-line">
<span class="bright-text">智创</span><span class="fade-text">无限,</span>
</span>
<span class="title-line">
<span class="bright-text">灵感</span><span class="fade-text">变现</span>
</span>
</h1>
<button class="main-button">立即体验</button>
</main>
<!-- 背景光影效果已删除 -->
</div>
</template>
<script setup>
</script>
<style scoped>
.welcome-page {
min-height: 100vh;
background: url('/images/backgrounds/welcome.jpg') center/cover no-repeat;
position: relative;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
/* 导航栏 */
.navbar {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
width: 95%;
max-width: 1200px;
background: rgba(26, 26, 46, 0.8);
backdrop-filter: blur(10px);
z-index: 1000;
height: 60px;
border-radius: 30px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.navbar-content {
width: 100%;
margin: 0 auto;
padding: 0 20px;
height: 100%;
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
}
.logo {
color: white;
font-size: 18px;
font-weight: 500;
}
.nav-links {
display: flex;
gap: 30px;
margin-left: auto;
margin-right: 15px;
}
.nav-link {
color: white;
text-decoration: none;
font-size: 16px;
font-weight: 400;
transition: color 0.3s ease;
}
.nav-link:hover {
color: #4a9eff;
}
.nav-button {
background: rgba(74, 158, 255, 0.8);
border: 1px solid rgba(255, 255, 255, 0.3);
color: white;
padding: 10px 20px;
border-radius: 20px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
}
.nav-button:hover {
background: rgba(74, 158, 255, 1);
transform: translateY(-2px);
}
/* 主要内容 */
.content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
text-align: center;
padding-top: 80px;
position: relative;
z-index: 10;
}
.title {
font-size: 6.5rem;
font-weight: 700;
color: white;
line-height: 1.1;
margin-bottom: 60px;
letter-spacing: -0.03em;
text-shadow: 0 4px 20px rgba(0, 0, 0, 0.7);
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
}
.title-line {
display: block;
text-align: center;
width: 100%;
}
.bright-text {
color: white;
opacity: 1;
}
.fade-text {
color: white;
opacity: 0.6;
}
.main-button {
background: linear-gradient(90deg, rgba(74, 158, 255, 0.8) 0%, rgba(255, 255, 255, 0.9) 100%);
border: none;
color: white;
padding: 22px 60px;
border-radius: 50px;
font-size: 22px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 6px 25px rgba(74, 158, 255, 0.3);
position: relative;
overflow: hidden;
backdrop-filter: blur(10px);
}
.main-button:hover {
transform: translateY(-4px);
box-shadow: 0 12px 40px rgba(74, 158, 255, 0.5);
}
.main-button:active {
transform: translateY(-2px);
}
/* 背景光影效果已删除 */
/* 响应式设计 */
@media (max-width: 1024px) {
.title {
font-size: 5.5rem;
margin-bottom: 50px;
}
.main-button {
padding: 20px 50px;
font-size: 20px;
}
}
@media (max-width: 768px) {
.nav-links {
display: none;
}
.title {
font-size: 4rem;
line-height: 1.2;
margin-bottom: 40px;
}
.main-button {
padding: 18px 40px;
font-size: 18px;
}
}
@media (max-width: 480px) {
.title {
font-size: 3rem;
line-height: 1.3;
}
.main-button {
padding: 16px 35px;
font-size: 16px;
}
}
</style>