Initial commit: AIGC项目完整代码
This commit is contained in:
21
demo/frontend/src/App-backup.vue
Normal file
21
demo/frontend/src/App-backup.vue
Normal 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
438
demo/frontend/src/App.vue
Normal 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>
|
||||
60
demo/frontend/src/api/auth.js
Normal file
60
demo/frontend/src/api/auth.js
Normal 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 }
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
38
demo/frontend/src/api/dashboard.js
Normal file
38
demo/frontend/src/api/dashboard.js
Normal 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')
|
||||
}
|
||||
}
|
||||
62
demo/frontend/src/api/orders.js
Normal file
62
demo/frontend/src/api/orders.js
Normal 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')
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
62
demo/frontend/src/api/payments.js
Normal file
62
demo/frontend/src/api/payments.js
Normal 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')
|
||||
}
|
||||
68
demo/frontend/src/api/request.js
Normal file
68
demo/frontend/src/api/request.js
Normal 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
|
||||
84
demo/frontend/src/components/Footer.vue
Normal file
84
demo/frontend/src/components/Footer.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<el-footer class="footer">
|
||||
<div class="footer-content">
|
||||
<div class="footer-info">
|
||||
<p>© 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>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
268
demo/frontend/src/components/NavBar.vue
Normal file
268
demo/frontend/src/components/NavBar.vue
Normal 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
27
demo/frontend/src/main.js
Normal 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')
|
||||
229
demo/frontend/src/router/index.js
Normal file
229
demo/frontend/src/router/index.js
Normal 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
|
||||
219
demo/frontend/src/stores/orders.js
Normal file
219
demo/frontend/src/stores/orders.js
Normal 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
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
159
demo/frontend/src/stores/user.js
Normal file
159
demo/frontend/src/stores/user.js
Normal 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
|
||||
}
|
||||
})
|
||||
556
demo/frontend/src/views/AdminDashboard.vue
Normal file
556
demo/frontend/src/views/AdminDashboard.vue
Normal 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>
|
||||
784
demo/frontend/src/views/AdminOrders.vue
Normal file
784
demo/frontend/src/views/AdminOrders.vue
Normal 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>
|
||||
|
||||
|
||||
745
demo/frontend/src/views/AdminUsers.vue
Normal file
745
demo/frontend/src/views/AdminUsers.vue
Normal 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>
|
||||
621
demo/frontend/src/views/Dashboard.vue
Normal file
621
demo/frontend/src/views/Dashboard.vue
Normal 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>
|
||||
436
demo/frontend/src/views/Home.vue
Normal file
436
demo/frontend/src/views/Home.vue
Normal 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>
|
||||
749
demo/frontend/src/views/ImageToVideo.vue
Normal file
749
demo/frontend/src/views/ImageToVideo.vue
Normal 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>
|
||||
781
demo/frontend/src/views/ImageToVideoCreate.vue
Normal file
781
demo/frontend/src/views/ImageToVideoCreate.vue
Normal 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>
|
||||
|
||||
|
||||
|
||||
587
demo/frontend/src/views/ImageToVideoDetail.vue
Normal file
587
demo/frontend/src/views/ImageToVideoDetail.vue
Normal 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>
|
||||
504
demo/frontend/src/views/Login.vue
Normal file
504
demo/frontend/src/views/Login.vue
Normal 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>
|
||||
794
demo/frontend/src/views/MyWorks.vue
Normal file
794
demo/frontend/src/views/MyWorks.vue
Normal 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>
|
||||
|
||||
|
||||
381
demo/frontend/src/views/OrderCreate.vue
Normal file
381
demo/frontend/src/views/OrderCreate.vue
Normal 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>
|
||||
|
||||
|
||||
201
demo/frontend/src/views/OrderDetail.vue
Normal file
201
demo/frontend/src/views/OrderDetail.vue
Normal 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>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
525
demo/frontend/src/views/Orders.vue
Normal file
525
demo/frontend/src/views/Orders.vue
Normal 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>
|
||||
|
||||
|
||||
254
demo/frontend/src/views/PaymentCreate.vue
Normal file
254
demo/frontend/src/views/PaymentCreate.vue
Normal 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>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
693
demo/frontend/src/views/Payments.vue
Normal file
693
demo/frontend/src/views/Payments.vue
Normal 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>
|
||||
533
demo/frontend/src/views/Profile.vue
Normal file
533
demo/frontend/src/views/Profile.vue
Normal 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>
|
||||
502
demo/frontend/src/views/Register.vue
Normal file
502
demo/frontend/src/views/Register.vue
Normal 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>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
13
demo/frontend/src/views/SimpleTest.vue
Normal file
13
demo/frontend/src/views/SimpleTest.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>测试页面</h1>
|
||||
<p>如果您能看到这个页面,说明Vue应用正常工作。</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
console.log('测试页面加载成功')
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
752
demo/frontend/src/views/StoryboardVideo.vue
Normal file
752
demo/frontend/src/views/StoryboardVideo.vue
Normal 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>
|
||||
|
||||
|
||||
|
||||
732
demo/frontend/src/views/StoryboardVideoCreate.vue
Normal file
732
demo/frontend/src/views/StoryboardVideoCreate.vue
Normal 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>
|
||||
|
||||
|
||||
|
||||
557
demo/frontend/src/views/Subscription.vue
Normal file
557
demo/frontend/src/views/Subscription.vue
Normal 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>
|
||||
750
demo/frontend/src/views/TextToVideo.vue
Normal file
750
demo/frontend/src/views/TextToVideo.vue
Normal 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>
|
||||
596
demo/frontend/src/views/TextToVideoCreate.vue
Normal file
596
demo/frontend/src/views/TextToVideoCreate.vue
Normal 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>
|
||||
756
demo/frontend/src/views/VideoDetail.vue
Normal file
756
demo/frontend/src/views/VideoDetail.vue
Normal 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>
|
||||
223
demo/frontend/src/views/Welcome.vue
Normal file
223
demo/frontend/src/views/Welcome.vue
Normal 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>
|
||||
Reference in New Issue
Block a user