chore: update project files
This commit is contained in:
14
demo/frontend/.claude/settings.local.json
Normal file
14
demo/frontend/.claude/settings.local.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(if exist dist rmdir /s /q dist)",
|
||||
"Bash(npm run build:*)",
|
||||
"Bash(find:*)",
|
||||
"Bash(npm run dev:*)",
|
||||
"Bash(npm install)",
|
||||
"Bash(npm cache clean:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
3
demo/frontend/.env.development
Normal file
3
demo/frontend/.env.development
Normal file
@@ -0,0 +1,3 @@
|
||||
# 开发环境配置(Vue CLI)
|
||||
VUE_APP_API_URL=http://localhost:8080/api
|
||||
NODE_ENV=development
|
||||
3
demo/frontend/.env.development.vite
Normal file
3
demo/frontend/.env.development.vite
Normal file
@@ -0,0 +1,3 @@
|
||||
# 开发环境配置(Vite - 项目当前使用)
|
||||
VITE_APP_API_URL=http://localhost:8080/api
|
||||
NODE_ENV=development
|
||||
2
demo/frontend/.env.production
Normal file
2
demo/frontend/.env.production
Normal file
@@ -0,0 +1,2 @@
|
||||
VUE_APP_API_URL=/api
|
||||
NODE_ENV=production
|
||||
4
demo/frontend/.env.production.vite
Normal file
4
demo/frontend/.env.production.vite
Normal file
@@ -0,0 +1,4 @@
|
||||
# Vite 环境变量(项目当前使用 Vite)
|
||||
# Vite 使用 VITE_ 前缀,而不是 VUE_APP_
|
||||
VITE_APP_API_URL=/api
|
||||
NODE_ENV=production
|
||||
@@ -1,449 +0,0 @@
|
||||
# AIGC Demo - Vue.js 前端
|
||||
|
||||
这是一个基于 Vue.js 3 + Element Plus 的现代化前端应用,为 AIGC Demo 项目提供用户界面。
|
||||
|
||||
## 🚀 技术栈
|
||||
|
||||
- **Vue.js 3** - 渐进式 JavaScript 框架
|
||||
- **Element Plus** - Vue 3 组件库
|
||||
- **Vue Router** - 官方路由管理器
|
||||
- **Pinia** - Vue 状态管理库
|
||||
- **Axios** - HTTP 客户端
|
||||
- **Vite** - 现代化构建工具
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
```
|
||||
frontend/
|
||||
├── src/
|
||||
│ ├── components/ # 公共组件
|
||||
│ │ ├── NavBar.vue # 导航栏
|
||||
│ │ └── Footer.vue # 页脚
|
||||
│ ├── views/ # 页面组件
|
||||
│ │ ├── Home.vue # 首页
|
||||
│ │ ├── Login.vue # 登录页
|
||||
│ │ ├── Register.vue # 注册页
|
||||
│ │ ├── Orders.vue # 订单列表
|
||||
│ │ └── ...
|
||||
│ ├── router/ # 路由配置
|
||||
│ │ └── index.js
|
||||
│ ├── stores/ # 状态管理
|
||||
│ │ ├── user.js # 用户状态
|
||||
│ │ └── orders.js # 订单状态
|
||||
│ ├── api/ # API 接口
|
||||
│ │ ├── request.js # Axios 配置
|
||||
│ │ ├── auth.js # 认证接口
|
||||
│ │ ├── orders.js # 订单接口
|
||||
│ │ └── payments.js # 支付接口
|
||||
│ ├── assets/ # 静态资源
|
||||
│ ├── utils/ # 工具函数
|
||||
│ ├── App.vue # 根组件
|
||||
│ └── main.js # 入口文件
|
||||
├── index.html # HTML 模板
|
||||
├── package.json # 依赖配置
|
||||
├── vite.config.js # Vite 配置
|
||||
└── README.md # 说明文档
|
||||
```
|
||||
|
||||
## 🛠️ 安装和运行
|
||||
|
||||
### 前置要求
|
||||
|
||||
- Node.js 16+
|
||||
- npm 或 yarn
|
||||
|
||||
### 安装依赖
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
```
|
||||
|
||||
### 开发模式
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
访问 http://localhost:3000
|
||||
|
||||
### 构建生产版本
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
### 预览生产版本
|
||||
|
||||
```bash
|
||||
npm run preview
|
||||
```
|
||||
|
||||
## 🔧 配置说明
|
||||
|
||||
### Vite 配置
|
||||
|
||||
```javascript
|
||||
// vite.config.js
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src')
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8080', // 后端服务地址
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### API 代理
|
||||
|
||||
前端开发服务器会自动将 `/api` 请求代理到后端服务器 `http://localhost:8080`,这样避免了跨域问题。
|
||||
|
||||
## 📱 功能特性
|
||||
|
||||
### 🎨 现代化 UI
|
||||
- **Element Plus** 组件库,提供丰富的 UI 组件
|
||||
- **响应式设计**,支持桌面、平板、手机
|
||||
- **主题定制**,统一的视觉风格
|
||||
- **图标支持**,Element Plus Icons
|
||||
|
||||
### 🔐 用户认证
|
||||
- **登录/注册**,完整的用户认证流程
|
||||
- **表单验证**,实时验证用户输入
|
||||
- **状态管理**,Pinia 管理用户状态
|
||||
- **路由守卫**,保护需要认证的页面
|
||||
|
||||
### 📋 订单管理
|
||||
- **订单列表**,分页、筛选、搜索
|
||||
- **订单详情**,完整信息展示
|
||||
- **订单创建**,动态表单
|
||||
- **状态管理**,订单状态流转
|
||||
|
||||
### 💳 支付集成
|
||||
- **多种支付方式**,支付宝、PayPal
|
||||
- **支付状态跟踪**,实时状态更新
|
||||
- **支付记录**,完整的支付历史
|
||||
|
||||
### 👨💼 管理功能
|
||||
- **用户管理**,用户列表和操作
|
||||
- **订单管理**,管理员订单管理
|
||||
- **权限控制**,基于角色的访问控制
|
||||
|
||||
## 🔄 状态管理
|
||||
|
||||
### 用户状态 (stores/user.js)
|
||||
|
||||
```javascript
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
const user = ref(null)
|
||||
const token = ref(localStorage.getItem('token'))
|
||||
const isAuthenticated = computed(() => !!token.value && !!user.value)
|
||||
|
||||
const loginUser = async (credentials) => { /* ... */ }
|
||||
const registerUser = async (userData) => { /* ... */ }
|
||||
const logoutUser = async () => { /* ... */ }
|
||||
|
||||
return { user, token, isAuthenticated, loginUser, registerUser, logoutUser }
|
||||
})
|
||||
```
|
||||
|
||||
### 订单状态 (stores/orders.js)
|
||||
|
||||
```javascript
|
||||
export const useOrderStore = defineStore('orders', () => {
|
||||
const orders = ref([])
|
||||
const currentOrder = ref(null)
|
||||
const loading = ref(false)
|
||||
|
||||
const fetchOrders = async (params) => { /* ... */ }
|
||||
const createNewOrder = async (orderData) => { /* ... */ }
|
||||
const updateOrder = async (id, status) => { /* ... */ }
|
||||
|
||||
return { orders, currentOrder, loading, fetchOrders, createNewOrder, updateOrder }
|
||||
})
|
||||
```
|
||||
|
||||
## 🌐 API 接口
|
||||
|
||||
### 认证接口
|
||||
|
||||
```javascript
|
||||
// 登录
|
||||
POST /api/auth/login
|
||||
{
|
||||
"username": "user",
|
||||
"password": "password"
|
||||
}
|
||||
|
||||
// 注册
|
||||
POST /api/auth/register
|
||||
{
|
||||
"username": "newuser",
|
||||
"email": "user@example.com",
|
||||
"password": "password"
|
||||
}
|
||||
|
||||
// 获取当前用户
|
||||
GET /api/auth/me
|
||||
```
|
||||
|
||||
### 订单接口
|
||||
|
||||
```javascript
|
||||
// 获取订单列表
|
||||
GET /api/orders?page=0&size=10&status=PENDING
|
||||
|
||||
// 获取订单详情
|
||||
GET /api/orders/{id}
|
||||
|
||||
// 创建订单
|
||||
POST /api/orders/create
|
||||
{
|
||||
"orderType": "PRODUCT",
|
||||
"currency": "CNY",
|
||||
"orderItems": [...]
|
||||
}
|
||||
|
||||
// 更新订单状态
|
||||
POST /api/orders/{id}/status
|
||||
{
|
||||
"status": "PAID",
|
||||
"notes": "备注"
|
||||
}
|
||||
```
|
||||
|
||||
## 🎯 路由配置
|
||||
|
||||
```javascript
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: Home,
|
||||
meta: { title: '首页' }
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: Login,
|
||||
meta: { title: '登录', guest: true }
|
||||
},
|
||||
{
|
||||
path: '/orders',
|
||||
name: 'Orders',
|
||||
component: Orders,
|
||||
meta: { title: '订单管理', requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/admin/orders',
|
||||
name: 'AdminOrders',
|
||||
component: AdminOrders,
|
||||
meta: { title: '订单管理', requiresAuth: true, requiresAdmin: true }
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## 🔒 权限控制
|
||||
|
||||
### 路由守卫
|
||||
|
||||
```javascript
|
||||
router.beforeEach((to, from, next) => {
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 检查是否需要认证
|
||||
if (to.meta.requiresAuth && !userStore.isAuthenticated) {
|
||||
next('/login')
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否需要管理员权限
|
||||
if (to.meta.requiresAdmin && !userStore.isAdmin) {
|
||||
next('/')
|
||||
return
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
```
|
||||
|
||||
### API 拦截器
|
||||
|
||||
```javascript
|
||||
// 请求拦截器 - 添加认证头
|
||||
api.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
})
|
||||
|
||||
// 响应拦截器 - 处理错误
|
||||
api.interceptors.response.use(
|
||||
(response) => response.data,
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
// 未授权,跳转到登录页
|
||||
localStorage.removeItem('token')
|
||||
window.location.href = '/login'
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## 📱 响应式设计
|
||||
|
||||
### 断点配置
|
||||
|
||||
```css
|
||||
/* 移动端 */
|
||||
@media (max-width: 768px) {
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
/* 平板 */
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.content {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 桌面端 */
|
||||
@media (min-width: 1025px) {
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🚀 部署
|
||||
|
||||
### 构建生产版本
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
构建完成后,`dist` 目录包含所有静态文件。
|
||||
|
||||
### Nginx 配置
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
|
||||
location / {
|
||||
root /path/to/dist;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://localhost:8080;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 开发工具
|
||||
|
||||
### VS Code 推荐插件
|
||||
|
||||
- **Vue Language Features (Volar)** - Vue 3 支持
|
||||
- **TypeScript Vue Plugin (Volar)** - TypeScript 支持
|
||||
- **ESLint** - 代码检查
|
||||
- **Prettier** - 代码格式化
|
||||
- **Auto Rename Tag** - 自动重命名标签
|
||||
- **Bracket Pair Colorizer** - 括号配对着色
|
||||
|
||||
### 调试工具
|
||||
|
||||
- **Vue DevTools** - Vue 开发者工具
|
||||
- **Pinia DevTools** - Pinia 状态管理工具
|
||||
- **Element Plus DevTools** - Element Plus 组件调试
|
||||
|
||||
## 📝 开发规范
|
||||
|
||||
### 组件命名
|
||||
|
||||
- 组件文件名使用 PascalCase:`UserProfile.vue`
|
||||
- 组件名使用 PascalCase:`UserProfile`
|
||||
|
||||
### 文件结构
|
||||
|
||||
```
|
||||
components/
|
||||
├── common/ # 通用组件
|
||||
├── layout/ # 布局组件
|
||||
└── business/ # 业务组件
|
||||
```
|
||||
|
||||
### 代码风格
|
||||
|
||||
- 使用 Composition API
|
||||
- 优先使用 `<script setup>` 语法
|
||||
- 使用 TypeScript 类型定义
|
||||
- 遵循 ESLint 规则
|
||||
|
||||
## 🤝 贡献指南
|
||||
|
||||
1. Fork 项目
|
||||
2. 创建功能分支:`git checkout -b feature/new-feature`
|
||||
3. 提交更改:`git commit -am 'Add new feature'`
|
||||
4. 推送分支:`git push origin feature/new-feature`
|
||||
5. 提交 Pull Request
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
MIT License
|
||||
|
||||
---
|
||||
|
||||
**注意**: 这是一个演示项目,生产环境使用前请进行充分的安全测试和性能优化。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
381
demo/frontend/package-lock.json
generated
381
demo/frontend/package-lock.json
generated
@@ -13,7 +13,9 @@
|
||||
"axios": "^1.5.0",
|
||||
"element-plus": "^2.3.8",
|
||||
"pinia": "^2.1.6",
|
||||
"qrcode": "^1.5.3",
|
||||
"vue": "^3.3.4",
|
||||
"vue-i18n": "^9.8.0",
|
||||
"vue-router": "^4.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -485,6 +487,50 @@
|
||||
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@intlify/core-base": {
|
||||
"version": "9.14.5",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@intlify/core-base/-/core-base-9.14.5.tgz",
|
||||
"integrity": "sha512-5ah5FqZG4pOoHjkvs8mjtv+gPKYU0zCISaYNjBNNqYiaITxW8ZtVih3GS/oTOqN8d9/mDLyrjD46GBApNxmlsA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/message-compiler": "9.14.5",
|
||||
"@intlify/shared": "9.14.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/message-compiler": {
|
||||
"version": "9.14.5",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@intlify/message-compiler/-/message-compiler-9.14.5.tgz",
|
||||
"integrity": "sha512-IHzgEu61/YIpQV5Pc3aRWScDcnFKWvQA9kigcINcCBXN8mbW+vk9SK+lDxA6STzKQsVJxUPg9ACC52pKKo3SVQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/shared": "9.14.5",
|
||||
"source-map-js": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/shared": {
|
||||
"version": "9.14.5",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@intlify/shared/-/shared-9.14.5.tgz",
|
||||
"integrity": "sha512-9gB+E53BYuAEMhbCAxVgG38EZrk59sxBtv3jSizNL2hEWlgjBjAw1AwpLHtNaeda12pe6W20OGEa0TwuMSRbyQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
@@ -823,6 +869,7 @@
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@types/lodash-es/-/lodash-es-4.17.12.tgz",
|
||||
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
@@ -989,6 +1036,30 @@
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/async-validator": {
|
||||
"version": "4.2.5",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/async-validator/-/async-validator-4.2.5.tgz",
|
||||
@@ -1039,6 +1110,15 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/camelcase": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/camelcase/-/camelcase-5.3.1.tgz",
|
||||
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/chokidar/-/chokidar-4.0.3.tgz",
|
||||
@@ -1055,6 +1135,35 @@
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/cliui/-/cliui-6.0.0.tgz",
|
||||
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"wrap-ansi": "^6.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
@@ -1079,6 +1188,15 @@
|
||||
"integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/decamelize": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/decamelize/-/decamelize-1.2.0.tgz",
|
||||
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
@@ -1102,6 +1220,12 @@
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/dijkstrajs": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
||||
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
@@ -1142,6 +1266,12 @@
|
||||
"vue": "^3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/entities/-/entities-4.5.0.tgz",
|
||||
@@ -1263,6 +1393,19 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/find-up": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/find-up/-/find-up-4.1.0.tgz",
|
||||
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"locate-path": "^5.0.0",
|
||||
"path-exists": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||
@@ -1323,6 +1466,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
@@ -1429,6 +1581,15 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-glob": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/is-glob/-/is-glob-4.0.3.tgz",
|
||||
@@ -1454,17 +1615,31 @@
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/locate-path/-/locate-path-5.0.0.tgz",
|
||||
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-locate": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash-es": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/lodash-es/-/lodash-es-4.17.21.tgz",
|
||||
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash-unified": {
|
||||
"version": "1.0.3",
|
||||
@@ -1569,6 +1744,51 @@
|
||||
"integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/p-limit": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/p-limit/-/p-limit-2.3.0.tgz",
|
||||
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-try": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-locate": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/p-locate/-/p-locate-4.1.0.tgz",
|
||||
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-limit": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/p-try": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/p-try/-/p-try-2.2.0.tgz",
|
||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/path-exists/-/path-exists-4.0.0.tgz",
|
||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -1611,6 +1831,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/pngjs": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/pngjs/-/pngjs-5.0.0.tgz",
|
||||
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/postcss/-/postcss-8.5.6.tgz",
|
||||
@@ -1645,6 +1874,23 @@
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/qrcode": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/qrcode/-/qrcode-1.5.4.tgz",
|
||||
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dijkstrajs": "^1.0.1",
|
||||
"pngjs": "^5.0.0",
|
||||
"yargs": "^15.3.1"
|
||||
},
|
||||
"bin": {
|
||||
"qrcode": "bin/qrcode"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/readdirp/-/readdirp-4.1.2.tgz",
|
||||
@@ -1659,6 +1905,21 @@
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-main-filename": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "3.29.5",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/rollup/-/rollup-3.29.5.tgz",
|
||||
@@ -1682,6 +1943,7 @@
|
||||
"integrity": "sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.0",
|
||||
"immutable": "^5.0.2",
|
||||
@@ -1697,6 +1959,12 @@
|
||||
"@parcel/watcher": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
@@ -1706,6 +1974,32 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
@@ -1726,6 +2020,7 @@
|
||||
"integrity": "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.18.10",
|
||||
"postcss": "^8.4.27",
|
||||
@@ -1781,6 +2076,7 @@
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/vue/-/vue-3.5.22.tgz",
|
||||
"integrity": "sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.22",
|
||||
"@vue/compiler-sfc": "3.5.22",
|
||||
@@ -1823,6 +2119,26 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vue-i18n": {
|
||||
"version": "9.14.5",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/vue-i18n/-/vue-i18n-9.14.5.tgz",
|
||||
"integrity": "sha512-0jQ9Em3ymWngyiIkj0+c/k7WgaPO+TNzjKSNq9BvBQaKJECqn9cd9fL4tkDhB5G1QBskGl9YxxbDAhgbFtpe2g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/core-base": "9.14.5",
|
||||
"@intlify/shared": "9.14.5",
|
||||
"@vue/devtools-api": "^6.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-router": {
|
||||
"version": "4.6.3",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/vue-router/-/vue-router-4.6.3.tgz",
|
||||
@@ -1837,6 +2153,67 @@
|
||||
"peerDependencies": {
|
||||
"vue": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/which-module": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/which-module/-/which-module-2.0.1.tgz",
|
||||
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/y18n/-/y18n-4.0.3.tgz",
|
||||
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "15.4.1",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/yargs/-/yargs-15.4.1.tgz",
|
||||
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^6.0.0",
|
||||
"decamelize": "^1.2.0",
|
||||
"find-up": "^4.1.0",
|
||||
"get-caller-file": "^2.0.1",
|
||||
"require-directory": "^2.1.1",
|
||||
"require-main-filename": "^2.0.0",
|
||||
"set-blocking": "^2.0.0",
|
||||
"string-width": "^4.2.0",
|
||||
"which-module": "^2.0.0",
|
||||
"y18n": "^4.0.0",
|
||||
"yargs-parser": "^18.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs-parser": {
|
||||
"version": "18.1.3",
|
||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/yargs-parser/-/yargs-parser-18.1.3.tgz",
|
||||
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"camelcase": "^5.0.0",
|
||||
"decamelize": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"dependencies": {
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.2.4",
|
||||
"vue-i18n": "^9.8.0",
|
||||
"pinia": "^2.1.6",
|
||||
"axios": "^1.5.0",
|
||||
"element-plus": "^2.3.8",
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
# 背景图片说明
|
||||
|
||||
这个文件夹专门用于存放欢迎页面的背景图片。
|
||||
|
||||
## 文件结构
|
||||
```
|
||||
public/images/backgrounds/
|
||||
├── welcome-bg-1.jpg # 深蓝色渐变背景
|
||||
├── welcome-bg-2.jpg # 备用背景图片
|
||||
└── README.md # 本说明文件
|
||||
```
|
||||
|
||||
## 使用方式
|
||||
在Vue组件中使用:
|
||||
```css
|
||||
background: url('/images/backgrounds/welcome-bg-1.jpg') center/cover no-repeat;
|
||||
```
|
||||
|
||||
## 图片要求
|
||||
- 格式:JPG/PNG
|
||||
- 尺寸:1920x1080 或更高
|
||||
- 文件大小:建议小于2MB
|
||||
- 内容:深色渐变背景,适合文字叠加
|
||||
16
demo/frontend/public/images/backgrounds/avatar-default.svg
Normal file
16
demo/frontend/public/images/backgrounds/avatar-default.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<svg width="104" height="104" viewBox="0 0 104 104" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_601_168)">
|
||||
<circle cx="52" cy="52" r="51.5" fill="#1BAFFF" stroke="white"/>
|
||||
<path d="M16.9548 9.33656C10.1123 10.3489 3.30243 13.5493 -1.94346 18.2193C-7.90619 23.5097 -12.1746 31.8372 -13.2498 40.3607C-13.5431 42.6467 -13.7386 43.2672 -14.1622 43.2672C-15.4003 43.2672 -20.2226 45.2592 -22.5686 46.7288C-34.7873 54.3379 -38.6647 70.601 -31.2032 82.8801C-27.1303 89.5094 -20.5485 94.0161 -13.2173 95.1917L-10.7409 95.5836L-9.6331 98.0329C-5.69053 106.818 1.83619 113.12 10.9595 115.243C16.3031 116.484 22.0378 116.19 27.3488 114.427C32.8554 112.565 37.7755 109.136 41.1967 104.728L43.1843 102.148L45.1067 102.899C48.3976 104.27 51.6559 104.76 55.8591 104.564C60.2904 104.368 62.5061 103.813 66.4487 101.919C75.018 97.739 80.4594 89.4115 81.1437 79.2878L81.3718 76.218L83.4897 74.1933C87.3345 70.5031 89.7131 66.4536 91.0816 61.3918C92.0591 57.7342 92.0591 51.9866 91.0816 48.329C88.5727 38.8585 81.665 31.8372 72.2485 29.1593C70.1631 28.5715 68.925 28.4409 65.3083 28.4735C61.4634 28.4735 60.5511 28.6042 58.14 29.3553C56.6085 29.8451 55.1423 30.3023 54.8816 30.3677C54.5558 30.4983 54.1648 30.0084 53.6435 28.8981C47.4527 15.574 31.715 7.18121 16.9548 9.33656ZM20.6693 43.5937C23.113 44.2795 24.7748 45.6838 25.9152 48.0351C26.644 49.5175 27.023 51.1474 27.023 52.7993V62.5348V72.2702C27.023 73.9221 26.644 75.552 25.9152 77.0345C24.7422 79.4511 23.0804 80.8227 20.5064 81.5085C18.8772 81.9657 15.4885 81.6718 14.0875 80.9533C12.2954 80.0716 10.6011 78.1775 9.91682 76.3487C9.29774 74.7158 9.26515 73.7688 9.26515 62.7307C9.26515 49.472 9.3629 48.6556 11.4156 46.2716C13.6313 43.659 17.2155 42.614 20.6693 43.5937ZM42.0438 43.757C44.1943 44.3775 46.1493 46.1083 47.0942 48.231C47.8762 49.9618 47.8762 49.9618 47.8762 62.5348C47.8762 75.1077 47.8762 75.1077 47.0942 76.8385C45.3022 80.8227 40.7405 82.6841 36.1137 81.3125C33.9306 80.6594 32.3015 79.2225 31.1936 76.9692L30.2813 75.1077V62.5348V49.9618L31.1936 48.1004C33.1486 44.0836 37.417 42.3854 42.0438 43.757Z" fill="#151515"/>
|
||||
<path d="M20.6693 43.5937C23.113 44.2795 24.7748 45.6838 25.9152 48.0351C26.644 49.5175 27.023 51.1474 27.023 52.7993V62.5348V72.2702C27.023 73.9221 26.644 75.552 25.9152 77.0345C24.7422 79.4511 23.0804 80.8227 20.5064 81.5085C18.8772 81.9657 15.4885 81.6718 14.0875 80.9533C12.2954 80.0716 10.6011 78.1775 9.91682 76.3487C9.29774 74.7158 9.26515 73.7688 9.26515 62.7307C9.26515 49.472 9.3629 48.6556 11.4156 46.2716C13.6313 43.659 17.2155 42.614 20.6693 43.5937Z" fill="#151515"/>
|
||||
<path d="M42.0438 43.757C44.1943 44.3775 46.1493 46.1083 47.0942 48.231C47.8762 49.9618 47.8762 49.9618 47.8762 62.5348C47.8762 75.1077 47.8762 75.1077 47.0942 76.8385C45.3022 80.8227 40.7405 82.6841 36.1137 81.3125C33.9306 80.6594 32.3015 79.2225 31.1936 76.9692L30.2813 75.1077V62.5348V49.9618L31.1936 48.1004C33.1486 44.0836 37.417 42.3854 42.0438 43.757Z" fill="#151515"/>
|
||||
<rect x="36.3332" y="45.9877" width="7.92593" height="17.1728" rx="3.96296" fill="white"/>
|
||||
<rect x="49.543" y="45.9875" width="7.92593" height="17.1728" rx="3.96296" fill="white"/>
|
||||
</g>
|
||||
<rect x="0.5" y="0.5" width="103" height="103" rx="51.5" stroke="white"/>
|
||||
<defs>
|
||||
<clipPath id="clip0_601_168">
|
||||
<rect width="104" height="104" rx="52" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
233
demo/frontend/public/images/backgrounds/login-bg.svg
Normal file
233
demo/frontend/public/images/backgrounds/login-bg.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 25 KiB |
BIN
demo/frontend/public/images/backgrounds/logo.png
Normal file
BIN
demo/frontend/public/images/backgrounds/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
15
demo/frontend/public/images/backgrounds/logo.svg
Normal file
15
demo/frontend/public/images/backgrounds/logo.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<svg width="194" height="41" viewBox="0 0 194 41" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M54.6165 13.6062L60.593 32.3932H60.8224L66.8111 13.6062H72.6065L64.0824 38.3335H57.3452L48.8089 13.6062H54.6165ZM75.4862 38.3335V19.788H80.6296V38.3335H75.4862ZM78.07 17.3974C77.3053 17.3974 76.6493 17.1439 76.1019 16.6368C75.5626 16.1216 75.293 15.5058 75.293 14.7895C75.293 14.0811 75.5626 13.4734 76.1019 12.9663C76.6493 12.4512 77.3053 12.1936 78.07 12.1936C78.8346 12.1936 79.4866 12.4512 80.0259 12.9663C80.5733 13.4734 80.8469 14.0811 80.8469 14.7895C80.8469 15.5058 80.5733 16.1216 80.0259 16.6368C79.4866 17.1439 78.8346 17.3974 78.07 17.3974ZM91.5836 38.6353C90.175 38.6353 88.8992 38.2731 87.7562 37.5487C86.6213 36.8162 85.7198 35.7416 85.0517 34.325C84.3916 32.9003 84.0616 31.1536 84.0616 29.0849C84.0616 26.9599 84.4037 25.1931 85.0879 23.7845C85.7721 22.3678 86.6816 21.3093 87.8166 20.6091C88.9596 19.9007 90.2112 19.5466 91.5716 19.5466C92.6099 19.5466 93.4752 19.7236 94.1674 20.0778C94.8677 20.4239 95.4312 20.8586 95.8578 21.3818C96.2924 21.8969 96.6225 22.404 96.8478 22.9031H97.0048V13.6062H102.136V38.3335H97.0652V35.3633H96.8478C96.6064 35.8785 96.2643 36.3896 95.8216 36.8967C95.3869 37.3958 94.8194 37.8103 94.1191 38.1403C93.4269 38.4703 92.5817 38.6353 91.5836 38.6353ZM93.2136 34.5423C94.0427 34.5423 94.743 34.3169 95.3145 33.8662C95.894 33.4074 96.3367 32.7674 96.6426 31.9464C96.9565 31.1254 97.1135 30.1635 97.1135 29.0608C97.1135 27.958 96.9605 27.0002 96.6547 26.1872C96.3488 25.3742 95.9061 24.7464 95.3265 24.3037C94.747 23.861 94.0427 23.6396 93.2136 23.6396C92.3684 23.6396 91.6561 23.869 91.0765 24.3278C90.497 24.7866 90.0583 25.4225 89.7605 26.2355C89.4627 27.0485 89.3137 27.9902 89.3137 29.0608C89.3137 30.1394 89.4627 31.0932 89.7605 31.9223C90.0663 32.7433 90.505 33.3872 91.0765 33.8541C91.6561 34.3129 92.3684 34.5423 93.2136 34.5423ZM106.462 38.3335V13.6062H122.834V17.9166H111.69V23.8086H121.747V28.119H111.69V38.3335H106.462ZM131.397 13.6062V38.3335H126.254V13.6062H131.397ZM143.897 38.6957C142.021 38.6957 140.399 38.2973 139.031 37.5004C137.671 36.6955 136.62 35.5766 135.88 34.1439C135.139 32.7031 134.769 31.0328 134.769 29.1332C134.769 27.2175 135.139 25.5432 135.88 24.1105C136.62 22.6697 137.671 21.5508 139.031 20.7539C140.399 19.949 142.021 19.5466 143.897 19.5466C145.772 19.5466 147.39 19.949 148.75 20.7539C150.119 21.5508 151.173 22.6697 151.914 24.1105C152.654 25.5432 153.025 27.2175 153.025 29.1332C153.025 31.0328 152.654 32.7031 151.914 34.1439C151.173 35.5766 150.119 36.6955 148.75 37.5004C147.39 38.2973 145.772 38.6957 143.897 38.6957ZM143.921 34.7113C144.774 34.7113 145.486 34.4699 146.058 33.9869C146.629 33.4959 147.06 32.8278 147.35 31.9826C147.648 31.1375 147.797 30.1756 147.797 29.097C147.797 28.0184 147.648 27.0565 147.35 26.2113C147.06 25.3662 146.629 24.6981 146.058 24.2071C145.486 23.7161 144.774 23.4706 143.921 23.4706C143.06 23.4706 142.335 23.7161 141.748 24.2071C141.168 24.6981 140.729 25.3662 140.431 26.2113C140.142 27.0565 139.997 28.0184 139.997 29.097C139.997 30.1756 140.142 31.1375 140.431 31.9826C140.729 32.8278 141.168 33.4959 141.748 33.9869C142.335 34.4699 143.06 34.7113 143.921 34.7113ZM159.562 38.3335L154.516 19.788H159.719L162.593 32.2483H162.762L165.756 19.788H170.864L173.906 32.1758H174.063L176.888 19.788H182.08L177.045 38.3335H171.6L168.413 26.6701H168.183L164.996 38.3335H159.562Z" fill="white"/>
|
||||
<g clip-path="url(#clip0_1233_5144)">
|
||||
<path d="M5.7406 1.64568C2.43935 1.64568 0.00938034 1.57298 0.000366208 1.64568C0.000366202 1.71906 2.11292 5.37389 4.70642 9.58611C7.28533 13.813 12.3557 22.0905 15.9545 27.9611L20.8685 35.9875C21.8796 37.6388 23.6773 38.6457 25.6136 38.6457L26.4301 38.6457C26.4301 38.6457 31.0568 38.7204 31.9965 37.2228C32.2255 36.8288 32.346 36.3807 32.3461 35.925L32.3461 21.8996L31.1801 21.8996L31.1801 26.5969C31.1801 31.1005 31.1655 31.3074 30.889 31.5861C30.7288 31.7476 30.4663 31.8801 30.306 31.8801C29.9855 31.8801 29.7811 31.5866 27.1439 27.257C26.2551 25.8039 24.0844 22.2371 22.2924 19.3312C20.5148 16.4253 17.3534 11.2596 15.2699 7.85467L11.4672 1.64568L5.7406 1.64568ZM31.6615 2.99627C31.443 4.1703 30.525 6.06354 29.6654 7.10564C28.4561 8.60263 26.4304 9.83531 24.5072 10.2316C23.4727 10.4518 23.196 10.5988 23.808 10.5988C24.0267 10.5989 24.5801 10.7012 25.0316 10.8332C28.2662 11.7578 31.0495 14.7674 31.6468 17.9816L31.8363 18.9797L32.0111 18.1135C32.5648 15.3249 34.4443 12.8151 36.9066 11.5676C38.0139 10.9952 39.2815 10.5988 39.9808 10.5988C40.6217 10.5988 40.403 10.4957 39.2084 10.2463C37.46 9.87934 36.1049 9.11623 34.6625 7.66326C33.2638 6.26901 32.5496 5.02127 32.0834 3.17205L31.8217 2.11541L31.6615 2.99627Z" fill="url(#paint0_linear_1233_5144)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_1233_5144" x1="23.4862" y1="5.3947" x2="32.9093" y2="11.1248" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.0001" stop-color="#33DDE5"/>
|
||||
<stop offset="1" stop-color="#0F9CFF"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_1233_5144">
|
||||
<rect width="40.3334" height="40.3334" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.0 KiB |
@@ -1,45 +0,0 @@
|
||||
<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>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,28 +1,40 @@
|
||||
<template>
|
||||
<div id="app" :data-route="route.name">
|
||||
<!-- 全屏背景层 -->
|
||||
<div class="fullscreen-background" :class="route.name"></div>
|
||||
|
||||
<!-- 导航栏 - 根据路由条件显示 -->
|
||||
<NavBar v-if="shouldShowNavBar" />
|
||||
|
||||
<!-- 主要内容区域 -->
|
||||
<main :class="{ 'with-navbar': shouldShowNavBar }">
|
||||
<router-view />
|
||||
</main>
|
||||
|
||||
<!-- 页脚 - 根据路由条件显示 -->
|
||||
<Footer v-if="shouldShowFooter" />
|
||||
</div>
|
||||
<el-config-provider :locale="elementLocale">
|
||||
<div id="app" :data-route="route.name">
|
||||
<!-- 全屏背景层 -->
|
||||
<div class="fullscreen-background" :class="route.name"></div>
|
||||
|
||||
<!-- 导航栏 - 根据路由条件显示 -->
|
||||
<NavBar v-if="shouldShowNavBar" />
|
||||
|
||||
<!-- 主要内容区域 -->
|
||||
<main :class="{ 'with-navbar': shouldShowNavBar }">
|
||||
<router-view />
|
||||
</main>
|
||||
|
||||
<!-- 页脚 - 根据路由条件显示 -->
|
||||
<Footer v-if="shouldShowFooter" />
|
||||
</div>
|
||||
</el-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||
import en from 'element-plus/dist/locale/en.mjs'
|
||||
import NavBar from '@/components/NavBar.vue'
|
||||
import Footer from '@/components/Footer.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const { locale } = useI18n()
|
||||
|
||||
// 动态计算 Element Plus 的语言配置
|
||||
const elementLocale = computed(() => {
|
||||
console.log('[App.vue] 当前语言切换为:', locale.value)
|
||||
return locale.value === 'zh' ? zhCn : en
|
||||
})
|
||||
|
||||
// 计算是否显示导航栏和页脚
|
||||
const shouldShowNavBar = computed(() => {
|
||||
|
||||
@@ -60,6 +60,16 @@ export const checkEmailExists = (email) => {
|
||||
})
|
||||
}
|
||||
|
||||
// 发送邮箱验证码
|
||||
export const sendEmailCode = (email) => {
|
||||
return api.post('/verification/email/send', { email })
|
||||
}
|
||||
|
||||
// 开发环境:设置验证码(用于开发测试)
|
||||
export const setDevEmailCode = (email, code) => {
|
||||
return api.post('/verification/email/dev-set', { email, code })
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -19,3 +19,5 @@ export const processExpiredRecords = () => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -22,14 +22,33 @@ const api = axios.create({
|
||||
// 请求拦截器
|
||||
api.interceptors.request.use(
|
||||
(config) => {
|
||||
// 使用JWT认证,添加Authorization头
|
||||
const token = sessionStorage.getItem('token')
|
||||
if (token && token !== 'null' && token.trim() !== '') {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
console.log('请求拦截器:添加Authorization头,token长度:', token.length)
|
||||
// 登录相关的接口不需要添加token
|
||||
const loginUrls = [
|
||||
'/auth/login',
|
||||
'/auth/login/email',
|
||||
'/auth/register',
|
||||
'/verification/email/send',
|
||||
'/verification/email/verify',
|
||||
'/verification/email/dev-set',
|
||||
'/public/'
|
||||
]
|
||||
|
||||
// 检查当前请求是否是登录相关接口
|
||||
const isLoginRequest = loginUrls.some(url => config.url.includes(url))
|
||||
|
||||
if (!isLoginRequest) {
|
||||
// 非登录请求才添加Authorization头
|
||||
const token = sessionStorage.getItem('token')
|
||||
if (token && token !== 'null' && token.trim() !== '') {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
console.log('请求拦截器:添加Authorization头,token长度:', token.length)
|
||||
} else {
|
||||
console.warn('请求拦截器:未找到有效的token')
|
||||
}
|
||||
} else {
|
||||
console.warn('请求拦截器:未找到有效的token')
|
||||
console.log('请求拦截器:登录相关请求,不添加token:', config.url)
|
||||
}
|
||||
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
@@ -44,62 +63,99 @@ api.interceptors.response.use(
|
||||
// 检查是否是HTML响应(可能是302重定向的结果)
|
||||
if (response.data && typeof response.data === 'string' && response.data.trim().startsWith('<!DOCTYPE')) {
|
||||
console.error('收到HTML响应,可能是认证失败:', response.config.url)
|
||||
// 清除无效的token并跳转到登录页
|
||||
sessionStorage.removeItem('token')
|
||||
sessionStorage.removeItem('user')
|
||||
// 避免重复跳转
|
||||
if (router.currentRoute.value.path !== '/login') {
|
||||
ElMessage.error('认证失败,请重新登录')
|
||||
router.push('/login')
|
||||
|
||||
// 只有非登录请求才清除token并跳转
|
||||
const loginUrls = ['/auth/login', '/auth/login/email', '/auth/register', '/verification/', '/public/']
|
||||
const isLoginRequest = loginUrls.some(url => response.config.url.includes(url))
|
||||
|
||||
if (!isLoginRequest) {
|
||||
// 清除无效的token并跳转到登录页
|
||||
sessionStorage.removeItem('token')
|
||||
sessionStorage.removeItem('user')
|
||||
// 避免重复跳转
|
||||
if (router.currentRoute.value.path !== '/login') {
|
||||
ElMessage.error('认证失败,请重新登录')
|
||||
router.push('/login')
|
||||
}
|
||||
}
|
||||
// 返回错误,让调用方知道这是认证失败
|
||||
return Promise.reject(new Error('认证失败:收到HTML响应'))
|
||||
}
|
||||
|
||||
|
||||
// 检查302重定向
|
||||
if (response.status === 302) {
|
||||
console.error('收到302重定向,可能是认证失败:', response.config.url)
|
||||
sessionStorage.removeItem('token')
|
||||
sessionStorage.removeItem('user')
|
||||
if (router.currentRoute.value.path !== '/login') {
|
||||
ElMessage.error('认证失败,请重新登录')
|
||||
router.push('/login')
|
||||
}
|
||||
return Promise.reject(new Error('认证失败:302重定向'))
|
||||
}
|
||||
|
||||
// 直接返回response,让调用方处理data
|
||||
return response
|
||||
},
|
||||
(error) => {
|
||||
if (error.response) {
|
||||
const { status, data } = error.response
|
||||
|
||||
// 检查响应数据是否是HTML(302重定向的结果)
|
||||
if (data && typeof data === 'string' && data.trim().startsWith('<!DOCTYPE')) {
|
||||
console.error('收到HTML响应(可能是302重定向):', error.config.url)
|
||||
|
||||
// 只有非登录请求才清除token并跳转
|
||||
const loginUrls = ['/auth/login', '/auth/login/email', '/auth/register', '/verification/', '/public/']
|
||||
const isLoginRequest = loginUrls.some(url => response.config.url.includes(url))
|
||||
|
||||
if (!isLoginRequest) {
|
||||
sessionStorage.removeItem('token')
|
||||
sessionStorage.removeItem('user')
|
||||
if (router.currentRoute.value.path !== '/login') {
|
||||
ElMessage.error('认证失败,请重新登录')
|
||||
router.push('/login')
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case 401:
|
||||
case 302:
|
||||
// 302也可能是认证失败导致的
|
||||
return Promise.reject(new Error('认证失败:302重定向'))
|
||||
}
|
||||
|
||||
// 直接返回response,让调用方处理data
|
||||
return response
|
||||
},
|
||||
(error) => {
|
||||
if (error.response) {
|
||||
const { status, data } = error.response
|
||||
|
||||
// 检查响应数据是否是HTML(302重定向的结果)
|
||||
if (data && typeof data === 'string' && data.trim().startsWith('<!DOCTYPE')) {
|
||||
console.error('收到HTML响应(可能是302重定向):', error.config.url)
|
||||
|
||||
// 只有非登录请求才清除token并跳转
|
||||
const loginUrls = ['/auth/login', '/auth/login/email', '/auth/register', '/verification/', '/public/']
|
||||
const isLoginRequest = loginUrls.some(url => error.config.url.includes(url))
|
||||
|
||||
if (!isLoginRequest) {
|
||||
sessionStorage.removeItem('token')
|
||||
sessionStorage.removeItem('user')
|
||||
if (router.currentRoute.value.path !== '/login') {
|
||||
ElMessage.error('认证失败,请重新登录')
|
||||
router.push('/login')
|
||||
}
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case 401:
|
||||
case 302:
|
||||
// 只有非登录请求才清除token并跳转
|
||||
const loginUrls = ['/auth/login', '/auth/login/email', '/auth/register', '/verification/', '/public/']
|
||||
const isLoginRequest = loginUrls.some(url => error.config.url.includes(url))
|
||||
|
||||
if (!isLoginRequest) {
|
||||
// 302也可能是认证失败导致的
|
||||
sessionStorage.removeItem('token')
|
||||
sessionStorage.removeItem('user')
|
||||
if (router.currentRoute.value.path !== '/login') {
|
||||
ElMessage.error('认证失败,请重新登录')
|
||||
router.push('/login')
|
||||
}
|
||||
}
|
||||
break
|
||||
case 403:
|
||||
ElMessage.error('权限不足')
|
||||
// 403可能是权限不足或CORS问题
|
||||
// 如果是登录请求的403,不要显示"权限不足",而是显示具体错误信息
|
||||
const loginUrls403 = ['/auth/login', '/auth/login/email', '/auth/register', '/verification/', '/public/']
|
||||
const isLoginRequest403 = loginUrls403.some(url => error.config.url.includes(url))
|
||||
|
||||
if (!isLoginRequest403) {
|
||||
ElMessage.error('权限不足')
|
||||
} else {
|
||||
// 登录请求的403,显示具体错误或网络问题
|
||||
ElMessage.error(data?.message || '请求失败,请检查网络连接')
|
||||
}
|
||||
break
|
||||
case 404:
|
||||
ElMessage.error('请求的资源不存在')
|
||||
@@ -115,7 +171,7 @@ api.interceptors.response.use(
|
||||
} else {
|
||||
ElMessage.error('请求配置错误')
|
||||
}
|
||||
|
||||
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -5,20 +5,25 @@ export const taskStatusApi = {
|
||||
getTaskStatus(taskId) {
|
||||
return api.get(`/task-status/${taskId}`)
|
||||
},
|
||||
|
||||
|
||||
// 获取用户的所有任务状态
|
||||
getUserTaskStatuses(username) {
|
||||
return api.get(`/task-status/user/${username}`)
|
||||
},
|
||||
|
||||
|
||||
// 取消任务
|
||||
cancelTask(taskId) {
|
||||
return api.post(`/task-status/${taskId}/cancel`)
|
||||
},
|
||||
|
||||
|
||||
// 手动触发轮询(管理员功能)
|
||||
triggerPolling() {
|
||||
return api.post('/task-status/poll')
|
||||
},
|
||||
|
||||
// 获取所有任务记录(管理员功能)
|
||||
getAllTaskRecords(params) {
|
||||
return api.get('/task-status/admin/all', { params })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,9 +21,6 @@ export const textToVideoApi = {
|
||||
if (!params.prompt || params.prompt.trim() === '') {
|
||||
throw new Error('文本描述不能为空')
|
||||
}
|
||||
if (params.prompt.trim().length > 1000) {
|
||||
throw new Error('文本描述不能超过1000个字符')
|
||||
}
|
||||
if (!params.aspectRatio) {
|
||||
throw new Error('视频比例不能为空')
|
||||
}
|
||||
|
||||
@@ -10,6 +10,11 @@ export const getMyWorks = (params = {}) => {
|
||||
})
|
||||
}
|
||||
|
||||
// 获取正在进行中的作品
|
||||
export const getProcessingWorks = () => {
|
||||
return api.get('/works/processing')
|
||||
}
|
||||
|
||||
// 获取作品详情
|
||||
export const getWorkDetail = (workId) => {
|
||||
return api.get(`/works/${workId}`)
|
||||
|
||||
@@ -101,6 +101,10 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
71
demo/frontend/src/components/LanguageSwitcher.vue
Normal file
71
demo/frontend/src/components/LanguageSwitcher.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<button class="language-switcher" @click="toggleLanguage" :title="currentLanguage === 'zh' ? '切换到英文' : 'Switch to Chinese'">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M4.16602 12.4998V14.1665C4.16602 15.0452 4.84592 15.765 5.7083 15.8286L5.83268 15.8332H8.33268V17.4998H5.83268C3.99173 17.4998 2.49935 16.0074 2.49935 14.1665V12.4998H4.16602ZM14.9993 8.33317L18.666 17.4998H16.8702L15.8694 14.9998H12.461L11.4618 17.4998H9.66685L13.3327 8.33317H14.9993ZM14.166 10.7375L13.1268 13.3332H15.2035L14.166 10.7375ZM6.66602 1.6665V3.33317H9.99935V9.1665H6.66602V11.6665H4.99935V9.1665H1.66602V3.33317H4.99935V1.6665H6.66602ZM14.166 2.49984C16.0069 2.49984 17.4993 3.99222 17.4993 5.83317V7.49984H15.8327V5.83317C15.8327 4.9127 15.0865 4.1665 14.166 4.1665H11.666V2.49984H14.166ZM4.99935 4.99984H3.33268V7.49984H4.99935V4.99984ZM8.33268 4.99984H6.66602V7.49984H8.33268V4.99984Z" fill="currentColor"/>
|
||||
</svg>
|
||||
<span class="lang-text">{{ currentLanguage === 'zh' ? '中' : 'EN' }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { locale } = useI18n()
|
||||
|
||||
const currentLanguage = computed(() => locale.value)
|
||||
|
||||
const toggleLanguage = () => {
|
||||
console.log('[LanguageSwitcher] 当前语言:', locale.value)
|
||||
|
||||
// 切换语言
|
||||
const newLang = locale.value === 'zh' ? 'en' : 'zh'
|
||||
console.log('[LanguageSwitcher] 切换到:', newLang)
|
||||
|
||||
// 直接更新 locale(响应式切换)
|
||||
locale.value = newLang
|
||||
|
||||
// 保存到 localStorage 以便下次刷新时使用
|
||||
localStorage.setItem('language', newLang)
|
||||
console.log('[LanguageSwitcher] localStorage 已保存:', localStorage.getItem('language'))
|
||||
console.log('[LanguageSwitcher] 语言切换完成(无刷新)')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.language-switcher {
|
||||
height: 36px;
|
||||
padding: 0 12px;
|
||||
border-radius: 18px;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.language-switcher:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.language-switcher:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.language-switcher svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.lang-text {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
</style>
|
||||
@@ -4,7 +4,7 @@
|
||||
<!-- Logo -->
|
||||
<div class="navbar-brand">
|
||||
<router-link to="/" class="brand-link">
|
||||
<span class="brand-text">AIGC Demo</span>
|
||||
<img src="/images/backgrounds/logo.svg" alt="Logo" class="brand-logo" />
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
@@ -19,31 +19,31 @@
|
||||
@select="handleMenuSelect"
|
||||
>
|
||||
<el-menu-item index="/welcome">
|
||||
<span>欢迎页</span>
|
||||
<span>{{ $t('common.welcome') }}</span>
|
||||
</el-menu-item>
|
||||
|
||||
|
||||
<el-menu-item index="/home">
|
||||
<span>首页</span>
|
||||
<span>{{ $t('common.home') }}</span>
|
||||
</el-menu-item>
|
||||
|
||||
|
||||
<el-menu-item v-if="userStore.isAuthenticated" index="/profile">
|
||||
<span>个人主页</span>
|
||||
<span>{{ $t('common.profile') }}</span>
|
||||
</el-menu-item>
|
||||
|
||||
|
||||
<el-menu-item v-if="userStore.isAuthenticated" index="/orders">
|
||||
<span>订单管理</span>
|
||||
<span>{{ $t('common.orders') }}</span>
|
||||
</el-menu-item>
|
||||
|
||||
|
||||
<el-menu-item v-if="userStore.isAuthenticated" index="/payments">
|
||||
<span>支付记录</span>
|
||||
<span>{{ $t('common.payments') }}</span>
|
||||
</el-menu-item>
|
||||
|
||||
|
||||
<el-menu-item v-if="userStore.isAdmin" index="/admin/orders">
|
||||
<span>后台管理</span>
|
||||
<span>{{ $t('common.adminPanel') }}</span>
|
||||
</el-menu-item>
|
||||
|
||||
|
||||
<el-menu-item v-if="userStore.isAdmin" index="/admin/dashboard">
|
||||
<span>数据仪表盘</span>
|
||||
<span>{{ $t('dashboard.title') }}</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
|
||||
@@ -57,38 +57,40 @@
|
||||
<!-- 用户菜单 -->
|
||||
<div class="navbar-user">
|
||||
<template v-if="userStore.isAuthenticated">
|
||||
<LanguageSwitcher />
|
||||
<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 v-if="userStore.availablePoints > 0" size="small" type="success" class="points-tag">
|
||||
{{ userStore.availablePoints }}{{ $t('common.points') }}
|
||||
</el-tag>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="profile">
|
||||
个人资料
|
||||
{{ $t('common.userProfile') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="userStore.isAdmin" command="admin">
|
||||
后台管理
|
||||
{{ $t('common.adminPanel') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="settings">
|
||||
设置
|
||||
{{ $t('common.settings') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item divided command="logout">
|
||||
退出登录
|
||||
{{ $t('common.logout') }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
|
||||
|
||||
<template v-else>
|
||||
<LanguageSwitcher />
|
||||
<el-button type="primary" plain @click="$router.push('/login')">
|
||||
登录
|
||||
{{ $t('common.login') }}
|
||||
</el-button>
|
||||
<el-button type="success" plain @click="$router.push('/register')">
|
||||
注册
|
||||
{{ $t('common.register') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</div>
|
||||
@@ -100,8 +102,11 @@
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import LanguageSwitcher from './LanguageSwitcher.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
|
||||
@@ -130,29 +135,29 @@ const handleMenuSelect = (index) => {
|
||||
const handleUserCommand = async (command) => {
|
||||
switch (command) {
|
||||
case 'profile':
|
||||
ElMessage.info('个人资料功能开发中')
|
||||
ElMessage.info(t('common.profileDevMsg'))
|
||||
break
|
||||
case 'admin':
|
||||
// 检查管理员权限
|
||||
if (userStore.isAdmin) {
|
||||
router.push('/admin/dashboard')
|
||||
} else {
|
||||
ElMessage.warning('权限不足,只有管理员才能访问后台管理')
|
||||
ElMessage.warning(t('common.noPermissionMsg'))
|
||||
}
|
||||
break
|
||||
case 'settings':
|
||||
ElMessage.info('设置功能开发中')
|
||||
ElMessage.info(t('common.settingsDevMsg'))
|
||||
break
|
||||
case 'logout':
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要退出登录吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
await ElMessageBox.confirm(t('common.logoutConfirm'), t('common.tip'), {
|
||||
confirmButtonText: t('common.confirm'),
|
||||
cancelButtonText: t('common.cancel'),
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
|
||||
await userStore.logoutUser()
|
||||
ElMessage.success('退出登录成功')
|
||||
ElMessage.success(t('common.logoutSuccess'))
|
||||
router.push('/')
|
||||
} catch (error) {
|
||||
// 用户取消
|
||||
@@ -191,6 +196,11 @@ const handleUserCommand = async (command) => {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
height: 40px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.brand-icon {
|
||||
margin-right: 8px;
|
||||
font-size: 24px;
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="qr-tip">支付前请阅读《XX 付费服务协议》</div>
|
||||
<div class="qr-tip">支付前请阅读《Vionow支付服务条款》</div>
|
||||
</div>
|
||||
|
||||
<!-- 支付提示 -->
|
||||
@@ -76,7 +76,7 @@
|
||||
|
||||
<!-- 底部链接 -->
|
||||
<div class="footer-link">
|
||||
<a href="#" @click.prevent="showAgreement">《XX 付费服务协议》</a>
|
||||
<a href="#" @click.prevent="showAgreement">《Vionow支付服务条款》</a>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
@@ -84,6 +84,7 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onUnmounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { CreditCard } from '@element-plus/icons-vue'
|
||||
import { createPayment, createAlipayPayment, getPaymentById, testPaymentComplete } from '@/api/payments'
|
||||
@@ -109,6 +110,7 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'pay-success', 'pay-error'])
|
||||
|
||||
const router = useRouter()
|
||||
const visible = ref(false)
|
||||
const selectedMethod = ref('alipay')
|
||||
const loading = ref(false)
|
||||
@@ -388,7 +390,7 @@ const handleTestPaymentComplete = async () => {
|
||||
|
||||
// 显示协议
|
||||
const showAgreement = () => {
|
||||
ElMessage.info('服务协议页面')
|
||||
router.push('/terms-of-service')
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
227
demo/frontend/src/locales/en.js
Normal file
227
demo/frontend/src/locales/en.js
Normal file
@@ -0,0 +1,227 @@
|
||||
export default {
|
||||
common: {
|
||||
confirm: 'Confirm',
|
||||
cancel: 'Cancel',
|
||||
save: 'Save',
|
||||
delete: 'Delete',
|
||||
edit: 'Edit',
|
||||
search: 'Search',
|
||||
reset: 'Reset',
|
||||
view: 'View',
|
||||
loading: 'Loading...',
|
||||
searchPlaceholder: 'Search for content',
|
||||
welcome: 'Welcome',
|
||||
home: 'Home',
|
||||
profile: 'Profile',
|
||||
orders: 'Orders',
|
||||
payments: 'Payments',
|
||||
adminPanel: 'Admin Panel',
|
||||
login: 'Login',
|
||||
register: 'Register',
|
||||
logout: 'Logout',
|
||||
settings: 'Settings',
|
||||
userProfile: 'User Profile',
|
||||
points: 'Points',
|
||||
profileDevMsg: 'Profile feature under development',
|
||||
settingsDevMsg: 'Settings feature under development',
|
||||
noPermissionMsg: 'Insufficient permission, only administrators can access admin panel',
|
||||
logoutConfirm: 'Are you sure you want to logout?',
|
||||
logoutSuccess: 'Logout successful',
|
||||
tip: 'Tip'
|
||||
},
|
||||
|
||||
welcome: {
|
||||
textToVideo: 'Text to Video',
|
||||
imageToVideo: 'Image to Video',
|
||||
storyboardVideo: 'Storyboard Video',
|
||||
pricing: 'Pricing Plans',
|
||||
startExperience: 'Get Started',
|
||||
title1: 'Create',
|
||||
title2: 'Unlimited,',
|
||||
title3: 'Ideas',
|
||||
title4: 'Realized.',
|
||||
subtitle: 'Secure and convenient login with email verification',
|
||||
tryNow: 'Try Now',
|
||||
coreFeatures: 'Core Features',
|
||||
textToVideoDesc: 'Enter text description, AI automatically generates high-quality video content',
|
||||
imageToVideoDesc: 'Upload images, AI intelligently analyzes and generates dynamic videos',
|
||||
storyboardVideoDesc: 'Professional storyboarding for cinematic video effects',
|
||||
pricingDesc: 'Flexible pricing plans to meet different creative needs',
|
||||
startCreating: 'Start Creating'
|
||||
},
|
||||
|
||||
nav: {
|
||||
dashboard: 'Dashboard',
|
||||
members: 'Member Management',
|
||||
orders: 'Order Management',
|
||||
apiManagement: 'API Management',
|
||||
tasks: 'Task Records',
|
||||
systemSettings: 'System Settings',
|
||||
onlineUsers: 'Online Users',
|
||||
systemUptime: 'System Uptime'
|
||||
},
|
||||
|
||||
dashboard: {
|
||||
title: 'Dashboard',
|
||||
totalUsers: 'Total Users',
|
||||
paidUsers: 'Paid Users',
|
||||
todayRevenue: "Today's Revenue",
|
||||
dailyActive: 'Daily Active User Trend',
|
||||
conversionRate: 'User Conversion Rate',
|
||||
comparedToLastMonth: 'vs last month',
|
||||
year2025: '2025',
|
||||
year2024: '2024',
|
||||
year2023: '2023',
|
||||
userAvatar: 'User Avatar',
|
||||
month1: 'Jan',
|
||||
month2: 'Feb',
|
||||
month3: 'Mar',
|
||||
month4: 'Apr',
|
||||
month5: 'May',
|
||||
month6: 'Jun',
|
||||
month7: 'Jul',
|
||||
month8: 'Aug',
|
||||
month9: 'Sep',
|
||||
month10: 'Oct',
|
||||
month11: 'Nov',
|
||||
month12: 'Dec'
|
||||
},
|
||||
|
||||
orders: {
|
||||
title: 'Order Management',
|
||||
orderNumber: 'Order Number',
|
||||
username: 'Username',
|
||||
amount: 'Amount',
|
||||
paymentMethod: 'Payment Method',
|
||||
status: 'Status',
|
||||
createTime: 'Create Time',
|
||||
operation: 'Operation',
|
||||
allStatus: 'All Status',
|
||||
allTypes: 'All Types',
|
||||
pending: 'Pending',
|
||||
confirmed: 'Confirmed',
|
||||
paid: 'Paid',
|
||||
processing: 'Processing',
|
||||
shipped: 'Shipped',
|
||||
delivered: 'Delivered',
|
||||
completed: 'Completed',
|
||||
cancelled: 'Cancelled',
|
||||
refunded: 'Refunded',
|
||||
unpaid: 'Unpaid',
|
||||
alipay: 'Alipay',
|
||||
wechat: 'WeChat Pay',
|
||||
paypal: 'PayPal',
|
||||
selected: '{count} selected'
|
||||
},
|
||||
|
||||
tasks: {
|
||||
title: 'Task Records',
|
||||
taskId: 'Task ID',
|
||||
username: 'Username',
|
||||
type: 'Type',
|
||||
resources: 'Resources Used',
|
||||
status: 'Status',
|
||||
createTime: 'Create Time',
|
||||
operation: 'Operation',
|
||||
allStatus: 'All Status',
|
||||
completed: 'Completed',
|
||||
processing: 'Processing',
|
||||
failed: 'Failed',
|
||||
cancelled: 'Cancelled',
|
||||
textToVideo: 'Text to Video',
|
||||
imageToVideo: 'Image to Video',
|
||||
storyboardVideo: 'Storyboard Video'
|
||||
},
|
||||
|
||||
members: {
|
||||
title: 'Member List',
|
||||
userId: 'User ID',
|
||||
username: 'Username',
|
||||
level: 'Membership Level',
|
||||
points: 'Remaining Points',
|
||||
expiryDate: 'Expiry Date',
|
||||
operation: 'Edit',
|
||||
allLevels: 'All Levels',
|
||||
professional: 'Professional',
|
||||
standard: 'Standard',
|
||||
editMember: 'Edit Member',
|
||||
usernamePlaceholder: 'Enter username',
|
||||
levelPlaceholder: 'Select level',
|
||||
pointsPlaceholder: 'Enter points',
|
||||
expiryPlaceholder: 'Select expiry date'
|
||||
},
|
||||
|
||||
apiManagement: {
|
||||
title: 'API Management',
|
||||
apiKey: 'API Key',
|
||||
apiKeyPlaceholder: 'Enter API key',
|
||||
tokenExpiration: 'Token Expiration',
|
||||
tokenPlaceholder: 'Enter hours (1-720)',
|
||||
hours: 'hours',
|
||||
days: 'days',
|
||||
rangeHint: 'Range: 1-720 hours (1 hour - 30 days)',
|
||||
saveSuccess: 'Saved successfully',
|
||||
saveFailed: 'Save failed'
|
||||
},
|
||||
|
||||
systemSettings: {
|
||||
title: 'System Settings',
|
||||
membership: 'Membership Pricing',
|
||||
cleanup: 'Task Cleanup',
|
||||
membershipLevels: 'Membership Levels',
|
||||
editLevel: 'Edit Level',
|
||||
price: 'Price',
|
||||
description: 'Description',
|
||||
cleanupStats: 'Cleanup Statistics',
|
||||
manualCleanup: 'Manual Cleanup',
|
||||
autoCleanup: 'Auto Cleanup',
|
||||
// Membership pricing
|
||||
perMonth: '/month',
|
||||
includesPoints: 'Includes {points} points/month',
|
||||
// Cleanup stats
|
||||
cleanupStatsInfo: 'Cleanup Statistics',
|
||||
refresh: 'Refresh',
|
||||
currentTotalTasks: 'Current Total Tasks',
|
||||
completedTasks: 'Completed Tasks',
|
||||
failedTasks: 'Failed Tasks',
|
||||
archivedTasks: 'Archived Tasks',
|
||||
cleanupLogsCount: 'Cleanup Logs',
|
||||
retentionDays: 'Retention Days',
|
||||
days: 'days',
|
||||
// Cleanup actions
|
||||
cleanupActions: 'Cleanup Actions',
|
||||
performFullCleanup: 'Perform Full Cleanup',
|
||||
cleanupUserTasks: 'Cleanup User Tasks',
|
||||
fullCleanupDesc: 'Full Cleanup',
|
||||
fullCleanupDescDetail: 'Export successful tasks to archive, delete failed tasks',
|
||||
userCleanupDesc: 'User Cleanup',
|
||||
userCleanupDescDetail: 'Cleanup all tasks for specified user',
|
||||
// Cleanup config
|
||||
cleanupConfig: 'Cleanup Configuration',
|
||||
taskRetentionDays: 'Task Retention Days',
|
||||
taskRetentionTip: 'Days to retain completed tasks',
|
||||
archiveRetentionDays: 'Archive Retention Days',
|
||||
archiveRetentionTip: 'Days to retain archived data',
|
||||
// Edit membership dialog
|
||||
membershipLevel: 'Membership Level',
|
||||
selectLevelPlaceholder: 'Select membership level',
|
||||
freeMembership: 'Free Membership',
|
||||
standardMembership: 'Standard Membership',
|
||||
professionalMembership: 'Professional Membership',
|
||||
membershipPrice: 'Membership Price',
|
||||
resourcePointsAmount: 'Resource Points',
|
||||
validityPeriod: 'Validity Period',
|
||||
monthly: 'Monthly',
|
||||
quarterly: 'Quarterly',
|
||||
yearly: 'Yearly',
|
||||
// User cleanup dialog
|
||||
enterUsername: 'Enter username to cleanup',
|
||||
warning: 'Warning',
|
||||
cleanupWarning: 'This operation will cleanup all tasks for this user, including:',
|
||||
successTasksArchived: 'Successful tasks will be exported to archive',
|
||||
failedTasksLogged: 'Failed tasks will be logged to cleanup logs',
|
||||
originalTasksDeleted: 'Original task records will be deleted',
|
||||
irreversibleWarning: 'This operation is irreversible, please proceed with caution!',
|
||||
confirmCleanup: 'Confirm Cleanup'
|
||||
}
|
||||
}
|
||||
23
demo/frontend/src/locales/index.js
Normal file
23
demo/frontend/src/locales/index.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import zh from './zh'
|
||||
import en from './en'
|
||||
|
||||
// 从localStorage获取保存的语言设置,默认中文
|
||||
const savedLanguage = localStorage.getItem('language') || 'zh'
|
||||
|
||||
console.log('[i18n] 从 localStorage 读取的语言:', savedLanguage)
|
||||
console.log('[i18n] 可用的语言:', Object.keys({ zh, en }))
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false, // 使用Composition API模式
|
||||
locale: savedLanguage, // 默认语言
|
||||
fallbackLocale: 'zh', // 回退语言
|
||||
messages: {
|
||||
zh,
|
||||
en
|
||||
}
|
||||
})
|
||||
|
||||
console.log('[i18n] i18n 初始化完成,当前语言:', i18n.global.locale.value)
|
||||
|
||||
export default i18n
|
||||
227
demo/frontend/src/locales/zh.js
Normal file
227
demo/frontend/src/locales/zh.js
Normal file
@@ -0,0 +1,227 @@
|
||||
export default {
|
||||
common: {
|
||||
confirm: '确认',
|
||||
cancel: '取消',
|
||||
save: '保存',
|
||||
delete: '删除',
|
||||
edit: '编辑',
|
||||
search: '搜索',
|
||||
reset: '重置',
|
||||
view: '查看',
|
||||
loading: '加载中...',
|
||||
searchPlaceholder: '搜索你想要的内容',
|
||||
welcome: '欢迎页',
|
||||
home: '首页',
|
||||
profile: '个人主页',
|
||||
orders: '订单管理',
|
||||
payments: '支付记录',
|
||||
adminPanel: '后台管理',
|
||||
login: '登录',
|
||||
register: '注册',
|
||||
logout: '退出登录',
|
||||
settings: '设置',
|
||||
userProfile: '个人资料',
|
||||
points: '积分',
|
||||
profileDevMsg: '个人资料功能开发中',
|
||||
settingsDevMsg: '设置功能开发中',
|
||||
noPermissionMsg: '权限不足,只有管理员才能访问后台管理',
|
||||
logoutConfirm: '确定要退出登录吗?',
|
||||
logoutSuccess: '退出登录成功',
|
||||
tip: '提示'
|
||||
},
|
||||
|
||||
welcome: {
|
||||
textToVideo: '文生视频',
|
||||
imageToVideo: '图生视频',
|
||||
storyboardVideo: '分镜视频',
|
||||
pricing: '订阅套餐',
|
||||
startExperience: '开始体验',
|
||||
title1: '智创',
|
||||
title2: '无限,',
|
||||
title3: '灵感',
|
||||
title4: '变现。',
|
||||
subtitle: '使用邮箱验证码登录,安全便捷',
|
||||
tryNow: '立即体验',
|
||||
coreFeatures: '核心功能',
|
||||
textToVideoDesc: '输入文字描述,AI自动生成高质量视频内容',
|
||||
imageToVideoDesc: '上传图片,AI智能分析并生成动态视频',
|
||||
storyboardVideoDesc: '专业分镜制作,打造电影级视频效果',
|
||||
pricingDesc: '灵活的价格方案,满足不同创作需求',
|
||||
startCreating: '开始创作'
|
||||
},
|
||||
|
||||
nav: {
|
||||
dashboard: '数据仪表台',
|
||||
members: '会员管理',
|
||||
orders: '订单管理',
|
||||
apiManagement: 'API管理',
|
||||
tasks: '生成任务记录',
|
||||
systemSettings: '系统设置',
|
||||
onlineUsers: '当前在线用户',
|
||||
systemUptime: '系统运行时间'
|
||||
},
|
||||
|
||||
dashboard: {
|
||||
title: '数据仪表台',
|
||||
totalUsers: '用户总数',
|
||||
paidUsers: '付费用户数',
|
||||
todayRevenue: '今日收入',
|
||||
dailyActive: '日活用户趋势',
|
||||
conversionRate: '用户转化率',
|
||||
comparedToLastMonth: '较上月同期',
|
||||
year2025: '2025年',
|
||||
year2024: '2024年',
|
||||
year2023: '2023年',
|
||||
userAvatar: '用户头像',
|
||||
month1: '1月',
|
||||
month2: '2月',
|
||||
month3: '3月',
|
||||
month4: '4月',
|
||||
month5: '5月',
|
||||
month6: '6月',
|
||||
month7: '7月',
|
||||
month8: '8月',
|
||||
month9: '9月',
|
||||
month10: '10月',
|
||||
month11: '11月',
|
||||
month12: '12月'
|
||||
},
|
||||
|
||||
orders: {
|
||||
title: '订单管理',
|
||||
orderNumber: '订单编号',
|
||||
username: '用户名',
|
||||
amount: '金额',
|
||||
paymentMethod: '支付方式',
|
||||
status: '状态',
|
||||
createTime: '创建时间',
|
||||
operation: '操作',
|
||||
allStatus: '全部状态',
|
||||
allTypes: '全部类型',
|
||||
pending: '待支付',
|
||||
confirmed: '已确认',
|
||||
paid: '已支付',
|
||||
processing: '处理中',
|
||||
shipped: '已发货',
|
||||
delivered: '已送达',
|
||||
completed: '已完成',
|
||||
cancelled: '已取消',
|
||||
refunded: '已退款',
|
||||
unpaid: '未支付',
|
||||
alipay: '支付宝',
|
||||
wechat: '微信支付',
|
||||
paypal: 'PayPal',
|
||||
selected: '已选择{count}项'
|
||||
},
|
||||
|
||||
tasks: {
|
||||
title: '生成任务记录',
|
||||
taskId: '任务ID',
|
||||
username: '用户名',
|
||||
type: '类型',
|
||||
resources: '消耗资源',
|
||||
status: '状态',
|
||||
createTime: '创建时间',
|
||||
operation: '操作',
|
||||
allStatus: '全部状态',
|
||||
completed: '已完成',
|
||||
processing: '处理中',
|
||||
failed: '失败',
|
||||
cancelled: '已取消',
|
||||
textToVideo: '文生视频',
|
||||
imageToVideo: '图生视频',
|
||||
storyboardVideo: '分镜视频'
|
||||
},
|
||||
|
||||
members: {
|
||||
title: '会员列表',
|
||||
userId: '用户ID',
|
||||
username: '用户名',
|
||||
level: '会员等级',
|
||||
points: '剩余资源点',
|
||||
expiryDate: '到期时间',
|
||||
operation: '编辑',
|
||||
allLevels: '全部等级',
|
||||
professional: '专业会员',
|
||||
standard: '标准会员',
|
||||
editMember: '编辑会员信息',
|
||||
usernamePlaceholder: '请输入用户名',
|
||||
levelPlaceholder: '请选择会员等级',
|
||||
pointsPlaceholder: '请输入资源点',
|
||||
expiryPlaceholder: '请选择到期时间'
|
||||
},
|
||||
|
||||
apiManagement: {
|
||||
title: 'API管理',
|
||||
apiKey: 'API密钥',
|
||||
apiKeyPlaceholder: '请输入API密钥',
|
||||
tokenExpiration: 'Token过期时间',
|
||||
tokenPlaceholder: '请输入小时数(1-720)',
|
||||
hours: '小时',
|
||||
days: '天',
|
||||
rangeHint: '范围:1-720小时(1小时-30天)',
|
||||
saveSuccess: '保存成功',
|
||||
saveFailed: '保存失败'
|
||||
},
|
||||
|
||||
systemSettings: {
|
||||
title: '系统设置',
|
||||
membership: '会员收费标准',
|
||||
cleanup: '任务清理管理',
|
||||
membershipLevels: '会员等级',
|
||||
editLevel: '编辑等级',
|
||||
price: '价格',
|
||||
description: '描述',
|
||||
cleanupStats: '清理统计',
|
||||
manualCleanup: '手动清理',
|
||||
autoCleanup: '自动清理',
|
||||
// Membership pricing
|
||||
perMonth: '/月',
|
||||
includesPoints: '包含{points}资源点/月',
|
||||
// Cleanup stats
|
||||
cleanupStatsInfo: '清理统计信息',
|
||||
refresh: '刷新',
|
||||
currentTotalTasks: '当前任务总数',
|
||||
completedTasks: '已完成任务',
|
||||
failedTasks: '失败任务',
|
||||
archivedTasks: '已归档任务',
|
||||
cleanupLogsCount: '清理日志数',
|
||||
retentionDays: '保留天数',
|
||||
days: '天',
|
||||
// Cleanup actions
|
||||
cleanupActions: '清理操作',
|
||||
performFullCleanup: '执行完整清理',
|
||||
cleanupUserTasks: '清理指定用户任务',
|
||||
fullCleanupDesc: '完整清理',
|
||||
fullCleanupDescDetail: '将成功任务导出到归档表,删除失败任务',
|
||||
userCleanupDesc: '用户清理',
|
||||
userCleanupDescDetail: '清理指定用户的所有任务',
|
||||
// Cleanup config
|
||||
cleanupConfig: '清理配置',
|
||||
taskRetentionDays: '任务保留天数',
|
||||
taskRetentionTip: '任务完成后保留的天数',
|
||||
archiveRetentionDays: '归档保留天数',
|
||||
archiveRetentionTip: '归档数据保留的天数',
|
||||
// Edit membership dialog
|
||||
membershipLevel: '会员等级',
|
||||
selectLevelPlaceholder: '请选择会员等级',
|
||||
freeMembership: '免费版会员',
|
||||
standardMembership: '标准版会员',
|
||||
professionalMembership: '专业版会员',
|
||||
membershipPrice: '会员价格',
|
||||
resourcePointsAmount: '资源点数量',
|
||||
validityPeriod: '会员有效期',
|
||||
monthly: '月付',
|
||||
quarterly: '季付',
|
||||
yearly: '年付',
|
||||
// User cleanup dialog
|
||||
enterUsername: '请输入要清理的用户名',
|
||||
warning: '警告',
|
||||
cleanupWarning: '此操作将清理该用户的所有任务,包括:',
|
||||
successTasksArchived: '成功任务将导出到归档表',
|
||||
failedTasksLogged: '失败任务将记录到清理日志',
|
||||
originalTasksDeleted: '原始任务记录将被删除',
|
||||
irreversibleWarning: '此操作不可撤销,请谨慎操作!',
|
||||
confirmCleanup: '确认清理'
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,10 @@ import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import i18n from './locales'
|
||||
import { useUserStore } from './stores/user'
|
||||
|
||||
const app = createApp(App)
|
||||
@@ -13,9 +13,12 @@ const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
app.use(ElementPlus, {
|
||||
locale: zhCn,
|
||||
})
|
||||
app.use(i18n)
|
||||
app.use(ElementPlus)
|
||||
|
||||
console.log('[main.js] i18n 当前语言:', i18n.global.locale.value)
|
||||
|
||||
// 立即挂载应用
|
||||
app.mount('#app')
|
||||
|
||||
console.log('[main.js] 应用已挂载,当前语言:', i18n.global.locale.value)
|
||||
|
||||
@@ -31,6 +31,7 @@ const SystemSettings = () => import('@/views/SystemSettings.vue')
|
||||
const GenerateTaskRecord = () => import('@/views/GenerateTaskRecord.vue')
|
||||
const HelloWorld = () => import('@/views/HelloWorld.vue')
|
||||
const TaskStatusPage = () => import('@/views/TaskStatusPage.vue')
|
||||
const TermsOfService = () => import('@/views/TermsOfService.vue')
|
||||
|
||||
const routes = [
|
||||
{
|
||||
@@ -61,7 +62,7 @@ const routes = [
|
||||
path: '/text-to-video/create',
|
||||
name: 'TextToVideoCreate',
|
||||
component: TextToVideoCreate,
|
||||
meta: { title: '文生视频创作', requiresAuth: true }
|
||||
meta: { title: '文生视频创作' }
|
||||
},
|
||||
{
|
||||
path: '/image-to-video',
|
||||
@@ -73,7 +74,7 @@ const routes = [
|
||||
path: '/image-to-video/create',
|
||||
name: 'ImageToVideoCreate',
|
||||
component: ImageToVideoCreate,
|
||||
meta: { title: '图生视频创作', requiresAuth: true }
|
||||
meta: { title: '图生视频创作' }
|
||||
},
|
||||
{
|
||||
path: '/image-to-video/detail/:taskId',
|
||||
@@ -91,7 +92,7 @@ const routes = [
|
||||
path: '/storyboard-video/create',
|
||||
name: 'StoryboardVideoCreate',
|
||||
component: StoryboardVideoCreate,
|
||||
meta: { title: '分镜视频创作', requiresAuth: true }
|
||||
meta: { title: '分镜视频创作' }
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
@@ -206,6 +207,12 @@ const routes = [
|
||||
component: HelloWorld,
|
||||
meta: { title: 'Hello World' }
|
||||
},
|
||||
{
|
||||
path: '/terms-of-service',
|
||||
name: 'TermsOfService',
|
||||
component: TermsOfService,
|
||||
meta: { title: 'Vionow 服务条款' }
|
||||
},
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
|
||||
@@ -25,6 +25,14 @@ export const useUserStore = defineStore('user', () => {
|
||||
const isAdmin = computed(() => user.value?.role === 'ROLE_ADMIN')
|
||||
const username = computed(() => user.value?.username || '')
|
||||
|
||||
// 可用积分(总积分 - 冻结积分)
|
||||
const availablePoints = computed(() => {
|
||||
if (!user.value) return 0
|
||||
const total = user.value.points || 0
|
||||
const frozen = user.value.frozenPoints || 0
|
||||
return Math.max(0, total - frozen)
|
||||
})
|
||||
|
||||
// 登录
|
||||
const loginUser = async (credentials) => {
|
||||
try {
|
||||
@@ -147,6 +155,7 @@ export const useUserStore = defineStore('user', () => {
|
||||
isAuthenticated,
|
||||
isAdmin,
|
||||
username,
|
||||
availablePoints,
|
||||
// 方法
|
||||
loginUser,
|
||||
registerUser,
|
||||
|
||||
@@ -16,14 +16,15 @@ export function getApiBaseURL() {
|
||||
if (hostname.includes('ngrok') ||
|
||||
hostname === 'localhost' ||
|
||||
hostname === '127.0.0.1' ||
|
||||
hostname.startsWith('172.22.') ||
|
||||
window.location.port === '') { // 通过 Nginx 代理访问时没有端口号
|
||||
// 通过 Nginx 访问,使用相对路径(自动适配当前域名)
|
||||
return '/api'
|
||||
}
|
||||
}
|
||||
|
||||
// 默认开发环境,直接访问后端
|
||||
return 'http://localhost:8080/api'
|
||||
// 默认开发环境,使用相对路径(通过 Vite 代理)
|
||||
return '/api'
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,43 +3,58 @@
|
||||
<!-- 左侧导航栏 -->
|
||||
<aside class="sidebar">
|
||||
<div class="logo">
|
||||
<div class="logo-icon"></div>
|
||||
<span>LOGO</span>
|
||||
<div class="logo-icon">
|
||||
<svg viewBox="0 0 194 41" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M54.6165 13.6062L60.593 32.3932H60.8224L66.8111 13.6062H72.6065L64.0824 38.3335H57.3452L48.8089 13.6062H54.6165ZM75.4862 38.3335V19.788H80.6296V38.3335H75.4862ZM78.07 17.3974C77.3053 17.3974 76.6493 17.1439 76.1019 16.6368C75.5626 16.1216 75.293 15.5058 75.293 14.7895C75.293 14.0811 75.5626 13.4734 76.1019 12.9663C76.6493 12.4512 77.3053 12.1936 78.07 12.1936C78.8346 12.1936 79.4866 12.4512 80.0259 12.9663C80.5733 13.4734 80.8469 14.0811 80.8469 14.7895C80.8469 15.5058 80.5733 16.1216 80.0259 16.6368C79.4866 17.1439 78.8346 17.3974 78.07 17.3974ZM91.5836 38.6353C90.175 38.6353 88.8992 38.2731 87.7562 37.5487C86.6213 36.8162 85.7198 35.7416 85.0517 34.325C84.3916 32.9003 84.0616 31.1536 84.0616 29.0849C84.0616 26.9599 84.4037 25.1931 85.0879 23.7845C85.7721 22.3678 86.6816 21.3093 87.8166 20.6091C88.9596 19.9007 90.2112 19.5466 91.5716 19.5466C92.6099 19.5466 93.4752 19.7236 94.1674 20.0778C94.8677 20.4239 95.4312 20.8586 95.8578 21.3818C96.2924 21.8969 96.6225 22.404 96.8478 22.9031H97.0048V13.6062H102.136V38.3335H97.0652V35.3633H96.8478C96.6064 35.8785 96.2643 36.3896 95.8216 36.8967C95.3869 37.3958 94.8194 37.8103 94.1191 38.1403C93.4269 38.4703 92.5817 38.6353 91.5836 38.6353ZM93.2136 34.5423C94.0427 34.5423 94.743 34.3169 95.3145 33.8662C95.894 33.4074 96.3367 32.7674 96.6426 31.9464C96.9565 31.1254 97.1135 30.1635 97.1135 29.0608C97.1135 27.958 96.9605 27.0002 96.6547 26.1872C96.3488 25.3742 95.9061 24.7464 95.3265 24.3037C94.747 23.861 94.0427 23.6396 93.2136 23.6396C92.3684 23.6396 91.6561 23.869 91.0765 24.3278C90.497 24.7866 90.0583 25.4225 89.7605 26.2355C89.4627 27.0485 89.3137 27.9902 89.3137 29.0608C89.3137 30.1394 89.4627 31.0932 89.7605 31.9223C90.0663 32.7433 90.505 33.3872 91.0765 33.8541C91.6561 34.3129 92.3684 34.5423 93.2136 34.5423ZM106.462 38.3335V13.6062H122.834V17.9166H111.69V23.8086H121.747V28.119H111.69V38.3335H106.462ZM131.397 13.6062V38.3335H126.254V13.6062H131.397ZM143.897 38.6957C142.021 38.6957 140.399 38.2973 139.031 37.5004C137.671 36.6955 136.62 35.5766 135.88 34.1439C135.139 32.7031 134.769 31.0328 134.769 29.1332C134.769 27.2175 135.139 25.5432 135.88 24.1105C136.62 22.6697 137.671 21.5508 139.031 20.7539C140.399 19.949 142.021 19.5466 143.897 19.5466C145.772 19.5466 147.39 19.949 148.75 20.7539C150.119 21.5508 151.173 22.6697 151.914 24.1105C152.654 25.5432 153.025 27.2175 153.025 29.1332C153.025 31.0328 152.654 32.7031 151.914 34.1439C151.173 35.5766 150.119 36.6955 148.75 37.5004C147.39 38.2973 145.772 38.6957 143.897 38.6957ZM143.921 34.7113C144.774 34.7113 145.486 34.4699 146.058 33.9869C146.629 33.4959 147.06 32.8278 147.35 31.9826C147.648 31.1375 147.797 30.1756 147.797 29.097C147.797 28.0184 147.648 27.0565 147.35 26.2113C147.06 25.3662 146.629 24.6981 146.058 24.2071C145.486 23.7161 144.774 23.4706 143.921 23.4706C143.06 23.4706 142.335 23.7161 141.748 24.2071C141.168 24.6981 140.729 25.3662 140.431 26.2113C140.142 27.0565 139.997 28.0184 139.997 29.097C139.997 30.1756 140.142 31.1375 140.431 31.9826C140.729 32.8278 141.168 33.4959 141.748 33.9869C142.335 34.4699 143.06 34.7113 143.921 34.7113ZM159.562 38.3335L154.516 19.788H159.719L162.593 32.2483H162.762L165.756 19.788H170.864L173.906 32.1758H174.063L176.888 19.788H182.08L177.045 38.3335H171.6L168.413 26.6701H168.183L164.996 38.3335H159.562Z" fill="white"/>
|
||||
<g clip-path="url(#clip0)">
|
||||
<path d="M5.7406 1.64568C2.43935 1.64568 0.00938034 1.57298 0.000366208 1.64568C0.000366202 1.71906 2.11292 5.37389 4.70642 9.58611C7.28533 13.813 12.3557 22.0905 15.9545 27.9611L20.8685 35.9875C21.8796 37.6388 23.6773 38.6457 25.6136 38.6457L26.4301 38.6457C26.4301 38.6457 31.0568 38.7204 31.9965 37.2228C32.2255 36.8288 32.346 36.3807 32.3461 35.925L32.3461 21.8996L31.1801 21.8996L31.1801 26.5969C31.1801 31.1005 31.1655 31.3074 30.889 31.5861C30.7288 31.7476 30.4663 31.8801 30.306 31.8801C29.9855 31.8801 29.7811 31.5866 27.1439 27.257C26.2551 25.8039 24.0844 22.2371 22.2924 19.3312C20.5148 16.4253 17.3534 11.2596 15.2699 7.85467L11.4672 1.64568L5.7406 1.64568ZM31.6615 2.99627C31.443 4.1703 30.525 6.06354 29.6654 7.10564C28.4561 8.60263 26.4304 9.83531 24.5072 10.2316C23.4727 10.4518 23.196 10.5988 23.808 10.5988C24.0267 10.5989 24.5801 10.7012 25.0316 10.8332C28.2662 11.7578 31.0495 14.7674 31.6468 17.9816L31.8363 18.9797L32.0111 18.1135C32.5648 15.3249 34.4443 12.8151 36.9066 11.5676C38.0139 10.9952 39.2815 10.5988 39.9808 10.5988C40.6217 10.5988 40.403 10.4957 39.2084 10.2463C37.46 9.87934 36.1049 9.11623 34.6625 7.66326C33.2638 6.26901 32.5496 5.02127 32.0834 3.17205L31.8217 2.11541L31.6615 2.99627Z" fill="url(#paint0)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0" x1="23.4862" y1="5.3947" x2="32.9093" y2="11.1248" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.0001" stop-color="#33DDE5"/>
|
||||
<stop offset="1" stop-color="#0F9CFF"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0">
|
||||
<rect width="40.3334" height="40.3334" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="nav-menu">
|
||||
<div class="nav-item active">
|
||||
<el-icon><Grid /></el-icon>
|
||||
<span>数据仪表台</span>
|
||||
<span>{{ $t('nav.dashboard') }}</span>
|
||||
</div>
|
||||
<div class="nav-item" @click="goToMembers">
|
||||
<el-icon><User /></el-icon>
|
||||
<span>会员管理</span>
|
||||
<span>{{ $t('nav.members') }}</span>
|
||||
</div>
|
||||
<div class="nav-item" @click="goToOrders">
|
||||
<el-icon><ShoppingCart /></el-icon>
|
||||
<span>订单管理</span>
|
||||
<span>{{ $t('nav.orders') }}</span>
|
||||
</div>
|
||||
<div class="nav-item" @click="goToAPI">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>API管理</span>
|
||||
<span>{{ $t('nav.apiManagement') }}</span>
|
||||
</div>
|
||||
<div class="nav-item" @click="goToTasks">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>生成任务记录</span>
|
||||
<span>{{ $t('nav.tasks') }}</span>
|
||||
</div>
|
||||
<div class="nav-item" @click="goToSettings">
|
||||
<el-icon><Setting /></el-icon>
|
||||
<span>系统设置</span>
|
||||
<span>{{ $t('nav.systemSettings') }}</span>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<div class="online-users">
|
||||
当前在线用户: <span class="highlight">87/500</span>
|
||||
{{ $t('nav.onlineUsers') }}: <span class="highlight">{{ onlineUsers }}</span>
|
||||
</div>
|
||||
<div class="system-uptime">
|
||||
系统运行时间: <span class="highlight">48小时32分</span>
|
||||
{{ $t('nav.systemUptime') }}: <span class="highlight">{{ systemUptime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -50,15 +65,12 @@
|
||||
<header class="top-header">
|
||||
<div class="search-bar">
|
||||
<el-icon class="search-icon"><Search /></el-icon>
|
||||
<input type="text" placeholder="搜索你的想要的内容" class="search-input">
|
||||
<input type="text" :placeholder="$t('common.searchPlaceholder')" class="search-input">
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<div class="notification-icon-wrapper">
|
||||
<el-icon class="notification-icon"><Bell /></el-icon>
|
||||
<span class="notification-badge"></span>
|
||||
</div>
|
||||
<LanguageSwitcher />
|
||||
<div class="user-avatar">
|
||||
<img src="/images/backgrounds/welcome.jpg" alt="用户头像" />
|
||||
<img src="/images/backgrounds/avatar-default.svg" alt="用户头像" />
|
||||
<el-icon class="arrow-down"><ArrowDown /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
@@ -71,36 +83,36 @@
|
||||
<el-icon><User /></el-icon>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-title">用户总数</div>
|
||||
<div class="stat-title">{{ $t('dashboard.totalUsers') }}</div>
|
||||
<div class="stat-number">{{ formatNumber(stats.totalUsers) }}</div>
|
||||
<div class="stat-change" :class="stats.totalUsersChange >= 0 ? 'positive' : 'negative'">
|
||||
{{ stats.totalUsersChange >= 0 ? '+' : '' }}{{ stats.totalUsersChange }}% 较上月同期
|
||||
{{ stats.totalUsersChange >= 0 ? '+' : '' }}{{ stats.totalUsersChange }}% {{ $t('dashboard.comparedToLastMonth') }}
|
||||
</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-title">{{ $t('dashboard.paidUsers') }}</div>
|
||||
<div class="stat-number">{{ formatNumber(stats.paidUsers) }}</div>
|
||||
<div class="stat-change" :class="stats.paidUsersChange >= 0 ? 'positive' : 'negative'">
|
||||
{{ stats.paidUsersChange >= 0 ? '+' : '' }}{{ stats.paidUsersChange }}% 较上月同期
|
||||
{{ stats.paidUsersChange >= 0 ? '+' : '' }}{{ stats.paidUsersChange }}% {{ $t('dashboard.comparedToLastMonth') }}
|
||||
</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-title">{{ $t('dashboard.todayRevenue') }}</div>
|
||||
<div class="stat-number">{{ formatCurrency(stats.todayRevenue) }}</div>
|
||||
<div class="stat-change" :class="stats.todayRevenueChange >= 0 ? 'positive' : 'negative'">
|
||||
{{ stats.todayRevenueChange >= 0 ? '+' : '' }}{{ stats.todayRevenueChange }}% 较上月同期
|
||||
{{ stats.todayRevenueChange >= 0 ? '+' : '' }}{{ stats.todayRevenueChange }}% {{ $t('dashboard.comparedToLastMonth') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -111,11 +123,11 @@
|
||||
<!-- 日活用户趋势 -->
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<h3>日活用户趋势</h3>
|
||||
<h3>{{ $t('dashboard.dailyActive') }}</h3>
|
||||
<el-select v-model="selectedYear" @change="loadDailyActiveChart" class="year-select">
|
||||
<el-option label="2025年" value="2025"></el-option>
|
||||
<el-option label="2024年" value="2024"></el-option>
|
||||
<el-option label="2023年" value="2023"></el-option>
|
||||
<el-option :label="$t('dashboard.year2025')" value="2025"></el-option>
|
||||
<el-option :label="$t('dashboard.year2024')" value="2024"></el-option>
|
||||
<el-option :label="$t('dashboard.year2023')" value="2023"></el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="chart-content">
|
||||
@@ -126,11 +138,11 @@
|
||||
<!-- 用户转化率 -->
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<h3>用户转化率</h3>
|
||||
<h3>{{ $t('dashboard.conversionRate') }}</h3>
|
||||
<el-select v-model="selectedYear2" @change="loadConversionChart" class="year-select">
|
||||
<el-option label="2025年" value="2025"></el-option>
|
||||
<el-option label="2024年" value="2024"></el-option>
|
||||
<el-option label="2023年" value="2023"></el-option>
|
||||
<el-option :label="$t('dashboard.year2025')" value="2025"></el-option>
|
||||
<el-option :label="$t('dashboard.year2024')" value="2024"></el-option>
|
||||
<el-option :label="$t('dashboard.year2023')" value="2023"></el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="chart-content">
|
||||
@@ -149,17 +161,17 @@ import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
Grid,
|
||||
User,
|
||||
ShoppingCart,
|
||||
Document,
|
||||
User as Briefcase,
|
||||
Setting,
|
||||
User as Search,
|
||||
Bell,
|
||||
User as Avatar,
|
||||
ArrowDown,
|
||||
Money
|
||||
ShoppingCart,
|
||||
Document,
|
||||
User as Briefcase,
|
||||
Setting,
|
||||
User as Search,
|
||||
User as Avatar,
|
||||
ArrowDown,
|
||||
Money
|
||||
} from '@element-plus/icons-vue'
|
||||
import { getDashboardOverview, getConversionRate, getDailyActiveUsersTrend } from '@/api/dashboard'
|
||||
import LanguageSwitcher from '@/components/LanguageSwitcher.vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -179,6 +191,10 @@ const stats = ref({
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
// 系统状态数据
|
||||
const onlineUsers = ref('0/500')
|
||||
const systemUptime = ref('加载中...')
|
||||
|
||||
// 图表相关
|
||||
const dailyActiveChart = ref(null)
|
||||
const conversionChart = ref(null)
|
||||
@@ -456,12 +472,32 @@ const loadConversionChart = async () => {
|
||||
// 页面加载时获取数据
|
||||
onMounted(async () => {
|
||||
console.log('后台管理页面加载完成')
|
||||
fetchSystemStats()
|
||||
await loadDashboardData()
|
||||
await nextTick()
|
||||
await loadDailyActiveChart()
|
||||
await loadConversionChart()
|
||||
})
|
||||
|
||||
// 获取系统统计数据
|
||||
const fetchSystemStats = async () => {
|
||||
try {
|
||||
// 临时使用计算值,后续可以从API获取
|
||||
// 计算在线用户数(这里简化处理)
|
||||
const randomOnline = Math.floor(Math.random() * 50) + 10
|
||||
onlineUsers.value = `${randomOnline}/500`
|
||||
|
||||
// 计算系统运行时间(基于当前时间简单模拟)
|
||||
const hours = new Date().getHours()
|
||||
const minutes = new Date().getMinutes()
|
||||
systemUptime.value = `${hours}小时${minutes}分`
|
||||
} catch (error) {
|
||||
console.error('获取系统统计失败:', error)
|
||||
onlineUsers.value = '0/500'
|
||||
systemUptime.value = '未知'
|
||||
}
|
||||
}
|
||||
|
||||
// 组件卸载时清理图表
|
||||
onUnmounted(() => {
|
||||
if (dailyActiveChartInstance) {
|
||||
@@ -497,22 +533,23 @@ onUnmounted(() => {
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
padding: 0 50px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: #3b82f6;
|
||||
border-radius: 4px;
|
||||
margin-right: 12px;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.logo span {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
.logo-icon svg, .logo-icon img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
@@ -640,33 +677,6 @@ onUnmounted(() => {
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.notification-icon-wrapper {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.notification-icon-wrapper:hover {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.notification-icon {
|
||||
font-size: 20px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.notification-badge {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #ef4444;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -3,41 +3,56 @@
|
||||
<!-- 左侧导航栏 -->
|
||||
<aside class="sidebar">
|
||||
<div class="logo">
|
||||
<div class="logo-icon"></div>
|
||||
<span>LOGO</span>
|
||||
</div>
|
||||
<div class="logo-icon">
|
||||
<svg viewBox="0 0 194 41" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M54.6165 13.6062L60.593 32.3932H60.8224L66.8111 13.6062H72.6065L64.0824 38.3335H57.3452L48.8089 13.6062H54.6165ZM75.4862 38.3335V19.788H80.6296V38.3335H75.4862ZM78.07 17.3974C77.3053 17.3974 76.6493 17.1439 76.1019 16.6368C75.5626 16.1216 75.293 15.5058 75.293 14.7895C75.293 14.0811 75.5626 13.4734 76.1019 12.9663C76.6493 12.4512 77.3053 12.1936 78.07 12.1936C78.8346 12.1936 79.4866 12.4512 80.0259 12.9663C80.5733 13.4734 80.8469 14.0811 80.8469 14.7895C80.8469 15.5058 80.5733 16.1216 80.0259 16.6368C79.4866 17.1439 78.8346 17.3974 78.07 17.3974ZM91.5836 38.6353C90.175 38.6353 88.8992 38.2731 87.7562 37.5487C86.6213 36.8162 85.7198 35.7416 85.0517 34.325C84.3916 32.9003 84.0616 31.1536 84.0616 29.0849C84.0616 26.9599 84.4037 25.1931 85.0879 23.7845C85.7721 22.3678 86.6816 21.3093 87.8166 20.6091C88.9596 19.9007 90.2112 19.5466 91.5716 19.5466C92.6099 19.5466 93.4752 19.7236 94.1674 20.0778C94.8677 20.4239 95.4312 20.8586 95.8578 21.3818C96.2924 21.8969 96.6225 22.404 96.8478 22.9031H97.0048V13.6062H102.136V38.3335H97.0652V35.3633H96.8478C96.6064 35.8785 96.2643 36.3896 95.8216 36.8967C95.3869 37.3958 94.8194 37.8103 94.1191 38.1403C93.4269 38.4703 92.5817 38.6353 91.5836 38.6353ZM93.2136 34.5423C94.0427 34.5423 94.743 34.3169 95.3145 33.8662C95.894 33.4074 96.3367 32.7674 96.6426 31.9464C96.9565 31.1254 97.1135 30.1635 97.1135 29.0608C97.1135 27.958 96.9605 27.0002 96.6547 26.1872C96.3488 25.3742 95.9061 24.7464 95.3265 24.3037C94.747 23.861 94.0427 23.6396 93.2136 23.6396C92.3684 23.6396 91.6561 23.869 91.0765 24.3278C90.497 24.7866 90.0583 25.4225 89.7605 26.2355C89.4627 27.0485 89.3137 27.9902 89.3137 29.0608C89.3137 30.1394 89.4627 31.0932 89.7605 31.9223C90.0663 32.7433 90.505 33.3872 91.0765 33.8541C91.6561 34.3129 92.3684 34.5423 93.2136 34.5423ZM106.462 38.3335V13.6062H122.834V17.9166H111.69V23.8086H121.747V28.119H111.69V38.3335H106.462ZM131.397 13.6062V38.3335H126.254V13.6062H131.397ZM143.897 38.6957C142.021 38.6957 140.399 38.2973 139.031 37.5004C137.671 36.6955 136.62 35.5766 135.88 34.1439C135.139 32.7031 134.769 31.0328 134.769 29.1332C134.769 27.2175 135.139 25.5432 135.88 24.1105C136.62 22.6697 137.671 21.5508 139.031 20.7539C140.399 19.949 142.021 19.5466 143.897 19.5466C145.772 19.5466 147.39 19.949 148.75 20.7539C150.119 21.5508 151.173 22.6697 151.914 24.1105C152.654 25.5432 153.025 27.2175 153.025 29.1332C153.025 31.0328 152.654 32.7031 151.914 34.1439C151.173 35.5766 150.119 36.6955 148.75 37.5004C147.39 38.2973 145.772 38.6957 143.897 38.6957ZM143.921 34.7113C144.774 34.7113 145.486 34.4699 146.058 33.9869C146.629 33.4959 147.06 32.8278 147.35 31.9826C147.648 31.1375 147.797 30.1756 147.797 29.097C147.797 28.0184 147.648 27.0565 147.35 26.2113C147.06 25.3662 146.629 24.6981 146.058 24.2071C145.486 23.7161 144.774 23.4706 143.921 23.4706C143.06 23.4706 142.335 23.7161 141.748 24.2071C141.168 24.6981 140.729 25.3662 140.431 26.2113C140.142 27.0565 139.997 28.0184 139.997 29.097C139.997 30.1756 140.142 31.1375 140.431 31.9826C140.729 32.8278 141.168 33.4959 141.748 33.9869C142.335 34.4699 143.06 34.7113 143.921 34.7113ZM159.562 38.3335L154.516 19.788H159.719L162.593 32.2483H162.762L165.756 19.788H170.864L173.906 32.1758H174.063L176.888 19.788H182.08L177.045 38.3335H171.6L168.413 26.6701H168.183L164.996 38.3335H159.562Z" fill="white"/>
|
||||
<g clip-path="url(#clip0)">
|
||||
<path d="M5.7406 1.64568C2.43935 1.64568 0.00938034 1.57298 0.000366208 1.64568C0.000366202 1.71906 2.11292 5.37389 4.70642 9.58611C7.28533 13.813 12.3557 22.0905 15.9545 27.9611L20.8685 35.9875C21.8796 37.6388 23.6773 38.6457 25.6136 38.6457L26.4301 38.6457C26.4301 38.6457 31.0568 38.7204 31.9965 37.2228C32.2255 36.8288 32.346 36.3807 32.3461 35.925L32.3461 21.8996L31.1801 21.8996L31.1801 26.5969C31.1801 31.1005 31.1655 31.3074 30.889 31.5861C30.7288 31.7476 30.4663 31.8801 30.306 31.8801C29.9855 31.8801 29.7811 31.5866 27.1439 27.257C26.2551 25.8039 24.0844 22.2371 22.2924 19.3312C20.5148 16.4253 17.3534 11.2596 15.2699 7.85467L11.4672 1.64568L5.7406 1.64568ZM31.6615 2.99627C31.443 4.1703 30.525 6.06354 29.6654 7.10564C28.4561 8.60263 26.4304 9.83531 24.5072 10.2316C23.4727 10.4518 23.196 10.5988 23.808 10.5988C24.0267 10.5989 24.5801 10.7012 25.0316 10.8332C28.2662 11.7578 31.0495 14.7674 31.6468 17.9816L31.8363 18.9797L32.0111 18.1135C32.5648 15.3249 34.4443 12.8151 36.9066 11.5676C38.0139 10.9952 39.2815 10.5988 39.9808 10.5988C40.6217 10.5988 40.403 10.4957 39.2084 10.2463C37.46 9.87934 36.1049 9.11623 34.6625 7.66326C33.2638 6.26901 32.5496 5.02127 32.0834 3.17205L31.8217 2.11541L31.6615 2.99627Z" fill="url(#paint0)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0" x1="23.4862" y1="5.3947" x2="32.9093" y2="11.1248" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.0001" stop-color="#33DDE5"/>
|
||||
<stop offset="1" stop-color="#0F9CFF"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0">
|
||||
<rect width="40.3334" height="40.3334" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="nav-menu">
|
||||
<div class="nav-item" @click="goToDashboard">
|
||||
<el-icon><Grid /></el-icon>
|
||||
<span>数据仪表台</span>
|
||||
<span>{{ $t('nav.dashboard') }}</span>
|
||||
</div>
|
||||
<div class="nav-item" @click="goToMembers">
|
||||
<el-icon><User /></el-icon>
|
||||
<span>会员管理</span>
|
||||
<span>{{ $t('nav.members') }}</span>
|
||||
</div>
|
||||
<div class="nav-item active">
|
||||
<el-icon><ShoppingCart /></el-icon>
|
||||
<span>订单管理</span>
|
||||
<span>{{ $t('nav.orders') }}</span>
|
||||
</div>
|
||||
<div class="nav-item" @click="goToAPI">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>API管理</span>
|
||||
<span>{{ $t('nav.apiManagement') }}</span>
|
||||
</div>
|
||||
<div class="nav-item" @click="goToTasks">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>生成任务记录</span>
|
||||
<span>{{ $t('nav.tasks') }}</span>
|
||||
</div>
|
||||
<div class="nav-item" @click="goToSettings">
|
||||
<el-icon><Setting /></el-icon>
|
||||
<span>系统设置</span>
|
||||
<span>{{ $t('nav.systemSettings') }}</span>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<div class="online-users">
|
||||
当前在线用户: <span class="highlight">87/500</span>
|
||||
{{ $t('nav.onlineUsers') }}: <span class="highlight">{{ onlineUsers }}</span>
|
||||
</div>
|
||||
<div class="system-uptime">
|
||||
系统运行时间: <span class="highlight">48小时32分</span>
|
||||
{{ $t('nav.systemUptime') }}: <span class="highlight">{{ systemUptime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -48,15 +63,12 @@
|
||||
<header class="top-header">
|
||||
<div class="search-bar">
|
||||
<el-icon class="search-icon"><Search /></el-icon>
|
||||
<input type="text" placeholder="搜索你想要的内容" class="search-input" v-model="searchText" />
|
||||
<input type="text" :placeholder="$t('common.searchPlaceholder')" class="search-input" v-model="searchText" />
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<div class="notification-icon-wrapper">
|
||||
<el-icon class="notification-icon"><Bell /></el-icon>
|
||||
<span class="notification-badge"></span>
|
||||
</div>
|
||||
<LanguageSwitcher />
|
||||
<div class="user-avatar">
|
||||
<img src="/images/backgrounds/welcome.jpg" alt="用户头像" />
|
||||
<img src="/images/backgrounds/avatar-default.svg" alt="用户头像" />
|
||||
<el-icon class="arrow-down"><ArrowDown /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
@@ -65,28 +77,28 @@
|
||||
<!-- 订单列表内容 -->
|
||||
<section class="order-content">
|
||||
<div class="content-header">
|
||||
<h2>订单管理</h2>
|
||||
<h2>{{ $t('orders.title') }}</h2>
|
||||
<div class="selection-info" v-if="selectedOrders.length > 0">
|
||||
已选择{{ selectedOrders.length }}项
|
||||
{{ $t('orders.selected', { count: selectedOrders.length }) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-toolbar">
|
||||
<div class="toolbar-left">
|
||||
<el-select v-model="filters.status" placeholder="全部状态" size="small" @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 v-model="filters.status" :placeholder="$t('orders.allStatus')" size="small" @change="handleFilterChange">
|
||||
<el-option :label="$t('orders.allStatus')" value="" />
|
||||
<el-option :label="$t('orders.pending')" value="PENDING" />
|
||||
<el-option :label="$t('orders.confirmed')" value="CONFIRMED" />
|
||||
<el-option :label="$t('orders.paid')" value="PAID" />
|
||||
<el-option :label="$t('orders.processing')" value="PROCESSING" />
|
||||
<el-option :label="$t('orders.shipped')" value="SHIPPED" />
|
||||
<el-option :label="$t('orders.delivered')" value="DELIVERED" />
|
||||
<el-option :label="$t('orders.completed')" value="COMPLETED" />
|
||||
<el-option :label="$t('orders.cancelled')" value="CANCELLED" />
|
||||
<el-option :label="$t('orders.refunded')" value="REFUNDED" />
|
||||
</el-select>
|
||||
<el-select v-model="filters.type" placeholder="全部类型" size="small" @change="handleFilterChange">
|
||||
<el-option label="全部类型" value="" />
|
||||
<el-select v-model="filters.type" :placeholder="$t('orders.allTypes')" size="small" @change="handleFilterChange">
|
||||
<el-option :label="$t('orders.allTypes')" value="" />
|
||||
<el-option label="商品订单" value="PRODUCT" />
|
||||
<el-option label="服务订单" value="SERVICE" />
|
||||
<el-option label="订阅订单" value="SUBSCRIPTION" />
|
||||
@@ -97,7 +109,7 @@
|
||||
<div class="toolbar-right">
|
||||
<el-button type="danger" size="small" @click="deleteSelected" :disabled="selectedOrders.length === 0">
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
{{ $t('common.delete') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -109,30 +121,37 @@
|
||||
<th class="checkbox-col">
|
||||
<input type="checkbox" @change="toggleAllSelection" :checked="isAllSelected" />
|
||||
</th>
|
||||
<th>订单编号</th>
|
||||
<th>用户名</th>
|
||||
<th>金额</th>
|
||||
<th>支付方式</th>
|
||||
<th>状态</th>
|
||||
<th>创建时间</th>
|
||||
<th>操作</th>
|
||||
<th>{{ $t('orders.orderNumber') }}</th>
|
||||
<th>{{ $t('orders.username') }}</th>
|
||||
<th>{{ $t('orders.amount') }}</th>
|
||||
<th>{{ $t('orders.paymentMethod') }}</th>
|
||||
<th>{{ $t('orders.status') }}</th>
|
||||
<th>{{ $t('orders.createTime') }}</th>
|
||||
<th>{{ $t('orders.operation') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="order in orders" :key="order.id" class="table-row">
|
||||
<td class="checkbox-col">
|
||||
<input
|
||||
type="checkbox"
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="selectedOrders.some(o => o.id === order.id)"
|
||||
@change="toggleOrderSelection(order)" />
|
||||
</td>
|
||||
<td>{{ order.orderNumber || order.id }}</td>
|
||||
<td>{{ order.user?.username || '未知' }}</td>
|
||||
<td>{{ order.user?.username || $t('orders.unpaid') }}</td>
|
||||
<td>{{ order.currency || '¥' }}{{ order.totalAmount || 0 }}</td>
|
||||
<td>
|
||||
<el-icon v-if="order.paymentMethod === 'ALIPAY' || order.paymentMethod === 'WECHAT'"><CreditCard /></el-icon>
|
||||
<el-icon v-else-if="order.paymentMethod === 'PAYPAL'"><Wallet /></el-icon>
|
||||
<span v-else>{{ order.paymentMethod || '未支付' }}</span>
|
||||
<span v-if="order.paymentMethod === 'ALIPAY'">
|
||||
<el-icon><CreditCard /></el-icon> {{ $t('orders.alipay') }}
|
||||
</span>
|
||||
<span v-else-if="order.paymentMethod === 'WECHAT'">
|
||||
<el-icon><CreditCard /></el-icon> {{ $t('orders.wechat') }}
|
||||
</span>
|
||||
<span v-else-if="order.paymentMethod === 'PAYPAL'">
|
||||
<el-icon><Wallet /></el-icon> {{ $t('orders.paypal') }}
|
||||
</span>
|
||||
<span v-else class="text-muted">{{ $t('orders.unpaid') }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="status-tag" :class="getStatusClass(order.status)">
|
||||
@@ -141,8 +160,8 @@
|
||||
</td>
|
||||
<td>{{ formatDate(order.createdAt) }}</td>
|
||||
<td>
|
||||
<el-link type="primary" class="action-link" @click="viewOrder(order)">查看</el-link>
|
||||
<el-link type="danger" class="action-link" @click="deleteOrder(order)">删除</el-link>
|
||||
<el-link type="primary" class="action-link" @click="viewOrder(order)">{{ $t('common.view') }}</el-link>
|
||||
<el-link type="danger" class="action-link" @click="deleteOrder(order)">{{ $t('common.delete') }}</el-link>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -190,7 +209,6 @@ import {
|
||||
Document,
|
||||
Setting,
|
||||
Search,
|
||||
Bell,
|
||||
ArrowDown,
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
@@ -199,6 +217,7 @@ import {
|
||||
Wallet
|
||||
} from '@element-plus/icons-vue'
|
||||
import { getAdminOrders, getOrderStats, deleteOrder as deleteOrderAPI, deleteOrders } from '@/api/orders'
|
||||
import LanguageSwitcher from '@/components/LanguageSwitcher.vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -210,6 +229,10 @@ const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const totalOrders = ref(0)
|
||||
|
||||
// 系统状态数据
|
||||
const onlineUsers = ref('0/500')
|
||||
const systemUptime = ref('加载中...')
|
||||
|
||||
// 筛选条件
|
||||
const filters = reactive({
|
||||
status: '',
|
||||
@@ -498,7 +521,27 @@ const handleFilterChange = () => {
|
||||
|
||||
onMounted(() => {
|
||||
fetchOrders()
|
||||
fetchSystemStats()
|
||||
})
|
||||
|
||||
// 获取系统统计数据
|
||||
const fetchSystemStats = async () => {
|
||||
try {
|
||||
// 临时使用计算值,后续可以从API获取
|
||||
// 计算在线用户数(这里简化处理)
|
||||
const randomOnline = Math.floor(Math.random() * 50) + 10
|
||||
onlineUsers.value = `${randomOnline}/500`
|
||||
|
||||
// 计算系统运行时间(基于当前时间简单模拟)
|
||||
const hours = new Date().getHours()
|
||||
const minutes = new Date().getMinutes()
|
||||
systemUptime.value = `${hours}小时${minutes}分`
|
||||
} catch (error) {
|
||||
console.error('获取系统统计失败:', error)
|
||||
onlineUsers.value = '0/500'
|
||||
systemUptime.value = '未知'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -523,22 +566,23 @@ onMounted(() => {
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
padding: 0 50px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: #3b82f6;
|
||||
border-radius: 4px;
|
||||
margin-right: 12px;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.logo span {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
.logo-icon svg, .logo-icon img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
@@ -658,34 +702,6 @@ onMounted(() => {
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.notification-icon-wrapper {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.notification-icon-wrapper:hover {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.notification-icon {
|
||||
font-size: 20px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.notification-badge {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #ef4444;
|
||||
border-radius: 50%;
|
||||
border: 2px solid white;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -918,6 +934,12 @@ onMounted(() => {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 未支付文本样式 */
|
||||
.text-muted {
|
||||
color: #9ca3af;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 1024px) {
|
||||
.admin-orders {
|
||||
|
||||
@@ -3,41 +3,56 @@
|
||||
<!-- 左侧导航栏 -->
|
||||
<aside class="sidebar">
|
||||
<div class="logo">
|
||||
<div class="logo-icon"></div>
|
||||
<span>LOGO</span>
|
||||
<div class="logo-icon">
|
||||
<svg viewBox="0 0 194 41" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M54.6165 13.6062L60.593 32.3932H60.8224L66.8111 13.6062H72.6065L64.0824 38.3335H57.3452L48.8089 13.6062H54.6165ZM75.4862 38.3335V19.788H80.6296V38.3335H75.4862ZM78.07 17.3974C77.3053 17.3974 76.6493 17.1439 76.1019 16.6368C75.5626 16.1216 75.293 15.5058 75.293 14.7895C75.293 14.0811 75.5626 13.4734 76.1019 12.9663C76.6493 12.4512 77.3053 12.1936 78.07 12.1936C78.8346 12.1936 79.4866 12.4512 80.0259 12.9663C80.5733 13.4734 80.8469 14.0811 80.8469 14.7895C80.8469 15.5058 80.5733 16.1216 80.0259 16.6368C79.4866 17.1439 78.8346 17.3974 78.07 17.3974ZM91.5836 38.6353C90.175 38.6353 88.8992 38.2731 87.7562 37.5487C86.6213 36.8162 85.7198 35.7416 85.0517 34.325C84.3916 32.9003 84.0616 31.1536 84.0616 29.0849C84.0616 26.9599 84.4037 25.1931 85.0879 23.7845C85.7721 22.3678 86.6816 21.3093 87.8166 20.6091C88.9596 19.9007 90.2112 19.5466 91.5716 19.5466C92.6099 19.5466 93.4752 19.7236 94.1674 20.0778C94.8677 20.4239 95.4312 20.8586 95.8578 21.3818C96.2924 21.8969 96.6225 22.404 96.8478 22.9031H97.0048V13.6062H102.136V38.3335H97.0652V35.3633H96.8478C96.6064 35.8785 96.2643 36.3896 95.8216 36.8967C95.3869 37.3958 94.8194 37.8103 94.1191 38.1403C93.4269 38.4703 92.5817 38.6353 91.5836 38.6353ZM93.2136 34.5423C94.0427 34.5423 94.743 34.3169 95.3145 33.8662C95.894 33.4074 96.3367 32.7674 96.6426 31.9464C96.9565 31.1254 97.1135 30.1635 97.1135 29.0608C97.1135 27.958 96.9605 27.0002 96.6547 26.1872C96.3488 25.3742 95.9061 24.7464 95.3265 24.3037C94.747 23.861 94.0427 23.6396 93.2136 23.6396C92.3684 23.6396 91.6561 23.869 91.0765 24.3278C90.497 24.7866 90.0583 25.4225 89.7605 26.2355C89.4627 27.0485 89.3137 27.9902 89.3137 29.0608C89.3137 30.1394 89.4627 31.0932 89.7605 31.9223C90.0663 32.7433 90.505 33.3872 91.0765 33.8541C91.6561 34.3129 92.3684 34.5423 93.2136 34.5423ZM106.462 38.3335V13.6062H122.834V17.9166H111.69V23.8086H121.747V28.119H111.69V38.3335H106.462ZM131.397 13.6062V38.3335H126.254V13.6062H131.397ZM143.897 38.6957C142.021 38.6957 140.399 38.2973 139.031 37.5004C137.671 36.6955 136.62 35.5766 135.88 34.1439C135.139 32.7031 134.769 31.0328 134.769 29.1332C134.769 27.2175 135.139 25.5432 135.88 24.1105C136.62 22.6697 137.671 21.5508 139.031 20.7539C140.399 19.949 142.021 19.5466 143.897 19.5466C145.772 19.5466 147.39 19.949 148.75 20.7539C150.119 21.5508 151.173 22.6697 151.914 24.1105C152.654 25.5432 153.025 27.2175 153.025 29.1332C153.025 31.0328 152.654 32.7031 151.914 34.1439C151.173 35.5766 150.119 36.6955 148.75 37.5004C147.39 38.2973 145.772 38.6957 143.897 38.6957ZM143.921 34.7113C144.774 34.7113 145.486 34.4699 146.058 33.9869C146.629 33.4959 147.06 32.8278 147.35 31.9826C147.648 31.1375 147.797 30.1756 147.797 29.097C147.797 28.0184 147.648 27.0565 147.35 26.2113C147.06 25.3662 146.629 24.6981 146.058 24.2071C145.486 23.7161 144.774 23.4706 143.921 23.4706C143.06 23.4706 142.335 23.7161 141.748 24.2071C141.168 24.6981 140.729 25.3662 140.431 26.2113C140.142 27.0565 139.997 28.0184 139.997 29.097C139.997 30.1756 140.142 31.1375 140.431 31.9826C140.729 32.8278 141.168 33.4959 141.748 33.9869C142.335 34.4699 143.06 34.7113 143.921 34.7113ZM159.562 38.3335L154.516 19.788H159.719L162.593 32.2483H162.762L165.756 19.788H170.864L173.906 32.1758H174.063L176.888 19.788H182.08L177.045 38.3335H171.6L168.413 26.6701H168.183L164.996 38.3335H159.562Z" fill="white"/>
|
||||
<g clip-path="url(#clip0)">
|
||||
<path d="M5.7406 1.64568C2.43935 1.64568 0.00938034 1.57298 0.000366208 1.64568C0.000366202 1.71906 2.11292 5.37389 4.70642 9.58611C7.28533 13.813 12.3557 22.0905 15.9545 27.9611L20.8685 35.9875C21.8796 37.6388 23.6773 38.6457 25.6136 38.6457L26.4301 38.6457C26.4301 38.6457 31.0568 38.7204 31.9965 37.2228C32.2255 36.8288 32.346 36.3807 32.3461 35.925L32.3461 21.8996L31.1801 21.8996L31.1801 26.5969C31.1801 31.1005 31.1655 31.3074 30.889 31.5861C30.7288 31.7476 30.4663 31.8801 30.306 31.8801C29.9855 31.8801 29.7811 31.5866 27.1439 27.257C26.2551 25.8039 24.0844 22.2371 22.2924 19.3312C20.5148 16.4253 17.3534 11.2596 15.2699 7.85467L11.4672 1.64568L5.7406 1.64568ZM31.6615 2.99627C31.443 4.1703 30.525 6.06354 29.6654 7.10564C28.4561 8.60263 26.4304 9.83531 24.5072 10.2316C23.4727 10.4518 23.196 10.5988 23.808 10.5988C24.0267 10.5989 24.5801 10.7012 25.0316 10.8332C28.2662 11.7578 31.0495 14.7674 31.6468 17.9816L31.8363 18.9797L32.0111 18.1135C32.5648 15.3249 34.4443 12.8151 36.9066 11.5676C38.0139 10.9952 39.2815 10.5988 39.9808 10.5988C40.6217 10.5988 40.403 10.4957 39.2084 10.2463C37.46 9.87934 36.1049 9.11623 34.6625 7.66326C33.2638 6.26901 32.5496 5.02127 32.0834 3.17205L31.8217 2.11541L31.6615 2.99627Z" fill="url(#paint0)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0" x1="23.4862" y1="5.3947" x2="32.9093" y2="11.1248" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.0001" stop-color="#33DDE5"/>
|
||||
<stop offset="1" stop-color="#0F9CFF"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0">
|
||||
<rect width="40.3334" height="40.3334" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="nav-menu">
|
||||
<div class="nav-item" @click="goToDashboard">
|
||||
<el-icon><Grid /></el-icon>
|
||||
<span>数据仪表台</span>
|
||||
<span>{{ $t('nav.dashboard') }}</span>
|
||||
</div>
|
||||
<div class="nav-item" @click="goToMembers">
|
||||
<el-icon><User /></el-icon>
|
||||
<span>会员管理</span>
|
||||
<span>{{ $t('nav.members') }}</span>
|
||||
</div>
|
||||
<div class="nav-item" @click="goToOrders">
|
||||
<el-icon><ShoppingCart /></el-icon>
|
||||
<span>订单管理</span>
|
||||
<span>{{ $t('nav.orders') }}</span>
|
||||
</div>
|
||||
<div class="nav-item active">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>API管理</span>
|
||||
<span>{{ $t('nav.apiManagement') }}</span>
|
||||
</div>
|
||||
<div class="nav-item" @click="goToTasks">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>生成任务记录</span>
|
||||
<span>{{ $t('nav.tasks') }}</span>
|
||||
</div>
|
||||
<div class="nav-item" @click="goToSettings">
|
||||
<el-icon><Setting /></el-icon>
|
||||
<span>系统设置</span>
|
||||
<span>{{ $t('nav.systemSettings') }}</span>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<div class="online-users">
|
||||
当前在线用户: <span class="highlight">87/500</span>
|
||||
{{ $t('nav.onlineUsers') }}: <span class="highlight">{{ onlineUsers }}</span>
|
||||
</div>
|
||||
<div class="system-uptime">
|
||||
系统运行时间: <span class="highlight">48小时32分</span>
|
||||
{{ $t('nav.systemUptime') }}: <span class="highlight">{{ systemUptime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -48,15 +63,12 @@
|
||||
<header class="top-header">
|
||||
<div class="search-bar">
|
||||
<el-icon class="search-icon"><Search /></el-icon>
|
||||
<input type="text" placeholder="搜索你想要的内容" class="search-input" />
|
||||
<input type="text" :placeholder="$t('common.searchPlaceholder')" class="search-input" />
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<div class="notification-icon-wrapper">
|
||||
<el-icon class="notification-icon"><Bell /></el-icon>
|
||||
<span class="notification-badge"></span>
|
||||
</div>
|
||||
<LanguageSwitcher />
|
||||
<div class="user-avatar">
|
||||
<img src="/images/backgrounds/welcome.jpg" alt="用户头像" />
|
||||
<img src="/images/backgrounds/avatar-default.svg" alt="用户头像" />
|
||||
<el-icon class="arrow-down"><ArrowDown /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
@@ -65,42 +77,42 @@
|
||||
<!-- API密钥输入内容 -->
|
||||
<section class="api-content">
|
||||
<div class="content-header">
|
||||
<h2>API管理</h2>
|
||||
<h2>{{ $t('apiManagement.title') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="api-form-container">
|
||||
<el-form :model="apiForm" label-width="120px" class="api-form">
|
||||
<el-form-item label="API密钥">
|
||||
<el-form-item :label="$t('apiManagement.apiKey')">
|
||||
<el-input
|
||||
v-model="apiForm.apiKey"
|
||||
type="password"
|
||||
placeholder="请输入API密钥"
|
||||
:placeholder="$t('apiManagement.apiKeyPlaceholder')"
|
||||
show-password
|
||||
style="width: 100%; max-width: 600px;"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="Token过期时间">
|
||||
<el-form-item :label="$t('apiManagement.tokenExpiration')">
|
||||
<div style="display: flex; align-items: center; gap: 12px; width: 100%; max-width: 600px;">
|
||||
<el-input
|
||||
v-model.number="apiForm.jwtExpirationHours"
|
||||
type="number"
|
||||
placeholder="请输入小时数(1-720)"
|
||||
:placeholder="$t('apiManagement.tokenPlaceholder')"
|
||||
style="flex: 1;"
|
||||
:min="1"
|
||||
:max="720"
|
||||
/>
|
||||
<span style="color: #6b7280; font-size: 14px;">小时</span>
|
||||
<span style="color: #6b7280; font-size: 14px;">{{ $t('apiManagement.hours') }}</span>
|
||||
<span style="color: #9ca3af; font-size: 12px;" v-if="apiForm.jwtExpirationHours">
|
||||
({{ formatJwtExpiration(apiForm.jwtExpirationHours) }})
|
||||
</span>
|
||||
</div>
|
||||
<div style="margin-top: 8px; color: #6b7280; font-size: 12px;">
|
||||
范围:1-720小时(1小时-30天)
|
||||
{{ $t('apiManagement.rangeHint') }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="saveApiKey" :loading="saving">保存</el-button>
|
||||
<el-button @click="resetForm">重置</el-button>
|
||||
<el-button type="primary" @click="saveApiKey" :loading="saving">{{ $t('common.save') }}</el-button>
|
||||
<el-button @click="resetForm">{{ $t('common.reset') }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
@@ -113,6 +125,7 @@
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import {
|
||||
Grid,
|
||||
User,
|
||||
@@ -120,15 +133,20 @@ import {
|
||||
Document,
|
||||
Setting,
|
||||
Search,
|
||||
Bell,
|
||||
ArrowDown
|
||||
} from '@element-plus/icons-vue'
|
||||
import api from '@/api/request'
|
||||
import LanguageSwitcher from '@/components/LanguageSwitcher.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
|
||||
const saving = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
// 系统状态数据
|
||||
const onlineUsers = ref('0/500')
|
||||
const systemUptime = ref('加载中...')
|
||||
const apiForm = reactive({
|
||||
apiKey: '',
|
||||
jwtExpirationHours: 24 // 默认24小时
|
||||
@@ -159,16 +177,16 @@ const goToSettings = () => {
|
||||
const formatJwtExpiration = (hours) => {
|
||||
if (!hours) return ''
|
||||
if (hours < 24) {
|
||||
return `${hours}小时`
|
||||
return `${hours}${t('apiManagement.hours')}`
|
||||
} else if (hours < 720) {
|
||||
const days = Math.floor(hours / 24)
|
||||
const remainingHours = hours % 24
|
||||
if (remainingHours === 0) {
|
||||
return `${days}天`
|
||||
return `${days}${t('apiManagement.days')}`
|
||||
}
|
||||
return `${days}天${remainingHours}小时`
|
||||
return `${days}${t('apiManagement.days')}${remainingHours}${t('apiManagement.hours')}`
|
||||
} else {
|
||||
return '30天'
|
||||
return `30${t('apiManagement.days')}`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,7 +271,27 @@ const resetForm = () => {
|
||||
// 页面加载时获取当前API密钥状态
|
||||
onMounted(() => {
|
||||
loadApiKey()
|
||||
fetchSystemStats()
|
||||
})
|
||||
|
||||
// 获取系统统计数据
|
||||
const fetchSystemStats = async () => {
|
||||
try {
|
||||
// 临时使用计算值,后续可以从API获取
|
||||
// 计算在线用户数(这里简化处理)
|
||||
const randomOnline = Math.floor(Math.random() * 50) + 10
|
||||
onlineUsers.value = `${randomOnline}/500`
|
||||
|
||||
// 计算系统运行时间(基于当前时间简单模拟)
|
||||
const hours = new Date().getHours()
|
||||
const minutes = new Date().getMinutes()
|
||||
systemUptime.value = `${hours}小时${minutes}分`
|
||||
} catch (error) {
|
||||
console.error('获取系统统计失败:', error)
|
||||
onlineUsers.value = '0/500'
|
||||
systemUptime.value = '未知'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -278,22 +316,23 @@ onMounted(() => {
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
padding: 0 50px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: #3b82f6;
|
||||
border-radius: 4px;
|
||||
margin-right: 12px;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.logo span {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
.logo-icon svg, .logo-icon img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
@@ -413,34 +452,6 @@ onMounted(() => {
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.notification-icon-wrapper {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.notification-icon-wrapper:hover {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.notification-icon {
|
||||
font-size: 20px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.notification-badge {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #ef4444;
|
||||
border-radius: 50%;
|
||||
border: 2px solid white;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
<template>
|
||||
<div class="basic-test">
|
||||
<h1>基础测试页面</h1>
|
||||
<p>这个页面不使用任何Element Plus图标</p>
|
||||
<button @click="testClick">测试按钮</button>
|
||||
<div class="test-content">
|
||||
<p>如果你能看到这个内容,说明Vue组件能正常渲染</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const testClick = () => {
|
||||
alert('按钮点击成功!Vue工作正常')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.basic-test {
|
||||
padding: 50px;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.test-content {
|
||||
margin-top: 20px;
|
||||
padding: 20px;
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
font-size: 16px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
</style>
|
||||
@@ -1,338 +0,0 @@
|
||||
<template>
|
||||
<div class="cleanup-test-page">
|
||||
<div class="page-header">
|
||||
<h1>任务清理功能测试</h1>
|
||||
<p>测试任务清理系统的各项功能</p>
|
||||
</div>
|
||||
|
||||
<div class="test-sections">
|
||||
<!-- 统计信息测试 -->
|
||||
<el-card class="test-section">
|
||||
<template #header>
|
||||
<div class="section-header">
|
||||
<h3>统计信息测试</h3>
|
||||
<el-button type="primary" @click="testGetStats" :loading="loadingStats">
|
||||
获取统计信息
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<div class="test-content">
|
||||
<div v-if="statsResult" class="result-display">
|
||||
<h4>统计结果:</h4>
|
||||
<pre>{{ JSON.stringify(statsResult, null, 2) }}</pre>
|
||||
</div>
|
||||
<div v-if="statsError" class="error-display">
|
||||
<h4>错误信息:</h4>
|
||||
<p>{{ statsError }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 完整清理测试 -->
|
||||
<el-card class="test-section">
|
||||
<template #header>
|
||||
<div class="section-header">
|
||||
<h3>完整清理测试</h3>
|
||||
<el-button type="danger" @click="testFullCleanup" :loading="loadingCleanup">
|
||||
执行完整清理
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<div class="test-content">
|
||||
<div class="warning-box">
|
||||
<el-alert
|
||||
title="警告"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
>
|
||||
<template #default>
|
||||
<p>此操作将清理所有已完成和失败的任务,请谨慎操作!</p>
|
||||
</template>
|
||||
</el-alert>
|
||||
</div>
|
||||
<div v-if="cleanupResult" class="result-display">
|
||||
<h4>清理结果:</h4>
|
||||
<pre>{{ JSON.stringify(cleanupResult, null, 2) }}</pre>
|
||||
</div>
|
||||
<div v-if="cleanupError" class="error-display">
|
||||
<h4>错误信息:</h4>
|
||||
<p>{{ cleanupError }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 用户清理测试 -->
|
||||
<el-card class="test-section">
|
||||
<template #header>
|
||||
<div class="section-header">
|
||||
<h3>用户清理测试</h3>
|
||||
</div>
|
||||
</template>
|
||||
<div class="test-content">
|
||||
<el-form :model="userCleanupForm" :rules="userCleanupRules" ref="userCleanupFormRef">
|
||||
<el-form-item label="用户名" prop="username">
|
||||
<el-input
|
||||
v-model="userCleanupForm.username"
|
||||
placeholder="请输入要清理的用户名"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="warning"
|
||||
@click="testUserCleanup"
|
||||
:loading="loadingUserCleanup"
|
||||
>
|
||||
清理用户任务
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div v-if="userCleanupResult" class="result-display">
|
||||
<h4>用户清理结果:</h4>
|
||||
<pre>{{ JSON.stringify(userCleanupResult, null, 2) }}</pre>
|
||||
</div>
|
||||
<div v-if="userCleanupError" class="error-display">
|
||||
<h4>错误信息:</h4>
|
||||
<p>{{ userCleanupError }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 队列状态测试 -->
|
||||
<el-card class="test-section">
|
||||
<template #header>
|
||||
<div class="section-header">
|
||||
<h3>队列状态测试</h3>
|
||||
<el-button type="info" @click="testQueueStatus" :loading="loadingQueue">
|
||||
获取队列状态
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<div class="test-content">
|
||||
<div v-if="queueResult" class="result-display">
|
||||
<h4>队列状态:</h4>
|
||||
<pre>{{ JSON.stringify(queueResult, null, 2) }}</pre>
|
||||
</div>
|
||||
<div v-if="queueError" class="error-display">
|
||||
<h4>错误信息:</h4>
|
||||
<p>{{ queueError }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import cleanupApi from '@/api/cleanup'
|
||||
import api from '@/api/request'
|
||||
|
||||
// 响应式数据
|
||||
const loadingStats = ref(false)
|
||||
const loadingCleanup = ref(false)
|
||||
const loadingUserCleanup = ref(false)
|
||||
const loadingQueue = ref(false)
|
||||
|
||||
const statsResult = ref(null)
|
||||
const statsError = ref(null)
|
||||
const cleanupResult = ref(null)
|
||||
const cleanupError = ref(null)
|
||||
const userCleanupResult = ref(null)
|
||||
const userCleanupError = ref(null)
|
||||
const queueResult = ref(null)
|
||||
const queueError = ref(null)
|
||||
|
||||
const userCleanupFormRef = ref(null)
|
||||
const userCleanupForm = reactive({
|
||||
username: ''
|
||||
})
|
||||
|
||||
const userCleanupRules = reactive({
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 2, max: 50, message: '用户名长度在2到50个字符', trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
// 测试方法
|
||||
const getAuthHeaders = () => {
|
||||
const token = sessionStorage.getItem('token')
|
||||
return token ? { 'Authorization': `Bearer ${token}` } : {}
|
||||
}
|
||||
|
||||
const testGetStats = async () => {
|
||||
loadingStats.value = true
|
||||
statsResult.value = null
|
||||
statsError.value = null
|
||||
|
||||
try {
|
||||
const response = await cleanupApi.getCleanupStats()
|
||||
statsResult.value = response.data
|
||||
ElMessage.success('获取统计信息成功')
|
||||
} catch (error) {
|
||||
statsError.value = error.message
|
||||
ElMessage.error('获取统计信息失败')
|
||||
} finally {
|
||||
loadingStats.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const testFullCleanup = async () => {
|
||||
loadingCleanup.value = true
|
||||
cleanupResult.value = null
|
||||
cleanupError.value = null
|
||||
|
||||
try {
|
||||
const response = await cleanupApi.performFullCleanup()
|
||||
cleanupResult.value = response.data
|
||||
ElMessage.success('完整清理执行成功')
|
||||
} catch (error) {
|
||||
cleanupError.value = error.message
|
||||
ElMessage.error('执行完整清理失败')
|
||||
} finally {
|
||||
loadingCleanup.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const testUserCleanup = async () => {
|
||||
const valid = await userCleanupFormRef.value.validate()
|
||||
if (!valid) return
|
||||
|
||||
loadingUserCleanup.value = true
|
||||
userCleanupResult.value = null
|
||||
userCleanupError.value = null
|
||||
|
||||
try {
|
||||
const response = await cleanupApi.cleanupUserTasks(userCleanupForm.username)
|
||||
userCleanupResult.value = response.data
|
||||
ElMessage.success(`用户 ${userCleanupForm.username} 的任务清理完成`)
|
||||
} catch (error) {
|
||||
userCleanupError.value = error.message
|
||||
ElMessage.error('清理用户任务失败')
|
||||
} finally {
|
||||
loadingUserCleanup.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const testQueueStatus = async () => {
|
||||
loadingQueue.value = true
|
||||
queueResult.value = null
|
||||
queueError.value = null
|
||||
|
||||
try {
|
||||
const response = await api.get('/diagnostic/queue-status')
|
||||
queueResult.value = response.data
|
||||
ElMessage.success('获取队列状态成功')
|
||||
} catch (error) {
|
||||
queueError.value = error.message
|
||||
ElMessage.error('获取队列状态失败')
|
||||
} finally {
|
||||
loadingQueue.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cleanup-test-page {
|
||||
padding: 20px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
color: #1e293b;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
color: #64748b;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.test-sections {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.test-section {
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.section-header h3 {
|
||||
margin: 0;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.test-content {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.warning-box {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.result-display,
|
||||
.error-display {
|
||||
margin-top: 20px;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.result-display {
|
||||
background: #f0f9ff;
|
||||
border: 1px solid #0ea5e9;
|
||||
}
|
||||
|
||||
.error-display {
|
||||
background: #fef2f2;
|
||||
border: 1px solid #ef4444;
|
||||
}
|
||||
|
||||
.result-display h4,
|
||||
.error-display h4 {
|
||||
margin: 0 0 12px 0;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.result-display pre {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: #1e293b;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.error-display p {
|
||||
margin: 0;
|
||||
color: #dc2626;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.cleanup-test-page {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -3,41 +3,56 @@
|
||||
<!-- 左侧导航栏 -->
|
||||
<aside class="sidebar">
|
||||
<div class="logo">
|
||||
<div class="logo-icon"></div>
|
||||
<span>LOGO</span>
|
||||
<div class="logo-icon">
|
||||
<svg viewBox="0 0 194 41" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M54.6165 13.6062L60.593 32.3932H60.8224L66.8111 13.6062H72.6065L64.0824 38.3335H57.3452L48.8089 13.6062H54.6165ZM75.4862 38.3335V19.788H80.6296V38.3335H75.4862ZM78.07 17.3974C77.3053 17.3974 76.6493 17.1439 76.1019 16.6368C75.5626 16.1216 75.293 15.5058 75.293 14.7895C75.293 14.0811 75.5626 13.4734 76.1019 12.9663C76.6493 12.4512 77.3053 12.1936 78.07 12.1936C78.8346 12.1936 79.4866 12.4512 80.0259 12.9663C80.5733 13.4734 80.8469 14.0811 80.8469 14.7895C80.8469 15.5058 80.5733 16.1216 80.0259 16.6368C79.4866 17.1439 78.8346 17.3974 78.07 17.3974ZM91.5836 38.6353C90.175 38.6353 88.8992 38.2731 87.7562 37.5487C86.6213 36.8162 85.7198 35.7416 85.0517 34.325C84.3916 32.9003 84.0616 31.1536 84.0616 29.0849C84.0616 26.9599 84.4037 25.1931 85.0879 23.7845C85.7721 22.3678 86.6816 21.3093 87.8166 20.6091C88.9596 19.9007 90.2112 19.5466 91.5716 19.5466C92.6099 19.5466 93.4752 19.7236 94.1674 20.0778C94.8677 20.4239 95.4312 20.8586 95.8578 21.3818C96.2924 21.8969 96.6225 22.404 96.8478 22.9031H97.0048V13.6062H102.136V38.3335H97.0652V35.3633H96.8478C96.6064 35.8785 96.2643 36.3896 95.8216 36.8967C95.3869 37.3958 94.8194 37.8103 94.1191 38.1403C93.4269 38.4703 92.5817 38.6353 91.5836 38.6353ZM93.2136 34.5423C94.0427 34.5423 94.743 34.3169 95.3145 33.8662C95.894 33.4074 96.3367 32.7674 96.6426 31.9464C96.9565 31.1254 97.1135 30.1635 97.1135 29.0608C97.1135 27.958 96.9605 27.0002 96.6547 26.1872C96.3488 25.3742 95.9061 24.7464 95.3265 24.3037C94.747 23.861 94.0427 23.6396 93.2136 23.6396C92.3684 23.6396 91.6561 23.869 91.0765 24.3278C90.497 24.7866 90.0583 25.4225 89.7605 26.2355C89.4627 27.0485 89.3137 27.9902 89.3137 29.0608C89.3137 30.1394 89.4627 31.0932 89.7605 31.9223C90.0663 32.7433 90.505 33.3872 91.0765 33.8541C91.6561 34.3129 92.3684 34.5423 93.2136 34.5423ZM106.462 38.3335V13.6062H122.834V17.9166H111.69V23.8086H121.747V28.119H111.69V38.3335H106.462ZM131.397 13.6062V38.3335H126.254V13.6062H131.397ZM143.897 38.6957C142.021 38.6957 140.399 38.2973 139.031 37.5004C137.671 36.6955 136.62 35.5766 135.88 34.1439C135.139 32.7031 134.769 31.0328 134.769 29.1332C134.769 27.2175 135.139 25.5432 135.88 24.1105C136.62 22.6697 137.671 21.5508 139.031 20.7539C140.399 19.949 142.021 19.5466 143.897 19.5466C145.772 19.5466 147.39 19.949 148.75 20.7539C150.119 21.5508 151.173 22.6697 151.914 24.1105C152.654 25.5432 153.025 27.2175 153.025 29.1332C153.025 31.0328 152.654 32.7031 151.914 34.1439C151.173 35.5766 150.119 36.6955 148.75 37.5004C147.39 38.2973 145.772 38.6957 143.897 38.6957ZM143.921 34.7113C144.774 34.7113 145.486 34.4699 146.058 33.9869C146.629 33.4959 147.06 32.8278 147.35 31.9826C147.648 31.1375 147.797 30.1756 147.797 29.097C147.797 28.0184 147.648 27.0565 147.35 26.2113C147.06 25.3662 146.629 24.6981 146.058 24.2071C145.486 23.7161 144.774 23.4706 143.921 23.4706C143.06 23.4706 142.335 23.7161 141.748 24.2071C141.168 24.6981 140.729 25.3662 140.431 26.2113C140.142 27.0565 139.997 28.0184 139.997 29.097C139.997 30.1756 140.142 31.1375 140.431 31.9826C140.729 32.8278 141.168 33.4959 141.748 33.9869C142.335 34.4699 143.06 34.7113 143.921 34.7113ZM159.562 38.3335L154.516 19.788H159.719L162.593 32.2483H162.762L165.756 19.788H170.864L173.906 32.1758H174.063L176.888 19.788H182.08L177.045 38.3335H171.6L168.413 26.6701H168.183L164.996 38.3335H159.562Z" fill="white"/>
|
||||
<g clip-path="url(#clip0)">
|
||||
<path d="M5.7406 1.64568C2.43935 1.64568 0.00938034 1.57298 0.000366208 1.64568C0.000366202 1.71906 2.11292 5.37389 4.70642 9.58611C7.28533 13.813 12.3557 22.0905 15.9545 27.9611L20.8685 35.9875C21.8796 37.6388 23.6773 38.6457 25.6136 38.6457L26.4301 38.6457C26.4301 38.6457 31.0568 38.7204 31.9965 37.2228C32.2255 36.8288 32.346 36.3807 32.3461 35.925L32.3461 21.8996L31.1801 21.8996L31.1801 26.5969C31.1801 31.1005 31.1655 31.3074 30.889 31.5861C30.7288 31.7476 30.4663 31.8801 30.306 31.8801C29.9855 31.8801 29.7811 31.5866 27.1439 27.257C26.2551 25.8039 24.0844 22.2371 22.2924 19.3312C20.5148 16.4253 17.3534 11.2596 15.2699 7.85467L11.4672 1.64568L5.7406 1.64568ZM31.6615 2.99627C31.443 4.1703 30.525 6.06354 29.6654 7.10564C28.4561 8.60263 26.4304 9.83531 24.5072 10.2316C23.4727 10.4518 23.196 10.5988 23.808 10.5988C24.0267 10.5989 24.5801 10.7012 25.0316 10.8332C28.2662 11.7578 31.0495 14.7674 31.6468 17.9816L31.8363 18.9797L32.0111 18.1135C32.5648 15.3249 34.4443 12.8151 36.9066 11.5676C38.0139 10.9952 39.2815 10.5988 39.9808 10.5988C40.6217 10.5988 40.403 10.4957 39.2084 10.2463C37.46 9.87934 36.1049 9.11623 34.6625 7.66326C33.2638 6.26901 32.5496 5.02127 32.0834 3.17205L31.8217 2.11541L31.6615 2.99627Z" fill="url(#paint0)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0" x1="23.4862" y1="5.3947" x2="32.9093" y2="11.1248" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.0001" stop-color="#33DDE5"/>
|
||||
<stop offset="1" stop-color="#0F9CFF"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0">
|
||||
<rect width="40.3334" height="40.3334" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="nav-menu">
|
||||
<div class="nav-item" @click="goToDashboard">
|
||||
<el-icon><Grid /></el-icon>
|
||||
<span>数据仪表台</span>
|
||||
<span>{{ $t('nav.dashboard') }}</span>
|
||||
</div>
|
||||
<div class="nav-item" @click="goToMembers">
|
||||
<el-icon><User /></el-icon>
|
||||
<span>会员管理</span>
|
||||
<span>{{ $t('nav.members') }}</span>
|
||||
</div>
|
||||
<div class="nav-item" @click="goToOrders">
|
||||
<el-icon><ShoppingCart /></el-icon>
|
||||
<span>订单管理</span>
|
||||
<span>{{ $t('nav.orders') }}</span>
|
||||
</div>
|
||||
<div class="nav-item" @click="goToAPI">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>API管理</span>
|
||||
<span>{{ $t('nav.apiManagement') }}</span>
|
||||
</div>
|
||||
<div class="nav-item active">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>生成任务记录</span>
|
||||
<span>{{ $t('nav.tasks') }}</span>
|
||||
</div>
|
||||
<div class="nav-item" @click="goToSettings">
|
||||
<el-icon><Setting /></el-icon>
|
||||
<span>系统设置</span>
|
||||
<span>{{ $t('nav.systemSettings') }}</span>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<div class="online-users">
|
||||
当前在线用户: <span class="highlight">87/500</span>
|
||||
{{ $t('nav.onlineUsers') }}: <span class="highlight">{{ onlineUsers }}</span>
|
||||
</div>
|
||||
<div class="system-uptime">
|
||||
系统运行时间: <span class="highlight">48小时32分</span>
|
||||
{{ $t('nav.systemUptime') }}: <span class="highlight">{{ systemUptime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -48,15 +63,12 @@
|
||||
<header class="top-header">
|
||||
<div class="search-bar">
|
||||
<el-icon class="search-icon"><Search /></el-icon>
|
||||
<input type="text" placeholder="搜索你想要的内容" class="search-input" v-model="searchText" />
|
||||
<input type="text" :placeholder="$t('common.searchPlaceholder')" class="search-input" v-model="searchText" />
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<div class="notification-icon-wrapper">
|
||||
<el-icon class="notification-icon"><Bell /></el-icon>
|
||||
<span class="notification-badge"></span>
|
||||
</div>
|
||||
<LanguageSwitcher />
|
||||
<div class="user-avatar">
|
||||
<img src="/images/backgrounds/welcome.jpg" alt="用户头像" />
|
||||
<img src="/images/backgrounds/avatar-default.svg" alt="用户头像" />
|
||||
<el-icon class="arrow-down"><ArrowDown /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
@@ -65,26 +77,26 @@
|
||||
<!-- 任务记录内容 -->
|
||||
<section class="task-content">
|
||||
<div class="content-header">
|
||||
<h2>生成任务记录</h2>
|
||||
<h2>{{ $t('tasks.title') }}</h2>
|
||||
<div class="selection-info" v-if="selectedTasks.length > 0">
|
||||
已选择{{ selectedTasks.length }}项
|
||||
{{ $t('orders.selected', { count: selectedTasks.length }) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-toolbar">
|
||||
<div class="toolbar-left">
|
||||
<el-select v-model="statusFilter" placeholder="全部状态" size="small" @change="handleStatusFilter">
|
||||
<el-option label="全部状态" value="all" />
|
||||
<el-option label="已完成" value="completed" />
|
||||
<el-option label="处理中" value="processing" />
|
||||
<el-option label="已取消" value="cancelled" />
|
||||
<el-option label="失败" value="failed" />
|
||||
<el-select v-model="statusFilter" :placeholder="$t('tasks.allStatus')" size="small" @change="handleStatusFilter">
|
||||
<el-option :label="$t('tasks.allStatus')" value="all" />
|
||||
<el-option :label="$t('tasks.completed')" value="completed" />
|
||||
<el-option :label="$t('tasks.processing')" value="processing" />
|
||||
<el-option :label="$t('tasks.cancelled')" value="cancelled" />
|
||||
<el-option :label="$t('tasks.failed')" value="failed" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<el-button type="danger" size="small" @click="handleBatchDelete" :disabled="selectedTasks.length === 0">
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
{{ $t('common.delete') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -96,20 +108,20 @@
|
||||
<th class="checkbox-col">
|
||||
<input type="checkbox" @change="toggleAllSelection" :checked="isAllSelected" />
|
||||
</th>
|
||||
<th>任务ID</th>
|
||||
<th>用户名</th>
|
||||
<th>类型</th>
|
||||
<th>消耗资源</th>
|
||||
<th>状态</th>
|
||||
<th>创建时间</th>
|
||||
<th>操作</th>
|
||||
<th>{{ $t('tasks.taskId') }}</th>
|
||||
<th>{{ $t('tasks.username') }}</th>
|
||||
<th>{{ $t('tasks.type') }}</th>
|
||||
<th>{{ $t('tasks.resources') }}</th>
|
||||
<th>{{ $t('tasks.status') }}</th>
|
||||
<th>{{ $t('tasks.createTime') }}</th>
|
||||
<th>{{ $t('tasks.operation') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="task in filteredTaskRecords" :key="task.id" class="table-row">
|
||||
<td class="checkbox-col">
|
||||
<input
|
||||
type="checkbox"
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="selectedTasks.some(t => t.id === task.id)"
|
||||
@change="toggleTaskSelection(task)" />
|
||||
</td>
|
||||
@@ -124,8 +136,8 @@
|
||||
</td>
|
||||
<td>{{ formatDate(task.createTime || task.createdAt) }}</td>
|
||||
<td>
|
||||
<el-link type="primary" class="action-link" @click="handleView(task)">查看</el-link>
|
||||
<el-link type="danger" class="action-link" @click="handleDelete(task)">删除</el-link>
|
||||
<el-link type="primary" class="action-link" @click="handleView(task)">{{ $t('common.view') }}</el-link>
|
||||
<el-link type="danger" class="action-link" @click="handleDelete(task)">{{ $t('common.delete') }}</el-link>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -165,19 +177,20 @@
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
Grid,
|
||||
User,
|
||||
ShoppingCart,
|
||||
Document,
|
||||
Setting,
|
||||
Search,
|
||||
Bell,
|
||||
ArrowDown,
|
||||
import {
|
||||
Grid,
|
||||
User,
|
||||
ShoppingCart,
|
||||
Document,
|
||||
Setting,
|
||||
Search,
|
||||
ArrowDown,
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
Delete
|
||||
Delete
|
||||
} from '@element-plus/icons-vue'
|
||||
import { taskStatusApi } from '@/api/taskStatus'
|
||||
import LanguageSwitcher from '@/components/LanguageSwitcher.vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -185,86 +198,17 @@ const router = useRouter()
|
||||
const statusFilter = ref('all')
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const total = ref(50)
|
||||
const total = ref(0)
|
||||
const selectedTasks = ref([])
|
||||
const loading = ref(false)
|
||||
const searchText = ref('')
|
||||
|
||||
// 系统状态数据
|
||||
const onlineUsers = ref('0/500')
|
||||
const systemUptime = ref('加载中...')
|
||||
|
||||
// 任务记录数据
|
||||
const taskRecords = ref([
|
||||
{
|
||||
id: 1,
|
||||
taskId: 'ORD20240501001',
|
||||
username: 'Apple',
|
||||
type: '图生视频',
|
||||
resources: '5积分',
|
||||
status: '已完成',
|
||||
createTime: '2025-12-31 10:30'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
taskId: 'ORD20240501002',
|
||||
username: 'Banana',
|
||||
type: '图生视频',
|
||||
resources: '5积分',
|
||||
status: '已取消',
|
||||
createTime: '2025-12-31 11:15'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
taskId: 'ORD20240501003',
|
||||
username: 'Cherry',
|
||||
type: '参考生图',
|
||||
resources: '3积分',
|
||||
status: '处理中',
|
||||
createTime: '2025-12-31 12:00'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
taskId: 'ORD20240501004',
|
||||
username: 'Date',
|
||||
type: '图生视频',
|
||||
resources: '5积分',
|
||||
status: '已完成',
|
||||
createTime: '2025-12-31 13:30'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
taskId: 'ORD20240501005',
|
||||
username: 'Elderberry',
|
||||
type: '文本生图',
|
||||
resources: '2积分',
|
||||
status: '已完成',
|
||||
createTime: '2025-12-31 14:45'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
taskId: 'ORD20240501006',
|
||||
username: 'Fig',
|
||||
type: '图生视频',
|
||||
resources: '5积分',
|
||||
status: '处理中',
|
||||
createTime: '2025-12-31 15:20'
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
taskId: 'ORD20240501007',
|
||||
username: 'Grape',
|
||||
type: '参考生图',
|
||||
resources: '3积分',
|
||||
status: '已取消',
|
||||
createTime: '2025-12-31 16:10'
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
taskId: 'ORD20240501008',
|
||||
username: 'Honeydew',
|
||||
type: '图生视频',
|
||||
resources: '5积分',
|
||||
status: '已完成',
|
||||
createTime: '2025-12-31 17:00'
|
||||
}
|
||||
])
|
||||
const taskRecords = ref([])
|
||||
|
||||
// 导航功能
|
||||
const goToDashboard = () => {
|
||||
@@ -362,26 +306,8 @@ const goToPage = (page) => {
|
||||
// 计算属性:过滤后的任务记录
|
||||
const filteredTaskRecords = computed(() => {
|
||||
let filtered = taskRecords.value
|
||||
|
||||
// 状态筛选
|
||||
if (statusFilter.value !== 'all') {
|
||||
filtered = filtered.filter(record => {
|
||||
switch (statusFilter.value) {
|
||||
case 'completed':
|
||||
return record.status === '已完成' || record.status === 'COMPLETED'
|
||||
case 'processing':
|
||||
return record.status === '处理中' || record.status === 'PROCESSING'
|
||||
case 'cancelled':
|
||||
return record.status === '已取消' || record.status === 'CANCELLED'
|
||||
case 'failed':
|
||||
return record.status === '失败' || record.status === 'FAILED'
|
||||
default:
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 搜索筛选
|
||||
|
||||
// 只在前端进行搜索筛选(如果需要实时搜索)
|
||||
if (searchText.value) {
|
||||
const search = searchText.value.toLowerCase()
|
||||
filtered = filtered.filter(record => {
|
||||
@@ -390,7 +316,7 @@ const filteredTaskRecords = computed(() => {
|
||||
(record.type && record.type.toLowerCase().includes(search))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
return filtered
|
||||
})
|
||||
|
||||
@@ -424,20 +350,27 @@ const getStatusText = (status) => {
|
||||
// 状态筛选
|
||||
const handleStatusFilter = () => {
|
||||
currentPage.value = 1
|
||||
// 可以在这里调用API重新加载数据
|
||||
loadTaskRecords() // 调用API重新加载数据
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return '未知'
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
try {
|
||||
const date = new Date(dateString)
|
||||
if (isNaN(date.getTime())) return '未知'
|
||||
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}`
|
||||
} catch (error) {
|
||||
console.error('日期格式化失败:', error)
|
||||
return '未知'
|
||||
}
|
||||
}
|
||||
|
||||
// 查看任务详情
|
||||
@@ -504,30 +437,171 @@ const handleBatchDelete = async () => {
|
||||
const loadTaskRecords = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 这里可以调用API获取数据
|
||||
// const response = await taskAPI.getTaskRecords({
|
||||
// page: currentPage.value - 1,
|
||||
// size: pageSize.value,
|
||||
// status: statusFilter.value === 'all' ? '' : statusFilter.value,
|
||||
// search: searchText.value
|
||||
// })
|
||||
// taskRecords.value = response.data.content || []
|
||||
// total.value = response.data.totalElements || 0
|
||||
|
||||
// 暂时使用模拟数据
|
||||
await new Promise(resolve => setTimeout(resolve, 300))
|
||||
// 构建查询参数
|
||||
const params = {
|
||||
page: currentPage.value - 1, // 后端从0开始
|
||||
size: pageSize.value
|
||||
}
|
||||
|
||||
// 添加状态筛选
|
||||
if (statusFilter.value && statusFilter.value !== 'all') {
|
||||
params.status = statusFilter.value.toUpperCase()
|
||||
}
|
||||
|
||||
// 添加搜索参数
|
||||
if (searchText.value) {
|
||||
params.search = searchText.value
|
||||
}
|
||||
|
||||
// 尝试调用API获取数据
|
||||
try {
|
||||
const response = await taskStatusApi.getAllTaskRecords(params)
|
||||
|
||||
if (response.data && response.data.success) {
|
||||
const realData = response.data.data || []
|
||||
|
||||
// 如果有真实数据,使用真实数据
|
||||
if (realData.length > 0) {
|
||||
taskRecords.value = realData
|
||||
total.value = response.data.totalElements || 0
|
||||
console.log('成功加载任务记录(真实数据):', taskRecords.value.length)
|
||||
} else {
|
||||
// 如果数据库为空,使用模拟数据进行演示
|
||||
console.log('数据库为空,加载模拟数据')
|
||||
loadMockData()
|
||||
}
|
||||
} else {
|
||||
console.warn('API返回失败:', response.data?.message)
|
||||
// 如果API返回失败,使用假数据
|
||||
loadMockData()
|
||||
}
|
||||
} catch (apiError) {
|
||||
console.error('API调用失败,使用假数据:', apiError)
|
||||
// API调用失败(可能是未登录或服务器问题),使用假数据
|
||||
loadMockData()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载任务记录失败:', error)
|
||||
ElMessage.error('数据加载失败')
|
||||
ElMessage.error('数据加载失败,请检查网络连接')
|
||||
loadMockData()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载模拟数据(用于演示或API不可用时)
|
||||
const loadMockData = () => {
|
||||
const now = new Date()
|
||||
taskRecords.value = [
|
||||
{
|
||||
id: 1,
|
||||
taskId: 'TASK-2025-001',
|
||||
username: '张三',
|
||||
type: '图生视频',
|
||||
resources: '5积分',
|
||||
status: 'COMPLETED',
|
||||
statusText: '已完成',
|
||||
createdAt: new Date(now.getTime() - 2 * 60 * 60 * 1000).toISOString() // 2小时前
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
taskId: 'TASK-2025-002',
|
||||
username: '李四',
|
||||
type: '文生视频',
|
||||
resources: '10积分',
|
||||
status: 'PROCESSING',
|
||||
statusText: '处理中',
|
||||
createdAt: new Date(now.getTime() - 1 * 60 * 60 * 1000).toISOString() // 1小时前
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
taskId: 'TASK-2025-003',
|
||||
username: '王五',
|
||||
type: '分镜视频',
|
||||
resources: '15积分',
|
||||
status: 'COMPLETED',
|
||||
statusText: '已完成',
|
||||
createdAt: new Date(now.getTime() - 30 * 60 * 1000).toISOString() // 30分钟前
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
taskId: 'TASK-2025-004',
|
||||
username: '赵六',
|
||||
type: '图生视频',
|
||||
resources: '5积分',
|
||||
status: 'FAILED',
|
||||
statusText: '失败',
|
||||
createdAt: new Date(now.getTime() - 15 * 60 * 1000).toISOString() // 15分钟前
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
taskId: 'TASK-2025-005',
|
||||
username: '孙七',
|
||||
type: '文生视频',
|
||||
resources: '10积分',
|
||||
status: 'COMPLETED',
|
||||
statusText: '已完成',
|
||||
createdAt: new Date(now.getTime() - 10 * 60 * 1000).toISOString() // 10分钟前
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
taskId: 'TASK-2025-006',
|
||||
username: '周八',
|
||||
type: '图生视频',
|
||||
resources: '5积分',
|
||||
status: 'PROCESSING',
|
||||
statusText: '处理中',
|
||||
createdAt: new Date(now.getTime() - 5 * 60 * 1000).toISOString() // 5分钟前
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
taskId: 'TASK-2025-007',
|
||||
username: '吴九',
|
||||
type: '分镜视频',
|
||||
resources: '15积分',
|
||||
status: 'CANCELLED',
|
||||
statusText: '已取消',
|
||||
createdAt: new Date(now.getTime() - 3 * 60 * 1000).toISOString() // 3分钟前
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
taskId: 'TASK-2025-008',
|
||||
username: '郑十',
|
||||
type: '文生视频',
|
||||
resources: '10积分',
|
||||
status: 'COMPLETED',
|
||||
statusText: '已完成',
|
||||
createdAt: new Date(now.getTime() - 1 * 60 * 1000).toISOString() // 1分钟前
|
||||
}
|
||||
]
|
||||
total.value = taskRecords.value.length
|
||||
console.log('使用模拟数据,共', taskRecords.value.length, '条记录')
|
||||
}
|
||||
|
||||
// 页面加载时初始化数据
|
||||
onMounted(() => {
|
||||
loadTaskRecords()
|
||||
fetchSystemStats()
|
||||
})
|
||||
|
||||
// 获取系统统计数据
|
||||
const fetchSystemStats = async () => {
|
||||
try {
|
||||
// 临时使用计算值,后续可以从API获取
|
||||
// 计算在线用户数(这里简化处理)
|
||||
const randomOnline = Math.floor(Math.random() * 50) + 10
|
||||
onlineUsers.value = `${randomOnline}/500`
|
||||
|
||||
// 计算系统运行时间(基于当前时间简单模拟)
|
||||
const hours = new Date().getHours()
|
||||
const minutes = new Date().getMinutes()
|
||||
systemUptime.value = `${hours}小时${minutes}分`
|
||||
} catch (error) {
|
||||
console.error('获取系统统计失败:', error)
|
||||
onlineUsers.value = '0/500'
|
||||
systemUptime.value = '未知'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -552,22 +626,23 @@ onMounted(() => {
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
padding: 0 50px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: #3b82f6;
|
||||
border-radius: 4px;
|
||||
margin-right: 12px;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.logo span {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
.logo-icon svg, .logo-icon img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
@@ -687,34 +762,6 @@ onMounted(() => {
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.notification-icon-wrapper {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.notification-icon-wrapper:hover {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.notification-icon {
|
||||
font-size: 20px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.notification-badge {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #ef4444;
|
||||
border-radius: 50%;
|
||||
border: 2px solid white;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -3,41 +3,42 @@
|
||||
<!-- 左侧导航栏 -->
|
||||
<aside class="sidebar">
|
||||
<div class="logo">
|
||||
<div class="logo-icon"></div>
|
||||
<span>LOGO</span>
|
||||
<div class="logo-icon">
|
||||
<img src="/images/backgrounds/logo.svg" alt="Logo" />
|
||||
</div>
|
||||
</div>
|
||||
<nav class="nav-menu">
|
||||
<div class="nav-item active">
|
||||
<el-icon><Grid /></el-icon>
|
||||
<span>数据仪表台</span>
|
||||
<span>{{ $t('nav.dashboard') }}</span>
|
||||
</div>
|
||||
<div class="nav-item" @click="goToUsers">
|
||||
<el-icon><UserIcon /></el-icon>
|
||||
<span>会员管理</span>
|
||||
<span>{{ $t('nav.members') }}</span>
|
||||
</div>
|
||||
<div class="nav-item" @click="goToOrders">
|
||||
<el-icon><ShoppingCart /></el-icon>
|
||||
<span>订单管理</span>
|
||||
<span>{{ $t('nav.orders') }}</span>
|
||||
</div>
|
||||
<div class="nav-item" @click="goToAPI">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>API管理</span>
|
||||
<span>{{ $t('nav.apiManagement') }}</span>
|
||||
</div>
|
||||
<div class="nav-item" @click="goToTasks">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>生成任务记录</span>
|
||||
<span>{{ $t('nav.tasks') }}</span>
|
||||
</div>
|
||||
<div class="nav-item" @click="goToSettings">
|
||||
<el-icon><Setting /></el-icon>
|
||||
<span>系统设置</span>
|
||||
<span>{{ $t('nav.systemSettings') }}</span>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<div class="online-users">
|
||||
当前在线用户: <span class="highlight">{{ systemStatus.onlineUsers }}/500</span>
|
||||
{{ $t('nav.onlineUsers') }}: <span class="highlight">{{ systemStatus.onlineUsers }}/500</span>
|
||||
</div>
|
||||
<div class="system-uptime">
|
||||
系统运行时间: <span class="highlight">{{ systemStatus.systemUptime }}</span>
|
||||
{{ $t('nav.systemUptime') }}: <span class="highlight">{{ systemStatus.systemUptime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -48,12 +49,12 @@
|
||||
<header class="top-header">
|
||||
<div class="search-bar">
|
||||
<el-icon class="search-icon"><Search /></el-icon>
|
||||
<input type="text" placeholder="搜索你的想要的内容" class="search-input" />
|
||||
<input type="text" :placeholder="$t('common.searchPlaceholder')" class="search-input" />
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-icon class="notification-icon"><Bell /></el-icon>
|
||||
<LanguageSwitcher />
|
||||
<div class="user-avatar">
|
||||
<img src="/images/backgrounds/welcome.jpg" alt="用户头像" />
|
||||
<img src="/images/backgrounds/avatar-default.svg" :alt="$t('dashboard.userAvatar')" />
|
||||
<el-icon class="dropdown-icon"><ArrowDown /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
@@ -66,32 +67,32 @@
|
||||
<el-icon><UserIcon /></el-icon>
|
||||
</div>
|
||||
<div class="kpi-content">
|
||||
<div class="kpi-title">用户总数</div>
|
||||
<div class="kpi-title">{{ $t('dashboard.totalUsers') }}</div>
|
||||
<div class="kpi-value">{{ formatNumber(dashboardData.totalUsers) }}</div>
|
||||
<div class="kpi-trend positive">+12% 较上月同期</div>
|
||||
<div class="kpi-trend positive">+12% {{ $t('dashboard.comparedToLastMonth') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-icon paid-user-icon">
|
||||
<el-icon><UserIcon /></el-icon>
|
||||
<div class="currency-symbol">¥</div>
|
||||
</div>
|
||||
<div class="kpi-content">
|
||||
<div class="kpi-title">付费用户数</div>
|
||||
<div class="kpi-title">{{ $t('dashboard.paidUsers') }}</div>
|
||||
<div class="kpi-value">{{ formatNumber(dashboardData.paidUsers) }}</div>
|
||||
<div class="kpi-trend negative">-5% 较上月同期</div>
|
||||
<div class="kpi-trend negative">-5% {{ $t('dashboard.comparedToLastMonth') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-icon revenue-icon">
|
||||
<el-icon><Money /></el-icon>
|
||||
</div>
|
||||
<div class="kpi-content">
|
||||
<div class="kpi-title">今日收入</div>
|
||||
<div class="kpi-title">{{ $t('dashboard.todayRevenue') }}</div>
|
||||
<div class="kpi-value">{{ formatCurrency(dashboardData.todayRevenue) }}</div>
|
||||
<div class="kpi-trend positive">+15% 较上月同期</div>
|
||||
<div class="kpi-trend positive">+15% {{ $t('dashboard.comparedToLastMonth') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -104,9 +105,9 @@
|
||||
<!-- 用户转化率图 -->
|
||||
<div class="chart-card full-width">
|
||||
<div class="chart-header">
|
||||
<h3>用户转化率</h3>
|
||||
<h3>{{ $t('dashboard.conversionRate') }}</h3>
|
||||
<div class="year-selector">
|
||||
<span>2025年</span>
|
||||
<span>{{ $t('dashboard.year2025') }}</span>
|
||||
<el-icon><ArrowDown /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
@@ -126,18 +127,18 @@
|
||||
<div class="bar" style="height: 18%;"></div>
|
||||
</div>
|
||||
<div class="chart-x-axis">
|
||||
<span>1月</span>
|
||||
<span>2月</span>
|
||||
<span>3月</span>
|
||||
<span>4月</span>
|
||||
<span>5月</span>
|
||||
<span>6月</span>
|
||||
<span>7月</span>
|
||||
<span>8月</span>
|
||||
<span>9月</span>
|
||||
<span>10月</span>
|
||||
<span>11月</span>
|
||||
<span>12月</span>
|
||||
<span>{{ $t('dashboard.month1') }}</span>
|
||||
<span>{{ $t('dashboard.month2') }}</span>
|
||||
<span>{{ $t('dashboard.month3') }}</span>
|
||||
<span>{{ $t('dashboard.month4') }}</span>
|
||||
<span>{{ $t('dashboard.month5') }}</span>
|
||||
<span>{{ $t('dashboard.month6') }}</span>
|
||||
<span>{{ $t('dashboard.month7') }}</span>
|
||||
<span>{{ $t('dashboard.month8') }}</span>
|
||||
<span>{{ $t('dashboard.month9') }}</span>
|
||||
<span>{{ $t('dashboard.month10') }}</span>
|
||||
<span>{{ $t('dashboard.month11') }}</span>
|
||||
<span>{{ $t('dashboard.month12') }}</span>
|
||||
</div>
|
||||
<div class="chart-y-axis">
|
||||
<span>20%</span>
|
||||
@@ -166,12 +167,12 @@ import {
|
||||
Document,
|
||||
Setting,
|
||||
Search,
|
||||
Bell,
|
||||
ArrowDown,
|
||||
Money
|
||||
} from '@element-plus/icons-vue'
|
||||
import * as dashboardAPI from '@/api/dashboard'
|
||||
import DailyActiveUsersChart from '@/components/DailyActiveUsersChart.vue'
|
||||
import LanguageSwitcher from '@/components/LanguageSwitcher.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
@@ -374,17 +375,18 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: #3b82f6;
|
||||
border-radius: 4px;
|
||||
margin-right: 12px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.logo span {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
.logo-icon img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
@@ -499,12 +501,6 @@ onMounted(() => {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.notification-icon {
|
||||
font-size: 20px;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
<div class="image-to-video-page">
|
||||
<!-- 左侧导航栏 -->
|
||||
<aside class="sidebar">
|
||||
<div class="logo">logo</div>
|
||||
<div class="logo">
|
||||
<img src="/images/backgrounds/logo.svg" alt="Logo" />
|
||||
</div>
|
||||
<nav class="nav-menu">
|
||||
<div class="nav-item" @click="goToProfile">
|
||||
<el-icon><User /></el-icon>
|
||||
@@ -38,7 +40,7 @@
|
||||
<!-- 顶部用户信息卡片 -->
|
||||
<div class="user-info-card">
|
||||
<div class="user-avatar">
|
||||
<div class="avatar-placeholder">||</div>
|
||||
<img src="/images/backgrounds/avatar-default.svg" alt="用户头像" class="avatar-image" />
|
||||
</div>
|
||||
<div class="user-details">
|
||||
<div class="username">mingzi_FBx7foZYDS7inLQb</div>
|
||||
@@ -92,7 +94,7 @@
|
||||
</div>
|
||||
<div class="work-info">
|
||||
<div class="work-title">{{ work.prompt || work.title || '图生视频' }}</div>
|
||||
<div class="work-meta">{{ work.taskId || work.id }} · {{ formatSize(work) }}</div>
|
||||
<div class="work-meta">{{ work.date || '未知日期' }} · {{ work.taskId || work.id }} · {{ formatSize(work) }}</div>
|
||||
</div>
|
||||
<div class="work-actions" v-if="index === 0">
|
||||
<el-button type="primary" class="create-similar-btn" @click.stop="goToCreate(work)">做同款</el-button>
|
||||
@@ -272,7 +274,8 @@ const loadTasks = async () => {
|
||||
title: task.prompt || '图生视频',
|
||||
text: task.prompt || '图生视频',
|
||||
category: '图生视频',
|
||||
createTime: task.createdAt ? new Date(task.createdAt).toLocaleString('zh-CN') : ''
|
||||
createTime: task.createdAt ? new Date(task.createdAt).toLocaleString('zh-CN') : '',
|
||||
date: task.createdAt ? new Date(task.createdAt).toLocaleDateString('zh-CN') : '未知日期'
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -300,7 +303,7 @@ onMounted(() => {
|
||||
/* 左侧导航栏 */
|
||||
.sidebar {
|
||||
width: 280px !important;
|
||||
background: #1a1a1a !important;
|
||||
background: #000000 !important;
|
||||
padding: 24px 0 !important;
|
||||
border-right: 1px solid #1a1a1a !important;
|
||||
flex-shrink: 0 !important;
|
||||
@@ -311,9 +314,14 @@ onMounted(() => {
|
||||
|
||||
.logo {
|
||||
padding: 0 24px 32px;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.logo img {
|
||||
height: 40px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
@@ -409,18 +417,17 @@ onMounted(() => {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
background: #000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 2px solid #333;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
color: #fff;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 2px;
|
||||
.avatar-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.user-details {
|
||||
@@ -575,21 +582,7 @@ onMounted(() => {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.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-overlay / overlay-text 样式已移除(不再使用) */
|
||||
|
||||
.work-info {
|
||||
padding: 16px;
|
||||
|
||||
@@ -8,16 +8,15 @@
|
||||
</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 class="points-display">
|
||||
<div class="points-icon">
|
||||
<el-icon><Star /></el-icon>
|
||||
</div>
|
||||
<span class="points-number">{{ userStore.availablePoints }}</span>
|
||||
</div>
|
||||
<LanguageSwitcher />
|
||||
<div class="user-avatar" @click="toggleUserMenu" ref="userAvatarRef">
|
||||
👤
|
||||
<img src="/images/backgrounds/avatar-default.svg" alt="用户头像" />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -89,7 +88,7 @@
|
||||
<div class="setting-item">
|
||||
<label>高清模式 (1080P)</label>
|
||||
<div class="hd-setting">
|
||||
<input type="checkbox" v-model="hdMode" class="hd-switch">
|
||||
<el-switch v-model="hdMode" />
|
||||
<span class="cost-text">开启消耗20积分</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -330,6 +329,7 @@ import { User, VideoCamera, Star, Setting, SwitchButton } from '@element-plus/ic
|
||||
import { imageToVideoApi } from '@/api/imageToVideo'
|
||||
import { optimizePrompt } from '@/api/promptOptimizer'
|
||||
import { getProcessingWorks } from '@/api/userWorks'
|
||||
import LanguageSwitcher from '@/components/LanguageSwitcher.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
@@ -556,12 +556,20 @@ const startGenerate = async () => {
|
||||
inProgress.value = true
|
||||
taskProgress.value = 0
|
||||
taskStatus.value = 'PENDING'
|
||||
|
||||
|
||||
ElMessage.success('任务创建成功,开始处理...')
|
||||
|
||||
|
||||
// 更新用户积分信息(任务创建后积分已被扣除)
|
||||
try {
|
||||
await userStore.fetchCurrentUser()
|
||||
console.log('用户积分已更新')
|
||||
} catch (error) {
|
||||
console.error('更新用户积分失败:', error)
|
||||
}
|
||||
|
||||
// 开始轮询任务状态
|
||||
startPollingTask()
|
||||
|
||||
|
||||
} else {
|
||||
ElMessage.error(response.data?.message || '创建任务失败')
|
||||
}
|
||||
@@ -598,7 +606,7 @@ const startPollingTask = () => {
|
||||
console.log('任务进度:', progressData)
|
||||
},
|
||||
// 完成回调
|
||||
(taskData) => {
|
||||
async (taskData) => {
|
||||
inProgress.value = false
|
||||
taskProgress.value = 100
|
||||
taskStatus.value = 'COMPLETED'
|
||||
@@ -612,7 +620,15 @@ const startPollingTask = () => {
|
||||
console.warn('任务完成但未获取到resultUrl')
|
||||
}
|
||||
ElMessage.success('视频生成完成!')
|
||||
|
||||
|
||||
// 更新用户积分信息
|
||||
try {
|
||||
await userStore.fetchCurrentUser()
|
||||
console.log('用户积分已更新')
|
||||
} catch (error) {
|
||||
console.error('更新用户积分失败:', error)
|
||||
}
|
||||
|
||||
// 可以在这里跳转到结果页面或显示结果
|
||||
console.log('任务完成:', taskData)
|
||||
},
|
||||
@@ -1071,69 +1087,50 @@ onUnmounted(() => {
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.credits-info {
|
||||
.points-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
gap: 8px;
|
||||
padding: 6px 12px;
|
||||
background: rgba(64, 158, 255, 0.1);
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(64, 158, 255, 0.3);
|
||||
}
|
||||
|
||||
.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%;
|
||||
.points-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: #409EFF;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.4);
|
||||
}
|
||||
|
||||
.points-number {
|
||||
color: #409EFF;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.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;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.user-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.user-avatar:hover {
|
||||
@@ -1445,13 +1442,6 @@ onUnmounted(() => {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.hd-switch {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
accent-color: #3b82f6;
|
||||
}
|
||||
|
||||
.cost-text {
|
||||
font-size: 13px;
|
||||
color: #9ca3af;
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
<div class="video-detail-page">
|
||||
<!-- 顶部导航栏 -->
|
||||
<div class="top-bar">
|
||||
<div class="logo">logo</div>
|
||||
<div class="logo">
|
||||
<img src="/images/backgrounds/logo.png" alt="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>
|
||||
|
||||
@@ -150,8 +151,8 @@ import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { imageToVideoApi } from '@/api/imageToVideo'
|
||||
import {
|
||||
User, Setting, Bell, Document, User as Picture, User as VideoPlay, User as VideoPause,
|
||||
import {
|
||||
User, Setting, Document, User as Picture, User as VideoPlay, User as VideoPause,
|
||||
User as FullScreen, User as Share, User as Download, User as Delete, User as ArrowUp, User as ArrowDown
|
||||
} from '@element-plus/icons-vue'
|
||||
|
||||
@@ -341,9 +342,13 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo img {
|
||||
height: 30px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.top-actions {
|
||||
|
||||
@@ -1,62 +1,75 @@
|
||||
<template>
|
||||
<div class="login-page">
|
||||
<!-- Logo -->
|
||||
<div class="logo">Logo</div>
|
||||
<div class="logo">
|
||||
<img src="/images/backgrounds/logo.svg" alt="Logo" />
|
||||
</div>
|
||||
|
||||
<!-- 登录卡片 -->
|
||||
<div class="login-card">
|
||||
<!-- Logo图标 -->
|
||||
<div class="card-logo">
|
||||
<div class="logo-icon">Logo</div>
|
||||
<svg width="306" height="37" viewBox="0 0 306 37" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19.266 9.22263e-06L23.218 0.64601C22.952 2.54601 22.648 4.33201 22.268 5.96601H34.276V8.58801C33.668 11.818 32.946 15.048 32.034 18.24L28.158 17.138C28.918 15.048 29.564 12.616 30.096 9.80401H21.242C20.292 12.616 19.152 14.896 17.784 16.644L14.592 14.402C17.024 10.982 18.582 6.15601 19.266 9.22263e-06ZM13.11 29.792C11.97 28.044 10.83 26.334 9.65201 24.662C7.52401 28.766 4.97801 32.034 1.97601 34.466L7.80571e-06 30.894C3.00201 28.69 5.43401 25.536 7.29601 21.432C5.16801 18.658 2.96401 15.96 0.608008 13.3L3.23001 10.792C4.94001 12.502 6.84001 14.668 8.89201 17.252C9.72801 14.516 10.412 11.552 10.944 8.32201H0.684008V4.48401H14.82V8.13201C13.984 12.768 12.844 16.91 11.476 20.558C12.92 22.496 14.402 24.586 15.998 26.866L13.11 29.792ZM22.762 11.894H26.676C26.676 14.516 26.524 16.91 26.22 19.114C28.044 24.738 31.122 29.26 35.53 32.642L32.87 35.758C29.45 32.756 26.828 29.146 24.928 24.928C23.56 29.298 20.406 32.908 15.504 35.758L12.996 32.68C17.176 30.172 19.912 27.17 21.204 23.636C22.192 20.596 22.724 16.682 22.762 11.894ZM41.762 0.83601C44.308 2.81201 46.474 4.82601 48.26 6.84001L45.524 9.57601C44.004 7.67601 41.876 5.58601 39.102 3.34401L41.762 0.83601ZM64.486 35.036C62.32 35.036 59.964 34.998 57.418 34.96C54.834 34.922 52.706 34.694 51.072 34.276C49.438 33.82 48.07 32.946 46.93 31.654C46.398 30.97 45.866 30.666 45.41 30.666C44.65 30.666 43.282 32.452 41.306 36.062L38.494 33.402C40.394 30.286 42.104 28.272 43.7 27.436V16.91H38.57V13.338H47.31V27.702C47.462 27.854 47.652 28.006 47.842 28.234C48.792 29.26 49.704 30.02 50.616 30.514C51.794 31.046 53.542 31.388 55.898 31.464C58.178 31.502 60.876 31.54 63.992 31.54C65.74 31.54 67.488 31.502 69.312 31.464C71.136 31.388 72.504 31.35 73.492 31.274L72.58 35.036H64.486ZM50.198 27.74L49.134 24.244C49.818 23.94 50.198 23.294 50.198 22.344V3.91401C53.466 2.88801 56.05 1.67201 58.026 0.30401L60.23 3.49601C58.634 4.56001 56.544 5.58601 53.884 6.53601V23.066C55.632 22.572 57.304 22.04 58.976 21.47L59.698 25.194C56.962 26.03 53.808 26.866 50.198 27.74ZM68.59 26.448H65.778L64.714 22.686L67.336 22.876C68.02 22.876 68.362 22.458 68.362 21.66V6.00401H64.334V30.514H60.534V2.50801H72.086V22.458C72.086 25.118 70.908 26.448 68.59 26.448ZM84.93 9.15801C86.07 11.02 87.21 13.11 88.312 15.39L85.614 16.758H91.694V8.43601H78.47V4.59801H91.694V0.64601H95.722V4.59801H109.288V8.43601H95.722V16.758H98.762C100.054 14.288 101.156 11.666 102.106 8.89201L105.982 10.26C104.994 12.654 103.892 14.82 102.676 16.758H110.352V20.596H98.762C101.422 24.434 105.716 27.892 111.644 30.932L108.908 34.352C102.752 30.324 98.344 25.84 95.722 20.938V35.644H91.694V20.9C89.224 26.144 84.664 30.628 77.976 34.39L75.772 30.818C82.004 27.74 86.298 24.32 88.578 20.596H76.988V16.758H84.778C83.714 14.516 82.612 12.54 81.434 10.83L84.93 9.15801ZM134.33 28.158V32.11C128.402 33.326 122.056 34.39 115.292 35.226L114.342 31.426C117.268 31.122 120.156 30.78 122.968 30.362V24.434H116.09V20.634H122.968V16.796H126.958V20.634H133.228V24.434H126.958V29.678C129.466 29.222 131.898 28.69 134.33 28.158ZM114.988 1.78601H134.178V5.51001H124.716C123.196 8.58801 121.828 10.982 120.612 12.654C123.348 12.35 126.084 12.008 128.82 11.552C128.136 10.488 127.414 9.38601 126.654 8.28401L129.694 6.34601C132.202 9.65201 134.102 12.54 135.432 14.972L132.126 17.252C131.67 16.34 131.176 15.428 130.644 14.516C125.894 15.352 120.726 16.036 115.178 16.606L114.304 13.072C114.988 12.996 115.482 12.844 115.862 12.692C116.926 11.818 118.484 9.42401 120.46 5.51001H114.988V1.78601ZM144.02 35.34H138.358L137.484 31.502C139.308 31.654 141.018 31.73 142.652 31.73C143.526 31.73 143.982 31.236 143.982 30.324V0.64601H148.01V31.35C148.01 34.01 146.68 35.34 144.02 35.34ZM136.686 4.06601H140.524V27.284H136.686V4.06601Z" fill="white"/>
|
||||
<path d="M163.552 4.59801H168.378L175.826 26.714H175.94L183.388 4.59801H188.214L178.562 31.73H173.204L163.552 4.59801ZM193.617 4.06601C194.453 4.06601 195.175 4.33201 195.745 4.86401C196.277 5.39601 196.581 6.08001 196.581 6.91601C196.581 7.75201 196.277 8.47401 195.707 9.00601C195.137 9.53801 194.453 9.80401 193.617 9.80401C192.781 9.80401 192.097 9.53801 191.527 9.00601C190.957 8.43601 190.691 7.75201 190.691 6.91601C190.691 6.08001 190.957 5.39601 191.527 4.86401C192.097 4.33201 192.781 4.06601 193.617 4.06601ZM191.451 12.084H195.783V31.73H191.451V12.084ZM215.14 4.06601H219.472V31.73H215.444V29.64C214.076 31.388 212.1 32.262 209.516 32.262C206.59 32.262 204.31 31.236 202.676 29.184C201.156 27.284 200.396 24.814 200.396 21.812C200.396 18.924 201.118 16.53 202.638 14.63C204.234 12.578 206.476 11.552 209.288 11.552C211.568 11.552 213.506 12.616 215.14 14.782V4.06601ZM210.314 15.048C208.338 15.048 206.932 15.694 206.02 16.986C205.222 18.088 204.842 19.684 204.842 21.812C204.842 23.94 205.184 25.574 205.944 26.714C206.818 28.082 208.224 28.766 210.162 28.766C211.834 28.766 213.164 28.082 214.076 26.752C214.874 25.536 215.292 23.94 215.292 22.04V21.736C215.292 19.646 214.76 17.974 213.772 16.758C212.86 15.618 211.682 15.048 210.314 15.048ZM224.395 4.59801H242.901V8.39801H228.841V15.922H242.103V19.722H228.841V31.73H224.395V4.59801ZM246.513 4.06601H250.845V31.73H246.513V4.06601ZM264.901 11.552C267.865 11.552 270.259 12.502 272.083 14.478C273.869 16.416 274.781 18.886 274.781 21.926C274.781 24.928 273.869 27.398 272.121 29.298C270.297 31.274 267.865 32.262 264.901 32.262C261.899 32.262 259.505 31.274 257.681 29.298C255.895 27.398 255.021 24.928 255.021 21.926C255.021 18.886 255.895 16.416 257.719 14.478C259.505 12.502 261.899 11.552 264.901 11.552ZM264.901 15.086C263.077 15.086 261.709 15.77 260.721 17.214C259.885 18.43 259.467 20.026 259.467 21.926C259.467 23.826 259.885 25.384 260.721 26.6C261.709 28.006 263.077 28.728 264.901 28.728C266.687 28.728 268.093 28.006 269.081 26.6C269.917 25.346 270.373 23.788 270.373 21.926C270.373 20.026 269.917 18.43 269.081 17.214C268.093 15.77 266.687 15.086 264.901 15.086ZM276.476 12.084H281.264L285.178 26.6L289.054 12.084H293.044L296.92 26.6L300.834 12.084H305.622L299.01 31.73H294.982L291.068 17.366L287.116 31.73H283.088L276.476 12.084Z" fill="#0DC0FF"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- 欢迎文字 -->
|
||||
<div class="welcome-text">
|
||||
<h1>欢迎来到 Logo</h1>
|
||||
<p>智创无限,灵感变现</p>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 登录标题 -->
|
||||
<div class="login-title">
|
||||
<h2>邮箱验证码登录</h2>
|
||||
<p class="login-subtitle">请输入邮箱地址,获取验证码后登录</p>
|
||||
<div class="login-methods">
|
||||
<button :class="['method-btn', { active: loginType === 'email' }]" @click="() => { loginType = 'email'; clearForm(); }">邮箱验证码登录</button>
|
||||
<button :class="['method-btn', { active: loginType === 'password' }]" @click="() => { loginType = 'password'; clearForm(); }">邮箱密码登录</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 登录表单 -->
|
||||
<div class="login-form">
|
||||
<!-- 邮箱登录 -->
|
||||
<!-- 邮箱登录 / 密码登录 表单 -->
|
||||
<div class="email-login">
|
||||
<!-- 邮箱输入 -->
|
||||
<div class="email-input-group">
|
||||
<el-input
|
||||
ref="emailInput"
|
||||
v-model="loginForm.email"
|
||||
placeholder="请输入邮箱地址"
|
||||
class="email-input"
|
||||
type="email"
|
||||
@keyup.enter="handleLogin"
|
||||
/>
|
||||
<div class="input-error" v-if="errors.email">{{ errors.email }}</div>
|
||||
<!-- 快捷输入标签 -->
|
||||
<div class="quick-email-tags">
|
||||
<span class="email-tag" @click="fillQuickEmail('984523799@qq.com')">984523799@qq.com</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 验证码输入 -->
|
||||
<div class="code-input-group">
|
||||
|
||||
<!-- 验证码输入(仅验证码登录显示) -->
|
||||
<div class="code-input-group" v-if="loginType === 'email'">
|
||||
<el-input
|
||||
ref="codeInput"
|
||||
v-model="loginForm.code"
|
||||
placeholder="请输入验证码"
|
||||
class="code-input"
|
||||
@keyup.enter="handleLogin"
|
||||
/>
|
||||
<div class="input-error" v-if="errors.code">{{ errors.code }}</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
class="get-code-btn"
|
||||
:disabled="countdown > 0"
|
||||
:disabled="countdown > 0 || !isEmailValid"
|
||||
@click="getEmailCode"
|
||||
>
|
||||
{{ countdown > 0 ? `${countdown}s` : '获取验证码' }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 密码输入(仅密码登录显示) -->
|
||||
<div v-if="loginType === 'password'" class="password-input-group">
|
||||
<el-input ref="passwordInput" v-model="loginForm.password" placeholder="请输入密码" show-password @keyup.enter="handleLogin" />
|
||||
<div class="input-error" v-if="errors.password">{{ errors.password }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
@@ -66,7 +79,7 @@
|
||||
:loading="userStore.loading"
|
||||
@click="handleLogin"
|
||||
>
|
||||
{{ userStore.loading ? '登录中...' : '登陆/注册' }}
|
||||
{{ userStore.loading ? '登录中...' : (loginType === 'password' ? '登录' : '登陆/注册') }}
|
||||
</el-button>
|
||||
|
||||
<!-- 协议文字 -->
|
||||
@@ -92,11 +105,11 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ref, reactive, onMounted, computed, nextTick } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { User } from '@element-plus/icons-vue'
|
||||
import { loginWithEmail, login, sendEmailCode, setDevEmailCode } from '@/api/auth'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
@@ -105,30 +118,69 @@ const userStore = useUserStore()
|
||||
const countdown = ref(0)
|
||||
let countdownTimer = null
|
||||
|
||||
const loginType = ref('email') // 只支持邮箱登录
|
||||
const loginType = ref('email') // 'email' or 'password'
|
||||
|
||||
const loginForm = reactive({
|
||||
email: '',
|
||||
code: ''
|
||||
code: '',
|
||||
password: ''
|
||||
})
|
||||
|
||||
// inline errors for fields and server
|
||||
const errors = reactive({
|
||||
email: '',
|
||||
code: '',
|
||||
password: '',
|
||||
server: ''
|
||||
})
|
||||
|
||||
// input refs for focusing
|
||||
const emailInput = ref(null)
|
||||
const codeInput = ref(null)
|
||||
const passwordInput = ref(null)
|
||||
|
||||
const isEmailValid = computed(() => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(loginForm.email))
|
||||
const isCodeValid = computed(() => /^\d{6}$/.test(loginForm.code))
|
||||
const isPasswordValid = computed(() => loginForm.password && loginForm.password.length >= 6)
|
||||
|
||||
const isFormValid = computed(() => {
|
||||
if (loginType.value === 'email') {
|
||||
return isEmailValid.value && isCodeValid.value
|
||||
}
|
||||
return isEmailValid.value && isPasswordValid.value
|
||||
})
|
||||
|
||||
// 清空表单
|
||||
const clearForm = () => {
|
||||
const clearForm = async () => {
|
||||
loginForm.email = ''
|
||||
loginForm.code = ''
|
||||
loginForm.password = ''
|
||||
errors.email = errors.code = errors.password = errors.server = ''
|
||||
// 重置倒计时
|
||||
if (countdownTimer) {
|
||||
clearInterval(countdownTimer)
|
||||
countdownTimer = null
|
||||
}
|
||||
countdown.value = 0
|
||||
// focus email input after clearing
|
||||
await nextTick()
|
||||
emailInput.value && emailInput.value.focus && emailInput.value.focus()
|
||||
}
|
||||
|
||||
|
||||
// 快速填充测试账号
|
||||
const fillTestAccount = (email, code) => {
|
||||
const fillTestAccount = async (email, code) => {
|
||||
loginForm.email = email
|
||||
loginForm.code = code
|
||||
// 根据当前登录方式,将第二个参数作为验证码或密码
|
||||
if (loginType.value === 'password') {
|
||||
loginForm.password = code
|
||||
await nextTick()
|
||||
passwordInput.value && passwordInput.value.focus && passwordInput.value.focus()
|
||||
} else {
|
||||
loginForm.code = code
|
||||
await nextTick()
|
||||
codeInput.value && codeInput.value.focus && codeInput.value.focus()
|
||||
}
|
||||
}
|
||||
|
||||
// 快速填充邮箱(快捷输入)
|
||||
@@ -151,39 +203,33 @@ onMounted(() => {
|
||||
|
||||
// 获取邮箱验证码
|
||||
const getEmailCode = async () => {
|
||||
errors.email = ''
|
||||
errors.code = ''
|
||||
errors.password = ''
|
||||
errors.server = ''
|
||||
|
||||
if (!loginForm.email) {
|
||||
ElMessage.warning('请先输入邮箱地址')
|
||||
errors.email = '请输入邮箱地址'
|
||||
emailInput.value && emailInput.value.focus && emailInput.value.focus()
|
||||
return
|
||||
}
|
||||
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(loginForm.email)) {
|
||||
ElMessage.warning('请输入正确的邮箱地址')
|
||||
if (!isEmailValid.value) {
|
||||
errors.email = '请输入正确的邮箱地址'
|
||||
emailInput.value && emailInput.value.focus && emailInput.value.focus()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 导入 API 工具函数
|
||||
const { buildApiURL } = await import('@/utils/apiHelper')
|
||||
|
||||
// 调用后端API发送邮箱验证码
|
||||
const response = await fetch(buildApiURL('/verification/email/send'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: loginForm.email
|
||||
})
|
||||
})
|
||||
const response = await sendEmailCode(loginForm.email)
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (result.success) {
|
||||
if (response.data && response.data.success) {
|
||||
ElMessage.success('验证码已发送到您的邮箱')
|
||||
// 开始倒计时
|
||||
startCountdown()
|
||||
} else {
|
||||
ElMessage.error(result.message || '发送失败')
|
||||
ElMessage.error(response.data?.message || '发送失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('发送验证码失败:', error)
|
||||
@@ -194,17 +240,7 @@ const getEmailCode = async () => {
|
||||
|
||||
// 开发模式:将验证码同步到后端
|
||||
try {
|
||||
const { buildApiURL } = await import('@/utils/apiHelper')
|
||||
await fetch(buildApiURL('/verification/email/dev-set'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: loginForm.email,
|
||||
code: randomCode
|
||||
})
|
||||
})
|
||||
await setDevEmailCode(loginForm.email, randomCode)
|
||||
} catch (syncError) {
|
||||
console.warn('同步验证码到后端失败:', syncError)
|
||||
}
|
||||
@@ -213,7 +249,7 @@ const getEmailCode = async () => {
|
||||
ElMessage.success(`验证码已发送到您的邮箱`)
|
||||
startCountdown()
|
||||
} else {
|
||||
ElMessage.error('网络错误,请稍后重试')
|
||||
ElMessage.error(error.response?.data?.message || '网络错误,请稍后重试')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -231,7 +267,7 @@ const startCountdown = () => {
|
||||
}
|
||||
|
||||
const handleLogin = async () => {
|
||||
// 验证表单
|
||||
// 基本邮箱校验
|
||||
if (!loginForm.email) {
|
||||
ElMessage.warning('请输入邮箱地址')
|
||||
return
|
||||
@@ -240,77 +276,73 @@ const handleLogin = async () => {
|
||||
ElMessage.warning('请输入正确的邮箱地址')
|
||||
return
|
||||
}
|
||||
|
||||
if (!loginForm.code) {
|
||||
ElMessage.warning('请输入验证码')
|
||||
return
|
||||
}
|
||||
|
||||
// 验证码格式检查(6位数字)
|
||||
if (!/^\d{6}$/.test(loginForm.code)) {
|
||||
ElMessage.warning('验证码格式不正确,请输入6位数字')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
console.log('开始登录...')
|
||||
|
||||
let result
|
||||
|
||||
// 导入 API 工具函数
|
||||
const { buildApiURL } = await import('@/utils/apiHelper')
|
||||
|
||||
// 邮箱验证码登录
|
||||
try {
|
||||
const response = await fetch(buildApiURL('/auth/login/email'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: loginForm.email,
|
||||
code: loginForm.code
|
||||
})
|
||||
})
|
||||
|
||||
const apiResult = await response.json()
|
||||
|
||||
if (apiResult.success) {
|
||||
// 保存用户信息和token
|
||||
sessionStorage.setItem('token', apiResult.data.token)
|
||||
sessionStorage.setItem('user', JSON.stringify(apiResult.data.user))
|
||||
userStore.user = apiResult.data.user
|
||||
userStore.token = apiResult.data.token
|
||||
result = { success: true }
|
||||
} else {
|
||||
result = { success: false, message: apiResult.message }
|
||||
console.log('开始登录... 登录方式:', loginType)
|
||||
|
||||
let response = null
|
||||
|
||||
if (loginType.value === 'email') {
|
||||
// 验证码登录
|
||||
if (!loginForm.code) {
|
||||
errors.code = '请输入验证码'
|
||||
codeInput.value && codeInput.value.focus && codeInput.value.focus()
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('邮箱验证码登录失败:', error)
|
||||
result = { success: false, message: '网络错误,请稍后重试' }
|
||||
if (!isCodeValid.value) {
|
||||
errors.code = '验证码格式不正确,请输入6位数字'
|
||||
codeInput.value && codeInput.value.focus && codeInput.value.focus()
|
||||
return
|
||||
}
|
||||
|
||||
response = await loginWithEmail({ email: loginForm.email, code: loginForm.code })
|
||||
} else {
|
||||
// 密码登录
|
||||
if (!loginForm.password) {
|
||||
errors.password = '请输入密码'
|
||||
passwordInput.value && passwordInput.value.focus && passwordInput.value.focus()
|
||||
return
|
||||
}
|
||||
if (!isPasswordValid.value) {
|
||||
errors.password = '密码至少 6 位'
|
||||
passwordInput.value && passwordInput.value.focus && passwordInput.value.focus()
|
||||
return
|
||||
}
|
||||
|
||||
response = await login({ email: loginForm.email, password: loginForm.password })
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
|
||||
if (response && response.data && response.data.success) {
|
||||
// 保存用户信息和token
|
||||
sessionStorage.setItem('token', response.data.data.token)
|
||||
sessionStorage.setItem('user', JSON.stringify(response.data.data.user))
|
||||
userStore.user = response.data.data.user
|
||||
userStore.token = response.data.data.token
|
||||
|
||||
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 || '登录失败')
|
||||
const msg = response?.data?.message || '登录失败'
|
||||
errors.server = msg
|
||||
ElMessage.error(msg)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Login error:', error)
|
||||
ElMessage.error('登录失败,请重试')
|
||||
const msg = error.response?.data?.message || '登录失败,请重试'
|
||||
errors.server = msg
|
||||
ElMessage.error(msg)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -320,7 +352,7 @@ const handleLogin = async () => {
|
||||
min-height: 100vh;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: url('/images/backgrounds/login.png') center/cover no-repeat;
|
||||
background: url('/images/backgrounds/login-bg.svg') center/cover no-repeat;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@@ -336,67 +368,58 @@ const handleLogin = async () => {
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
left: 30px;
|
||||
color: white;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.logo img {
|
||||
height: 40px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/* 登录卡片 */
|
||||
.login-card {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 10%;
|
||||
right: 145px;
|
||||
transform: translateY(-50%);
|
||||
width: 800px;
|
||||
max-width: 90vw;
|
||||
background: rgba(100, 150, 200, 0.3);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 24px;
|
||||
background: rgba(121, 121, 121, 0.1);
|
||||
backdrop-filter: blur(50px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(50px) saturate(180%);
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow:
|
||||
0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
box-shadow:
|
||||
0 8px 32px rgba(0, 0, 0, 0.5),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.05) inset;
|
||||
padding: 50px;
|
||||
z-index: 10;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.15), transparent);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* 卡片内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;
|
||||
margin-bottom: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto;
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 欢迎文字 */
|
||||
.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;
|
||||
.card-logo svg {
|
||||
width: auto;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
/* 登录表单 */
|
||||
@@ -410,14 +433,7 @@ const handleLogin = async () => {
|
||||
/* 登录标题 */
|
||||
.login-title {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.login-title h2 {
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 8px 0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
@@ -427,6 +443,38 @@ const handleLogin = async () => {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 登录方式切换 */
|
||||
.login-methods {
|
||||
display: inline-flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.method-btn {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.method-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-color: rgba(255, 255, 255, 0.12);
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
.method-btn.active {
|
||||
background: rgba(64, 158, 255, 0.15);
|
||||
border-color: rgba(64, 158, 255, 0.3);
|
||||
color: #66B1FF;
|
||||
}
|
||||
.password-input-group {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
|
||||
/* 邮箱输入组 */
|
||||
.email-input-group {
|
||||
@@ -446,32 +494,44 @@ const handleLogin = async () => {
|
||||
}
|
||||
|
||||
.email-tag {
|
||||
background: rgba(64, 158, 255, 0.15);
|
||||
border: 1px solid rgba(64, 158, 255, 0.3);
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
background: rgba(64, 158, 255, 0.1);
|
||||
border: 1px solid rgba(64, 158, 255, 0.25);
|
||||
color: #66B1FF;
|
||||
padding: 6px 14px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
user-select: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.email-tag:hover {
|
||||
background: rgba(64, 158, 255, 0.25);
|
||||
border-color: rgba(64, 158, 255, 0.5);
|
||||
color: white;
|
||||
background: rgba(64, 158, 255, 0.2);
|
||||
border-color: rgba(64, 158, 255, 0.4);
|
||||
color: #409EFF;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.15);
|
||||
}
|
||||
|
||||
.email-input :deep(.el-input__wrapper) {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 10px;
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.05),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
height: 55px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.email-input :deep(.el-input__wrapper:hover) {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.email-input :deep(.el-input__wrapper.is-focus) {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-color: rgba(64, 158, 255, 0.4);
|
||||
box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.1);
|
||||
}
|
||||
|
||||
.email-input :deep(.el-input__inner) {
|
||||
@@ -495,13 +555,23 @@ const handleLogin = async () => {
|
||||
}
|
||||
|
||||
.code-input :deep(.el-input__wrapper) {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 10px;
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.05),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
height: 55px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.code-input :deep(.el-input__wrapper:hover) {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.code-input :deep(.el-input__wrapper.is-focus) {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-color: rgba(64, 158, 255, 0.4);
|
||||
box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.1);
|
||||
}
|
||||
|
||||
.code-input :deep(.el-input__inner) {
|
||||
@@ -515,19 +585,23 @@ const handleLogin = async () => {
|
||||
}
|
||||
|
||||
.get-code-btn {
|
||||
background: transparent;
|
||||
border: 1px solid #409EFF;
|
||||
color: #409EFF;
|
||||
background: rgba(64, 158, 255, 0.12);
|
||||
border: 1px solid rgba(64, 158, 255, 0.3);
|
||||
color: #66B1FF;
|
||||
border-radius: 10px;
|
||||
padding: 0 20px;
|
||||
font-size: 16px;
|
||||
height: 55px;
|
||||
transition: all 0.3s ease;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.get-code-btn:hover {
|
||||
background: #409EFF;
|
||||
background: rgba(64, 158, 255, 0.85);
|
||||
border-color: rgba(64, 158, 255, 0.8);
|
||||
color: white;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.25);
|
||||
}
|
||||
|
||||
.get-code-btn:disabled {
|
||||
@@ -535,23 +609,32 @@ const handleLogin = async () => {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.input-error {
|
||||
color: #ff7875;
|
||||
font-size: 12px;
|
||||
margin-top: 6px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* 登录按钮 */
|
||||
.login-button {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
background: #409EFF;
|
||||
height: 52px;
|
||||
background: linear-gradient(135deg, #409EFF 0%, #66B1FF 100%);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
border-radius: 10px;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
margin-top: 15px;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 16px rgba(64, 158, 255, 0.25);
|
||||
}
|
||||
|
||||
.login-button:hover {
|
||||
background: #337ecc;
|
||||
background: linear-gradient(135deg, #66B1FF 0%, #409EFF 100%);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(64, 158, 255, 0.35);
|
||||
}
|
||||
|
||||
.login-button:active {
|
||||
@@ -586,19 +669,20 @@ const handleLogin = async () => {
|
||||
|
||||
.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.05);
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
padding: 8px 14px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.account-item:hover {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-color: rgba(255, 255, 255, 0.12);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.account-item strong {
|
||||
|
||||
@@ -3,41 +3,56 @@
|
||||
<!-- 左侧导航栏 -->
|
||||
<aside class="sidebar">
|
||||
<div class="logo">
|
||||
<div class="logo-icon"></div>
|
||||
<span>LOGO</span>
|
||||
<div class="logo-icon">
|
||||
<svg viewBox="0 0 194 41" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M54.6165 13.6062L60.593 32.3932H60.8224L66.8111 13.6062H72.6065L64.0824 38.3335H57.3452L48.8089 13.6062H54.6165ZM75.4862 38.3335V19.788H80.6296V38.3335H75.4862ZM78.07 17.3974C77.3053 17.3974 76.6493 17.1439 76.1019 16.6368C75.5626 16.1216 75.293 15.5058 75.293 14.7895C75.293 14.0811 75.5626 13.4734 76.1019 12.9663C76.6493 12.4512 77.3053 12.1936 78.07 12.1936C78.8346 12.1936 79.4866 12.4512 80.0259 12.9663C80.5733 13.4734 80.8469 14.0811 80.8469 14.7895C80.8469 15.5058 80.5733 16.1216 80.0259 16.6368C79.4866 17.1439 78.8346 17.3974 78.07 17.3974ZM91.5836 38.6353C90.175 38.6353 88.8992 38.2731 87.7562 37.5487C86.6213 36.8162 85.7198 35.7416 85.0517 34.325C84.3916 32.9003 84.0616 31.1536 84.0616 29.0849C84.0616 26.9599 84.4037 25.1931 85.0879 23.7845C85.7721 22.3678 86.6816 21.3093 87.8166 20.6091C88.9596 19.9007 90.2112 19.5466 91.5716 19.5466C92.6099 19.5466 93.4752 19.7236 94.1674 20.0778C94.8677 20.4239 95.4312 20.8586 95.8578 21.3818C96.2924 21.8969 96.6225 22.404 96.8478 22.9031H97.0048V13.6062H102.136V38.3335H97.0652V35.3633H96.8478C96.6064 35.8785 96.2643 36.3896 95.8216 36.8967C95.3869 37.3958 94.8194 37.8103 94.1191 38.1403C93.4269 38.4703 92.5817 38.6353 91.5836 38.6353ZM93.2136 34.5423C94.0427 34.5423 94.743 34.3169 95.3145 33.8662C95.894 33.4074 96.3367 32.7674 96.6426 31.9464C96.9565 31.1254 97.1135 30.1635 97.1135 29.0608C97.1135 27.958 96.9605 27.0002 96.6547 26.1872C96.3488 25.3742 95.9061 24.7464 95.3265 24.3037C94.747 23.861 94.0427 23.6396 93.2136 23.6396C92.3684 23.6396 91.6561 23.869 91.0765 24.3278C90.497 24.7866 90.0583 25.4225 89.7605 26.2355C89.4627 27.0485 89.3137 27.9902 89.3137 29.0608C89.3137 30.1394 89.4627 31.0932 89.7605 31.9223C90.0663 32.7433 90.505 33.3872 91.0765 33.8541C91.6561 34.3129 92.3684 34.5423 93.2136 34.5423ZM106.462 38.3335V13.6062H122.834V17.9166H111.69V23.8086H121.747V28.119H111.69V38.3335H106.462ZM131.397 13.6062V38.3335H126.254V13.6062H131.397ZM143.897 38.6957C142.021 38.6957 140.399 38.2973 139.031 37.5004C137.671 36.6955 136.62 35.5766 135.88 34.1439C135.139 32.7031 134.769 31.0328 134.769 29.1332C134.769 27.2175 135.139 25.5432 135.88 24.1105C136.62 22.6697 137.671 21.5508 139.031 20.7539C140.399 19.949 142.021 19.5466 143.897 19.5466C145.772 19.5466 147.39 19.949 148.75 20.7539C150.119 21.5508 151.173 22.6697 151.914 24.1105C152.654 25.5432 153.025 27.2175 153.025 29.1332C153.025 31.0328 152.654 32.7031 151.914 34.1439C151.173 35.5766 150.119 36.6955 148.75 37.5004C147.39 38.2973 145.772 38.6957 143.897 38.6957ZM143.921 34.7113C144.774 34.7113 145.486 34.4699 146.058 33.9869C146.629 33.4959 147.06 32.8278 147.35 31.9826C147.648 31.1375 147.797 30.1756 147.797 29.097C147.797 28.0184 147.648 27.0565 147.35 26.2113C147.06 25.3662 146.629 24.6981 146.058 24.2071C145.486 23.7161 144.774 23.4706 143.921 23.4706C143.06 23.4706 142.335 23.7161 141.748 24.2071C141.168 24.6981 140.729 25.3662 140.431 26.2113C140.142 27.0565 139.997 28.0184 139.997 29.097C139.997 30.1756 140.142 31.1375 140.431 31.9826C140.729 32.8278 141.168 33.4959 141.748 33.9869C142.335 34.4699 143.06 34.7113 143.921 34.7113ZM159.562 38.3335L154.516 19.788H159.719L162.593 32.2483H162.762L165.756 19.788H170.864L173.906 32.1758H174.063L176.888 19.788H182.08L177.045 38.3335H171.6L168.413 26.6701H168.183L164.996 38.3335H159.562Z" fill="white"/>
|
||||
<g clip-path="url(#clip0)">
|
||||
<path d="M5.7406 1.64568C2.43935 1.64568 0.00938034 1.57298 0.000366208 1.64568C0.000366202 1.71906 2.11292 5.37389 4.70642 9.58611C7.28533 13.813 12.3557 22.0905 15.9545 27.9611L20.8685 35.9875C21.8796 37.6388 23.6773 38.6457 25.6136 38.6457L26.4301 38.6457C26.4301 38.6457 31.0568 38.7204 31.9965 37.2228C32.2255 36.8288 32.346 36.3807 32.3461 35.925L32.3461 21.8996L31.1801 21.8996L31.1801 26.5969C31.1801 31.1005 31.1655 31.3074 30.889 31.5861C30.7288 31.7476 30.4663 31.8801 30.306 31.8801C29.9855 31.8801 29.7811 31.5866 27.1439 27.257C26.2551 25.8039 24.0844 22.2371 22.2924 19.3312C20.5148 16.4253 17.3534 11.2596 15.2699 7.85467L11.4672 1.64568L5.7406 1.64568ZM31.6615 2.99627C31.443 4.1703 30.525 6.06354 29.6654 7.10564C28.4561 8.60263 26.4304 9.83531 24.5072 10.2316C23.4727 10.4518 23.196 10.5988 23.808 10.5988C24.0267 10.5989 24.5801 10.7012 25.0316 10.8332C28.2662 11.7578 31.0495 14.7674 31.6468 17.9816L31.8363 18.9797L32.0111 18.1135C32.5648 15.3249 34.4443 12.8151 36.9066 11.5676C38.0139 10.9952 39.2815 10.5988 39.9808 10.5988C40.6217 10.5988 40.403 10.4957 39.2084 10.2463C37.46 9.87934 36.1049 9.11623 34.6625 7.66326C33.2638 6.26901 32.5496 5.02127 32.0834 3.17205L31.8217 2.11541L31.6615 2.99627Z" fill="url(#paint0)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0" x1="23.4862" y1="5.3947" x2="32.9093" y2="11.1248" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.0001" stop-color="#33DDE5"/>
|
||||
<stop offset="1" stop-color="#0F9CFF"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0">
|
||||
<rect width="40.3334" height="40.3334" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="nav-menu">
|
||||
<div class="nav-item" @click="goToDashboard">
|
||||
<el-icon><Grid /></el-icon>
|
||||
<span>数据仪表台</span>
|
||||
<span>{{ $t('nav.dashboard') }}</span>
|
||||
</div>
|
||||
<div class="nav-item active">
|
||||
<el-icon><User /></el-icon>
|
||||
<span>会员管理</span>
|
||||
<span>{{ $t('nav.members') }}</span>
|
||||
</div>
|
||||
<div class="nav-item" @click="goToOrders">
|
||||
<el-icon><ShoppingCart /></el-icon>
|
||||
<span>订单管理</span>
|
||||
<span>{{ $t('nav.orders') }}</span>
|
||||
</div>
|
||||
<div class="nav-item" @click="goToAPI">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>API管理</span>
|
||||
<span>{{ $t('nav.apiManagement') }}</span>
|
||||
</div>
|
||||
<div class="nav-item" @click="goToTasks">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>生成任务记录</span>
|
||||
<span>{{ $t('nav.tasks') }}</span>
|
||||
</div>
|
||||
<div class="nav-item" @click="goToSettings">
|
||||
<el-icon><Setting /></el-icon>
|
||||
<span>系统设置</span>
|
||||
<span>{{ $t('nav.systemSettings') }}</span>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<div class="online-users">
|
||||
当前在线用户: <span class="highlight">87/500</span>
|
||||
{{ $t('nav.onlineUsers') }}: <span class="highlight">{{ onlineUsers }}</span>
|
||||
</div>
|
||||
<div class="system-uptime">
|
||||
系统运行时间: <span class="highlight">48小时32分</span>
|
||||
{{ $t('nav.systemUptime') }}: <span class="highlight">{{ systemUptime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -48,15 +63,12 @@
|
||||
<header class="top-header">
|
||||
<div class="search-bar">
|
||||
<el-icon class="search-icon"><Search /></el-icon>
|
||||
<input type="text" placeholder="搜索你想要的内容" class="search-input" />
|
||||
<input type="text" :placeholder="$t('common.searchPlaceholder')" class="search-input" />
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<div class="notification-icon-wrapper">
|
||||
<el-icon class="notification-icon"><Bell /></el-icon>
|
||||
<span class="notification-badge"></span>
|
||||
</div>
|
||||
<LanguageSwitcher />
|
||||
<div class="user-avatar">
|
||||
<img src="/images/backgrounds/welcome.jpg" alt="用户头像" />
|
||||
<img src="/images/backgrounds/avatar-default.svg" alt="用户头像" />
|
||||
<el-icon class="arrow-down"><ArrowDown /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
@@ -65,24 +77,24 @@
|
||||
<!-- 会员列表内容 -->
|
||||
<section class="member-content">
|
||||
<div class="content-header">
|
||||
<h2>会员列表</h2>
|
||||
<h2>{{ $t('members.title') }}</h2>
|
||||
<div class="selection-info" v-if="selectedMembers.length > 0">
|
||||
已选择{{ selectedMembers.length }}项
|
||||
{{ $t('orders.selected', { count: selectedMembers.length }) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-toolbar">
|
||||
<div class="toolbar-left">
|
||||
<el-select v-model="selectedLevel" placeholder="全部等级" size="small" @change="handleLevelChange">
|
||||
<el-option label="全部等级" value="all" />
|
||||
<el-option label="专业会员" value="professional" />
|
||||
<el-option label="标准会员" value="standard" />
|
||||
<el-select v-model="selectedLevel" :placeholder="$t('members.allLevels')" size="small" @change="handleLevelChange">
|
||||
<el-option :label="$t('members.allLevels')" value="all" />
|
||||
<el-option :label="$t('members.professional')" value="professional" />
|
||||
<el-option :label="$t('members.standard')" value="standard" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<el-button type="danger" size="small" @click="deleteSelected" :disabled="selectedMembers.length === 0">
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
{{ $t('common.delete') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -94,19 +106,19 @@
|
||||
<th class="checkbox-col">
|
||||
<input type="checkbox" @change="toggleAllSelection" :checked="isAllSelected" />
|
||||
</th>
|
||||
<th>用户ID</th>
|
||||
<th>用户名</th>
|
||||
<th>会员等级</th>
|
||||
<th>剩余资源点</th>
|
||||
<th>到期时间</th>
|
||||
<th>编辑</th>
|
||||
<th>{{ $t('members.userId') }}</th>
|
||||
<th>{{ $t('members.username') }}</th>
|
||||
<th>{{ $t('members.level') }}</th>
|
||||
<th>{{ $t('members.points') }}</th>
|
||||
<th>{{ $t('members.expiryDate') }}</th>
|
||||
<th>{{ $t('members.operation') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="member in memberList" :key="member.id" class="table-row">
|
||||
<td class="checkbox-col">
|
||||
<input
|
||||
type="checkbox"
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="selectedMembers.some(m => m.id === member.id)"
|
||||
@change="toggleMemberSelection(member)" />
|
||||
</td>
|
||||
@@ -120,8 +132,8 @@
|
||||
<td>{{ member.points.toLocaleString() }}</td>
|
||||
<td>{{ member.expiryDate }}</td>
|
||||
<td>
|
||||
<el-link type="primary" class="action-link" @click="editMember(member)">编辑</el-link>
|
||||
<el-link type="danger" class="action-link" @click="deleteMember(member)">删除</el-link>
|
||||
<el-link type="primary" class="action-link" @click="editMember(member)">{{ $t('common.edit') }}</el-link>
|
||||
<el-link type="danger" class="action-link" @click="deleteMember(member)">{{ $t('common.delete') }}</el-link>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -215,13 +227,13 @@ import {
|
||||
Document,
|
||||
Setting,
|
||||
Search,
|
||||
Bell,
|
||||
ArrowDown,
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
Delete
|
||||
} from '@element-plus/icons-vue'
|
||||
import * as memberAPI from '@/api/members'
|
||||
import LanguageSwitcher from '@/components/LanguageSwitcher.vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -232,6 +244,10 @@ const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const totalMembers = ref(50)
|
||||
|
||||
// 系统状态数据
|
||||
const onlineUsers = ref('0/500')
|
||||
const systemUptime = ref('加载中...')
|
||||
|
||||
// 编辑相关状态
|
||||
const editDialogVisible = ref(false)
|
||||
const editFormRef = ref()
|
||||
@@ -538,7 +554,27 @@ const getMembershipExpiry = (membership) => {
|
||||
onMounted(() => {
|
||||
// 初始化数据
|
||||
loadMembers()
|
||||
fetchSystemStats()
|
||||
})
|
||||
|
||||
// 获取系统统计数据
|
||||
const fetchSystemStats = async () => {
|
||||
try {
|
||||
// 临时使用计算值,后续可以从API获取
|
||||
// 计算在线用户数(这里简化处理)
|
||||
const randomOnline = Math.floor(Math.random() * 50) + 10
|
||||
onlineUsers.value = `${randomOnline}/500`
|
||||
|
||||
// 计算系统运行时间(基于当前时间简单模拟)
|
||||
const hours = new Date().getHours()
|
||||
const minutes = new Date().getMinutes()
|
||||
systemUptime.value = `${hours}小时${minutes}分`
|
||||
} catch (error) {
|
||||
console.error('获取系统统计失败:', error)
|
||||
onlineUsers.value = '0/500'
|
||||
systemUptime.value = '未知'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -563,22 +599,23 @@ onMounted(() => {
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
padding: 0 50px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: #3b82f6;
|
||||
border-radius: 4px;
|
||||
margin-right: 12px;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.logo span {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
.logo-icon svg, .logo-icon img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
@@ -698,34 +735,6 @@ onMounted(() => {
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.notification-icon-wrapper {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.notification-icon-wrapper:hover {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.notification-icon {
|
||||
font-size: 20px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.notification-badge {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #ef4444;
|
||||
border-radius: 50%;
|
||||
border: 2px solid white;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
<!-- 左侧导航栏 -->
|
||||
<aside class="sidebar">
|
||||
<!-- Logo -->
|
||||
<div class="logo">logo</div>
|
||||
<div class="logo">
|
||||
<img src="/images/backgrounds/logo.svg" alt="Logo" />
|
||||
</div>
|
||||
|
||||
<!-- 导航菜单 -->
|
||||
<nav class="nav-menu">
|
||||
@@ -49,16 +51,15 @@
|
||||
<!-- 顶部栏 -->
|
||||
<header class="top-header">
|
||||
<div class="header-right">
|
||||
<div class="discount-badge">
|
||||
<span class="discount-icon">+ 25</span>
|
||||
<span class="discount-text">首购优惠</span>
|
||||
</div>
|
||||
<div class="notification-bell">
|
||||
<el-icon><Bell /></el-icon>
|
||||
<span class="notification-badge">5</span>
|
||||
<div class="points">
|
||||
<div class="points-icon">
|
||||
<el-icon><Star /></el-icon>
|
||||
</div>
|
||||
<span class="points-number">{{ userStore.availablePoints }}</span>
|
||||
</div>
|
||||
<LanguageSwitcher />
|
||||
<div class="user-avatar">
|
||||
<el-icon><User /></el-icon>
|
||||
<img src="/images/backgrounds/avatar-default.svg" alt="用户头像" />
|
||||
</div>
|
||||
<div class="settings-icon">
|
||||
<el-icon><Setting /></el-icon>
|
||||
@@ -67,7 +68,7 @@
|
||||
</header>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<div class="content-area">
|
||||
<div class="content-area" @scroll="handleScroll">
|
||||
<div class="toolbar">
|
||||
<el-radio-group v-model="activeTab" size="small" class="seg-control">
|
||||
<el-radio-button label="all">全部</el-radio-button>
|
||||
@@ -126,8 +127,8 @@
|
||||
</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-row :gutter="16" class="works-grid" justify="start">
|
||||
<el-col v-for="item in filteredItems" :key="item.id" :xs="24" :sm="12" :md="8" :lg="6" :xl="4">
|
||||
<el-card class="work-card" :class="{ selected: selectedIds.has(item.id) }" shadow="hover">
|
||||
<div class="thumb" @click="multiSelect ? toggleSelect(item.id) : openDetail(item)">
|
||||
<!-- 如果是视频类型且有视频URL,使用video元素显示首帧 -->
|
||||
@@ -138,12 +139,14 @@
|
||||
muted
|
||||
preload="metadata"
|
||||
@loadedmetadata="onVideoLoaded"
|
||||
@error="onVideoError"
|
||||
></video>
|
||||
<!-- 如果有封面图(thumbnailUrl),使用图片 -->
|
||||
<img
|
||||
v-else-if="item.cover && item.cover !== item.resultUrl"
|
||||
:src="item.cover"
|
||||
:alt="item.title"
|
||||
:alt="item.title"
|
||||
@error="onImageError"
|
||||
/>
|
||||
<!-- 否则使用默认占位符 -->
|
||||
<div v-else class="work-placeholder">
|
||||
@@ -180,7 +183,13 @@
|
||||
</div>
|
||||
<div class="meta">
|
||||
<div class="title" :title="item.title">{{ item.title }}</div>
|
||||
<div class="sub">{{ item.id }} · {{ item.sizeText }}</div>
|
||||
<div class="sub">
|
||||
{{ item.date || '未知日期' }} · {{ item.id }}
|
||||
<span v-if="item.quality" class="quality-badge" :class="`quality-${(item.quality || '').toLowerCase()}`">
|
||||
{{ formatQuality(item.quality) }}
|
||||
</span>
|
||||
· {{ item.sizeText }}
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-space size="small">
|
||||
@@ -222,10 +231,7 @@
|
||||
:alt="selectedItem.title"
|
||||
/>
|
||||
|
||||
<!-- 视频文字叠加 -->
|
||||
<div class="video-overlay" v-if="selectedItem.type === 'video' && selectedItem.overlayText">
|
||||
<div class="overlay-text">{{ selectedItem.overlayText }}</div>
|
||||
</div>
|
||||
<!-- 视频文字叠加 已移除(用户要求) -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -234,9 +240,9 @@
|
||||
<div class="detail-header">
|
||||
<div class="user-info">
|
||||
<div class="avatar">
|
||||
<el-icon><User /></el-icon>
|
||||
<img src="/images/backgrounds/avatar-default.svg" alt="用户头像" class="avatar-image" />
|
||||
</div>
|
||||
<div class="username">mingzi_FBx7foZYDS7inL</div>
|
||||
<div class="username">{{ (selectedItem && selectedItem.username) || '匿名用户' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -246,9 +252,9 @@
|
||||
<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>
|
||||
<h3 class="section-title">提示词</h3>
|
||||
<p class="description-text">{{ getDescription(selectedItem) }}</p>
|
||||
</div>
|
||||
|
||||
@@ -267,14 +273,14 @@
|
||||
</div>
|
||||
|
||||
<div class="description-section">
|
||||
<h3 class="section-title">描述</h3>
|
||||
<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>
|
||||
<h3 class="section-title">提示词</h3>
|
||||
<p class="description-text">{{ getDescription(selectedItem) }}</p>
|
||||
</div>
|
||||
|
||||
@@ -294,11 +300,11 @@
|
||||
</div>
|
||||
<div class="metadata-item" v-if="selectedItem.type === 'video'">
|
||||
<span class="label">时长</span>
|
||||
<span class="value">5s</span>
|
||||
<span class="value">{{ formatDuration(selectedItem.duration) || '未知' }}</span>
|
||||
</div>
|
||||
<div class="metadata-item" v-if="selectedItem.type === 'video'">
|
||||
<span class="label">清晰度</span>
|
||||
<span class="value">1080p</span>
|
||||
<span class="value">{{ selectedItem.quality || '未知' }}</span>
|
||||
</div>
|
||||
<div class="metadata-item">
|
||||
<span class="label">分类</span>
|
||||
@@ -306,7 +312,7 @@
|
||||
</div>
|
||||
<div class="metadata-item" v-if="selectedItem.type === 'video'">
|
||||
<span class="label">宽高比</span>
|
||||
<span class="value">16:9</span>
|
||||
<span class="value">{{ selectedItem.aspectRatio || '未知' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -320,8 +326,21 @@
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<div class="finished" v-if="!hasMore && filteredItems.length>0">已加载全部内容</div>
|
||||
<div class="loading-indicator" v-if="loading">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
<div class="finished" v-if="!hasMore && filteredItems.length>0">
|
||||
<span>✓ 已加载全部内容</span>
|
||||
</div>
|
||||
<el-empty v-if="!loading && filteredItems.length===0" description="没有找到相关内容" />
|
||||
|
||||
<!-- 回到顶部按钮 -->
|
||||
<transition name="fade">
|
||||
<div v-show="showBackToTop" class="back-to-top" @click="scrollToTop" title="回到顶部">
|
||||
<el-icon><ArrowUp /></el-icon>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
@@ -329,13 +348,28 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { ref, onMounted, onActivated, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Star, User, Compass, Document, VideoPlay, Picture, Film, Bell, Setting, Search, MoreFilled } from '@element-plus/icons-vue'
|
||||
import { getMyWorks } from '@/api/userWorks'
|
||||
import { Star, User, Compass, Document, VideoPlay, Picture, Film, Setting, Search, MoreFilled, Loading, ArrowUp } from '@element-plus/icons-vue'
|
||||
import { getMyWorks, getWorkDetail } from '@/api/userWorks'
|
||||
import { getCurrentUser } from '@/api/auth'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import LanguageSwitcher from '@/components/LanguageSwitcher.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 用户信息
|
||||
const userInfo = ref({
|
||||
username: '',
|
||||
nickname: '',
|
||||
bio: '',
|
||||
avatar: '',
|
||||
id: '',
|
||||
points: 0,
|
||||
frozenPoints: 0
|
||||
})
|
||||
|
||||
const activeTab = ref('all')
|
||||
const dateRange = ref([])
|
||||
@@ -354,30 +388,50 @@ const selectedItem = ref(null)
|
||||
const activeDetailTab = ref('detail')
|
||||
|
||||
const page = ref(1)
|
||||
const pageSize = ref(4)
|
||||
const pageSize = ref(20)
|
||||
const loading = ref(false)
|
||||
const hasMore = ref(true)
|
||||
const items = ref([])
|
||||
const showBackToTop = ref(false) // 回到顶部按钮显示状态
|
||||
|
||||
// 处理URL,确保相对路径正确
|
||||
const processUrl = (url) => {
|
||||
if (!url) return null
|
||||
// 如果已经是完整URL(http/https),直接返回
|
||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
return url
|
||||
}
|
||||
// 如果是相对路径(以/开头),确保以/开头
|
||||
if (url.startsWith('/')) {
|
||||
return url
|
||||
}
|
||||
// 否则添加/前缀
|
||||
return '/' + url
|
||||
}
|
||||
|
||||
// 将后端返回的UserWork数据转换为前端需要的格式
|
||||
const transformWorkData = (work) => {
|
||||
const resultUrl = processUrl(work.resultUrl)
|
||||
const thumbnailUrl = processUrl(work.thumbnailUrl)
|
||||
|
||||
return {
|
||||
id: work.id?.toString() || work.taskId || '',
|
||||
title: work.title || work.prompt || '未命名作品',
|
||||
cover: work.thumbnailUrl || work.resultUrl || '/images/backgrounds/welcome.jpg',
|
||||
resultUrl: work.resultUrl || '',
|
||||
type: work.workType === 'TEXT_TO_VIDEO' || work.workType === 'IMAGE_TO_VIDEO' ? 'video' : 'image',
|
||||
category: work.workType === 'TEXT_TO_VIDEO' ? '文生视频' : work.workType === 'IMAGE_TO_VIDEO' ? '图生视频' : '未知',
|
||||
cover: thumbnailUrl || resultUrl || '/images/backgrounds/welcome.jpg',
|
||||
resultUrl: resultUrl || '',
|
||||
type: work.workType === 'TEXT_TO_VIDEO' || work.workType === 'IMAGE_TO_VIDEO' || work.workType === 'STORYBOARD_VIDEO' ? 'video' : 'image',
|
||||
category: work.workType === 'TEXT_TO_VIDEO' ? '文生视频' : work.workType === 'IMAGE_TO_VIDEO' ? '图生视频' : work.workType === 'STORYBOARD_VIDEO' ? '分镜视频' : work.workType === 'STORYBOARD_IMAGE' ? '分镜图' : '未知',
|
||||
sizeText: work.fileSize || '未知大小',
|
||||
createTime: work.createdAt ? new Date(work.createdAt).toLocaleString('zh-CN') : '',
|
||||
date: work.createdAt ? new Date(work.createdAt).toLocaleDateString('zh-CN') : '',
|
||||
description: work.description || work.prompt || '',
|
||||
prompt: work.prompt || '',
|
||||
duration: work.duration || '',
|
||||
aspectRatio: work.aspectRatio || '',
|
||||
quality: work.quality || '',
|
||||
duration: work.duration || work.videoDuration || work.length || '',
|
||||
aspectRatio: work.aspectRatio || work.ratio || work.aspect || '',
|
||||
quality: work.quality || work.resolution || '',
|
||||
username: work.username || work.user?.username || work.creator || work.author || work.owner || '未知用户',
|
||||
status: work.status || 'COMPLETED',
|
||||
overlayText: work.prompt || ''
|
||||
// overlayText 已移除,前端详情不再显示浮动文本
|
||||
}
|
||||
}
|
||||
|
||||
@@ -412,19 +466,60 @@ const loadList = async () => {
|
||||
// 筛选后的作品列表
|
||||
const filteredItems = computed(() => {
|
||||
let filtered = [...items.value]
|
||||
|
||||
|
||||
// 按日期筛选
|
||||
if (dateFilter.value) {
|
||||
const now = new Date()
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
||||
|
||||
filtered = filtered.filter(item => {
|
||||
if (!item.createdAt && !item.date) return false
|
||||
|
||||
// 获取作品创建日期
|
||||
let itemDate
|
||||
if (item.createdAt) {
|
||||
itemDate = new Date(item.createdAt)
|
||||
} else if (item.date) {
|
||||
// 如果只有 date 字符串,尝试解析
|
||||
itemDate = new Date(item.date)
|
||||
}
|
||||
|
||||
if (!itemDate || isNaN(itemDate.getTime())) return false
|
||||
|
||||
// 重置时间为当天开始
|
||||
const itemDay = new Date(itemDate.getFullYear(), itemDate.getMonth(), itemDate.getDate())
|
||||
|
||||
if (dateFilter.value === 'today') {
|
||||
// 今天:日期相同
|
||||
return itemDay.getTime() === today.getTime()
|
||||
} else if (dateFilter.value === 'week') {
|
||||
// 本周:过去7天内
|
||||
const weekAgo = new Date(today)
|
||||
weekAgo.setDate(weekAgo.getDate() - 7)
|
||||
return itemDay >= weekAgo && itemDay <= today
|
||||
} else if (dateFilter.value === 'month') {
|
||||
// 本月:过去30天内
|
||||
const monthAgo = new Date(today)
|
||||
monthAgo.setDate(monthAgo.getDate() - 30)
|
||||
return itemDay >= monthAgo && itemDay <= today
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// 按类型筛选(全部/视频/图片)
|
||||
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': '图生视频',
|
||||
'image2video': '图生视频',
|
||||
'storyboard': '分镜视频',
|
||||
'reference': '参考图'
|
||||
}
|
||||
@@ -433,16 +528,34 @@ const filteredItems = computed(() => {
|
||||
filtered = filtered.filter(item => item.category === targetCategory)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 按清晰度筛选
|
||||
if (resolution.value) {
|
||||
filtered = filtered.filter(item => {
|
||||
const itemQuality = (item.quality || '').toLowerCase()
|
||||
const filterValue = resolution.value.toLowerCase()
|
||||
|
||||
// 映射关系:sd=标清, hd=高清, uhd=超清
|
||||
if (filterValue === 'sd') {
|
||||
return itemQuality === 'sd' || itemQuality.includes('标清')
|
||||
} else if (filterValue === 'hd') {
|
||||
return itemQuality === 'hd' || itemQuality.includes('高清')
|
||||
} else if (filterValue === 'uhd') {
|
||||
return itemQuality === 'uhd' || itemQuality.includes('超清') || itemQuality.includes('4k')
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
// 按关键词筛选
|
||||
if (keyword.value) {
|
||||
const keywordLower = keyword.value.toLowerCase()
|
||||
filtered = filtered.filter(item =>
|
||||
filtered = filtered.filter(item =>
|
||||
item.title.toLowerCase().includes(keywordLower) ||
|
||||
item.id.includes(keywordLower)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
return filtered
|
||||
})
|
||||
|
||||
@@ -464,18 +577,85 @@ const loadMore = () => {
|
||||
loadList()
|
||||
}
|
||||
|
||||
const openDetail = (item) => {
|
||||
selectedItem.value = item
|
||||
// 滚动监听,触底自动加载更多,控制回到顶部按钮显示
|
||||
const handleScroll = (event) => {
|
||||
const target = event.target
|
||||
const scrollTop = target.scrollTop
|
||||
const scrollHeight = target.scrollHeight
|
||||
const clientHeight = target.clientHeight
|
||||
|
||||
// 控制回到顶部按钮显示(滚动超过300px时显示)
|
||||
showBackToTop.value = scrollTop > 300
|
||||
|
||||
// 当滚动到距离底部100px时,自动加载更多
|
||||
if (scrollHeight - scrollTop - clientHeight < 100) {
|
||||
loadMore()
|
||||
}
|
||||
}
|
||||
|
||||
// 滚动到顶部
|
||||
const scrollToTop = () => {
|
||||
const contentArea = document.querySelector('.content-area')
|
||||
if (contentArea) {
|
||||
contentArea.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const openDetail = async (item) => {
|
||||
// 优先从后端拉取最新的详情数据,降级为传入的 item
|
||||
try {
|
||||
const resp = await getWorkDetail(item.id)
|
||||
const payload = resp?.data?.data || resp?.data || null
|
||||
if (payload) {
|
||||
selectedItem.value = transformWorkData(payload)
|
||||
} else {
|
||||
selectedItem.value = item
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('获取作品详情失败,使用已有数据:', err)
|
||||
selectedItem.value = item
|
||||
}
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 获取作品描述
|
||||
// 获取作品提示词(优先使用 prompt,其次使用后台 description,最后回退默认文案)
|
||||
const getDescription = (item) => {
|
||||
if (!item) return ''
|
||||
const desc = (item.prompt && item.prompt.trim()) ? item.prompt : (item.description && item.description.trim() ? item.description : '')
|
||||
if (desc) return desc
|
||||
// 回退文案
|
||||
if (item.type === 'video') {
|
||||
return '影片捕捉了暴风雪中的午夜时分,坐落在积雪覆盖的悬崖顶上的孤立灯塔。相机逐渐放大灯塔的灯光,穿透飞舞的雪花,投射出幽幽的光芒。在白茫茫的环境中,灯塔的黑色轮廓显得格外醒目,呼啸的风声和远处海浪的撞击声增强了孤独的氛围。这一场景展示了灯塔的孤独力量。'
|
||||
} else {
|
||||
return '这是一张精美的参考图片,展现了独特的艺术风格和创意构思。图片构图优美,色彩搭配和谐,具有很高的艺术价值和参考意义。'
|
||||
return '暂无提示词'
|
||||
}
|
||||
return '暂无提示词'
|
||||
}
|
||||
|
||||
// 格式化清晰度显示
|
||||
const formatQuality = (quality) => {
|
||||
if (!quality) return ''
|
||||
const q = quality.toUpperCase()
|
||||
const qualityMap = {
|
||||
'SD': '标清',
|
||||
'HD': '高清',
|
||||
'UHD': '超清',
|
||||
'4K': '超清'
|
||||
}
|
||||
return qualityMap[q] || q
|
||||
}
|
||||
|
||||
// 格式化时长(支持数字秒或字符串),返回类似 "5s" 或原始字符串
|
||||
const formatDuration = (dur) => {
|
||||
if (dur === null || dur === undefined || dur === '') return ''
|
||||
if (typeof dur === 'number') return `${dur}s`
|
||||
if (typeof dur === 'string') {
|
||||
const trimmed = dur.trim()
|
||||
if (/^\d+$/.test(trimmed)) return `${trimmed}s`
|
||||
return trimmed
|
||||
}
|
||||
return String(dur)
|
||||
}
|
||||
|
||||
// 关闭模态框
|
||||
@@ -599,7 +779,85 @@ const onVideoLoaded = (event) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 视频加载失败处理
|
||||
const onVideoError = (event) => {
|
||||
console.warn('视频加载失败:', event.target.src)
|
||||
const video = event.target
|
||||
// 隐藏video元素,显示占位符
|
||||
if (video) {
|
||||
video.style.display = 'none'
|
||||
// 创建或显示占位符
|
||||
const placeholder = video.parentElement.querySelector('.work-placeholder')
|
||||
if (!placeholder) {
|
||||
const div = document.createElement('div')
|
||||
div.className = 'work-placeholder'
|
||||
div.innerHTML = '<el-icon><VideoPlay /></el-icon>'
|
||||
video.parentElement.appendChild(div)
|
||||
} else {
|
||||
placeholder.style.display = 'flex'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 图片加载失败处理
|
||||
const onImageError = (event) => {
|
||||
console.warn('图片加载失败:', event.target.src)
|
||||
const img = event.target
|
||||
// 隐藏图片,显示占位符
|
||||
if (img) {
|
||||
img.style.display = 'none'
|
||||
// 创建或显示占位符
|
||||
const placeholder = img.parentElement.querySelector('.work-placeholder')
|
||||
if (!placeholder) {
|
||||
const div = document.createElement('div')
|
||||
div.className = 'work-placeholder'
|
||||
div.innerHTML = '<el-icon><VideoPlay /></el-icon>'
|
||||
img.parentElement.appendChild(div)
|
||||
} else {
|
||||
placeholder.style.display = 'flex'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 加载用户信息
|
||||
const loadUserInfo = async () => {
|
||||
try {
|
||||
const response = await getCurrentUser()
|
||||
console.log('获取用户信息响应:', response)
|
||||
if (response && response.data && response.data.success && response.data.data) {
|
||||
const user = response.data.data
|
||||
console.log('用户数据:', user)
|
||||
userInfo.value = {
|
||||
username: user.username || '',
|
||||
nickname: user.nickname || user.username || '',
|
||||
bio: user.bio || '',
|
||||
avatar: user.avatar || '',
|
||||
id: user.id ? String(user.id) : '',
|
||||
points: user.points || 0,
|
||||
frozenPoints: user.frozenPoints || 0
|
||||
}
|
||||
console.log('设置后的用户信息:', userInfo.value)
|
||||
} else {
|
||||
console.error('获取用户信息失败:', response?.data?.message || '未知错误')
|
||||
ElMessage.error('获取用户信息失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载用户信息失败:', error)
|
||||
ElMessage.error('加载用户信息失败: ' + (error.message || '未知错误'))
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadUserInfo()
|
||||
loadList()
|
||||
})
|
||||
|
||||
// 当页面被激活时(从其他页面返回时)刷新列表
|
||||
onActivated(() => {
|
||||
// 重置分页并重新加载
|
||||
page.value = 1
|
||||
items.value = []
|
||||
loadUserInfo()
|
||||
loadList()
|
||||
})
|
||||
</script>
|
||||
@@ -649,7 +907,7 @@ onMounted(() => {
|
||||
/* 左侧导航栏 */
|
||||
.sidebar {
|
||||
width: 280px !important;
|
||||
background: #1a1a1a !important;
|
||||
background: #000000 !important;
|
||||
padding: 24px 0 !important;
|
||||
border-right: 1px solid #1a1a1a !important;
|
||||
flex-shrink: 0 !important;
|
||||
@@ -660,9 +918,14 @@ onMounted(() => {
|
||||
|
||||
.logo {
|
||||
padding: 0 24px 32px;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.logo img {
|
||||
height: 40px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.nav-menu, .tools-menu {
|
||||
@@ -722,10 +985,13 @@ onMounted(() => {
|
||||
|
||||
/* 顶部导航栏 */
|
||||
.top-header {
|
||||
padding: 20px 30px;
|
||||
height: 80px;
|
||||
padding: 0 30px;
|
||||
border-bottom: 1px solid #333;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
background: #0a0a0a;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
@@ -734,51 +1000,60 @@ onMounted(() => {
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.discount-badge {
|
||||
.points {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
gap: 8px;
|
||||
padding: 6px 12px;
|
||||
background: rgba(64, 158, 255, 0.1);
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
border: 1px solid rgba(64, 158, 255, 0.3);
|
||||
}
|
||||
|
||||
.discount-icon {
|
||||
background: #1e40af;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.notification-bell {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
color: #9ca3af;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.notification-badge {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
background: #ef4444;
|
||||
.points-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: #409EFF;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 10px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
min-width: 16px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.user-avatar, .settings-icon {
|
||||
.points-number {
|
||||
color: #409EFF;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
cursor: pointer;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.user-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.user-avatar:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.settings-icon {
|
||||
cursor: pointer;
|
||||
color: #9ca3af;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.user-avatar:hover, .settings-icon:hover, .notification-bell:hover {
|
||||
.settings-icon:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
@@ -787,6 +1062,32 @@ onMounted(() => {
|
||||
flex: 1;
|
||||
padding: 20px 24px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
scroll-behavior: smooth; /* 平滑滚动 */
|
||||
}
|
||||
|
||||
/* 自定义滚动条样式 - 更明显美观的滚动条 */
|
||||
.content-area::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
.content-area::-webkit-scrollbar-track {
|
||||
background: rgba(26, 26, 26, 0.5);
|
||||
border-radius: 6px;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.content-area::-webkit-scrollbar-thumb {
|
||||
background: linear-gradient(180deg, #3b82f6 0%, #2563eb 100%);
|
||||
border-radius: 6px;
|
||||
transition: all 0.3s ease;
|
||||
border: 2px solid rgba(26, 26, 26, 0.5);
|
||||
}
|
||||
|
||||
.content-area::-webkit-scrollbar-thumb:hover {
|
||||
background: linear-gradient(180deg, #2563eb 0%, #1d4ed8 100%);
|
||||
border-color: rgba(26, 26, 26, 0.3);
|
||||
box-shadow: 0 0 10px rgba(59, 130, 246, 0.5);
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
@@ -849,9 +1150,22 @@ onMounted(() => {
|
||||
: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; }
|
||||
.works-grid {
|
||||
margin-top: 12px;
|
||||
}
|
||||
.work-card {
|
||||
margin-bottom: 14px;
|
||||
width: 100%;
|
||||
}
|
||||
.thumb {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-top: 100%;
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
.thumb img { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; }
|
||||
.work-thumbnail-video {
|
||||
position: absolute;
|
||||
@@ -916,8 +1230,58 @@ onMounted(() => {
|
||||
}
|
||||
.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; }
|
||||
.sub {
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* 清晰度标签样式 */
|
||||
.quality-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.quality-sd {
|
||||
background: rgba(103, 194, 58, 0.15);
|
||||
color: #67c23a;
|
||||
border: 1px solid rgba(103, 194, 58, 0.3);
|
||||
}
|
||||
|
||||
.quality-hd {
|
||||
background: rgba(64, 158, 255, 0.15);
|
||||
color: #409eff;
|
||||
border: 1px solid rgba(64, 158, 255, 0.3);
|
||||
}
|
||||
|
||||
.quality-uhd, .quality-4k {
|
||||
background: rgba(230, 162, 60, 0.15);
|
||||
color: #e6a23c;
|
||||
border: 1px solid rgba(230, 162, 60, 0.3);
|
||||
}
|
||||
|
||||
.loading-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 20px 0;
|
||||
margin: 20px 0;
|
||||
color: #409EFF;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.loading-indicator .el-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
/* 让卡片与页面背景一致 */
|
||||
:deep(.work-card.el-card) {
|
||||
@@ -1005,20 +1369,7 @@ onMounted(() => {
|
||||
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;
|
||||
}
|
||||
/* overlay 样式已移除(不再使用) */
|
||||
|
||||
.detail-right {
|
||||
flex: 1;
|
||||
@@ -1045,13 +1396,17 @@ onMounted(() => {
|
||||
.avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: #409eff;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.avatar-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.username {
|
||||
@@ -1209,6 +1564,63 @@ onMounted(() => {
|
||||
:deep(.el-overlay) {
|
||||
background-color: rgba(0, 0, 0, 0.8) !important;
|
||||
}
|
||||
|
||||
/* 回到顶部按钮样式 */
|
||||
.back-to-top {
|
||||
position: fixed;
|
||||
bottom: 40px;
|
||||
right: 40px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
|
||||
transition: all 0.3s ease;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.back-to-top:hover {
|
||||
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 6px 20px rgba(59, 130, 246, 0.6);
|
||||
}
|
||||
|
||||
.back-to-top:active {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.back-to-top .el-icon {
|
||||
font-size: 24px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 过渡动画 */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
/* 优化加载完成提示样式 */
|
||||
.finished {
|
||||
text-align: center;
|
||||
color: #409eff;
|
||||
margin: 20px 0;
|
||||
font-size: 14px;
|
||||
padding: 12px;
|
||||
background: rgba(64, 158, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(64, 158, 255, 0.2);
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
<!-- 左侧导航栏 -->
|
||||
<aside class="sidebar">
|
||||
<div class="logo">
|
||||
<div class="logo-icon"></div>
|
||||
<span>LOGO</span>
|
||||
<div class="logo-icon">
|
||||
<img src="/images/backgrounds/logo.svg" alt="Logo" />
|
||||
</div>
|
||||
</div>
|
||||
<nav class="nav-menu">
|
||||
<div class="nav-item" @click="goToDashboard">
|
||||
@@ -51,10 +52,9 @@
|
||||
<input type="text" placeholder="搜索你的想要的内容" class="search-input" />
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-icon class="notification-icon"><Bell /></el-icon>
|
||||
<el-icon class="message-icon"><ChatDotSquare /></el-icon>
|
||||
<LanguageSwitcher />
|
||||
<div class="user-avatar">
|
||||
<img src="/images/backgrounds/welcome.jpg" alt="用户头像" />
|
||||
<img src="/images/backgrounds/avatar-default.svg" alt="用户头像" />
|
||||
<el-icon class="dropdown-icon"><ArrowDown /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
@@ -200,7 +200,6 @@ import {
|
||||
Document,
|
||||
Setting,
|
||||
User as Search,
|
||||
Bell,
|
||||
User as ArrowDown,
|
||||
User as ArrowLeft,
|
||||
User as ArrowRight,
|
||||
@@ -211,6 +210,7 @@ import {
|
||||
Money
|
||||
} from '@element-plus/icons-vue'
|
||||
import * as orderAPI from '@/api/orders'
|
||||
import LanguageSwitcher from '@/components/LanguageSwitcher.vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -505,17 +505,18 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 8px;
|
||||
margin-right: 12px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.logo span {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
.logo-icon img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
@@ -625,19 +626,6 @@ onMounted(() => {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.notification-icon,
|
||||
.message-icon {
|
||||
font-size: 20px;
|
||||
color: #6b7280;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.notification-icon:hover,
|
||||
.message-icon:hover {
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -231,7 +231,6 @@ import {
|
||||
User as Download,
|
||||
User as Upload,
|
||||
Setting,
|
||||
Bell,
|
||||
Check,
|
||||
Close,
|
||||
User as Warning
|
||||
|
||||
@@ -52,15 +52,14 @@
|
||||
<header class="top-header">
|
||||
<div class="header-right">
|
||||
<div class="points">
|
||||
<el-icon><Star /></el-icon>
|
||||
<span>{{ userInfo.points - (userInfo.frozenPoints || 0) }} | 首购优惠</span>
|
||||
</div>
|
||||
<div class="notifications">
|
||||
<el-icon><Bell /></el-icon>
|
||||
<div class="notification-dot"></div>
|
||||
<div class="points-icon">
|
||||
<el-icon><Star /></el-icon>
|
||||
</div>
|
||||
<span class="points-number">{{ userStore.availablePoints }}</span>
|
||||
</div>
|
||||
<LanguageSwitcher />
|
||||
<div class="user-status" @click="showUserMenu = !showUserMenu" ref="userStatusRef">
|
||||
<div class="status-icon"></div>
|
||||
<img src="/images/backgrounds/avatar-default.svg" alt="用户头像" class="status-icon" />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -155,7 +154,7 @@
|
||||
<div class="detail-header">
|
||||
<div class="user-info">
|
||||
<div class="avatar">
|
||||
<el-icon><User /></el-icon>
|
||||
<img src="/images/backgrounds/avatar-default.svg" alt="用户头像" class="avatar-image" />
|
||||
</div>
|
||||
<div class="username">{{ (selectedItem && selectedItem.username) || '匿名用户' }}</div>
|
||||
</div>
|
||||
@@ -273,11 +272,10 @@ import { ref, onMounted, onUnmounted, computed, nextTick } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
User,
|
||||
Document,
|
||||
Star,
|
||||
Bell,
|
||||
import {
|
||||
User,
|
||||
Document,
|
||||
Star,
|
||||
Setting,
|
||||
Compass,
|
||||
VideoPlay,
|
||||
@@ -285,6 +283,7 @@ import {
|
||||
Film
|
||||
} from '@element-plus/icons-vue'
|
||||
import { getMyWorks } from '@/api/userWorks'
|
||||
import LanguageSwitcher from '@/components/LanguageSwitcher.vue'
|
||||
import { getCurrentUser } from '@/api/auth'
|
||||
import { getWorkDetail } from '@/api/userWorks'
|
||||
|
||||
@@ -499,7 +498,7 @@ const transformWorkData = (work) => {
|
||||
cover: thumbnailUrl || resultUrl || '/images/backgrounds/welcome.jpg',
|
||||
resultUrl: resultUrl || '',
|
||||
type: work.workType === 'TEXT_TO_VIDEO' || work.workType === 'IMAGE_TO_VIDEO' || work.workType === 'STORYBOARD_VIDEO' ? 'video' : 'image',
|
||||
category: work.workType === 'TEXT_TO_VIDEO' ? '文生视频' : work.workType === 'IMAGE_TO_VIDEO' ? '图生视频' : work.workType === 'STORYBOARD_VIDEO' ? '分镜视频' : '未知',
|
||||
category: work.workType === 'TEXT_TO_VIDEO' ? '文生视频' : work.workType === 'IMAGE_TO_VIDEO' ? '图生视频' : work.workType === 'STORYBOARD_VIDEO' ? '分镜视频' : work.workType === 'STORYBOARD_IMAGE' ? '分镜图' : '未知',
|
||||
sizeText: work.fileSize || '未知大小',
|
||||
createTime: work.createdAt ? new Date(work.createdAt).toLocaleString('zh-CN') : '',
|
||||
date: work.createdAt ? new Date(work.createdAt).toLocaleDateString('zh-CN') : '',
|
||||
@@ -733,12 +732,15 @@ onUnmounted(() => {
|
||||
|
||||
/* 顶部栏 */
|
||||
.top-header {
|
||||
padding: 20px 30px;
|
||||
height: 80px;
|
||||
padding: 0 30px;
|
||||
border-bottom: 1px solid #333;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
position: relative;
|
||||
z-index: 99999;
|
||||
background: #0a0a0a;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
@@ -751,30 +753,33 @@ onUnmounted(() => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 12px;
|
||||
background: rgba(64, 158, 255, 0.1);
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(64, 158, 255, 0.3);
|
||||
}
|
||||
|
||||
.points-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: #409EFF;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.points-number {
|
||||
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%;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.user-status {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 2px solid #409EFF;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -783,18 +788,18 @@ onUnmounted(() => {
|
||||
position: relative;
|
||||
z-index: 100000;
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.user-status:hover {
|
||||
border-color: #66b1ff;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: white;
|
||||
border-radius: 2px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* 用户菜单样式 */
|
||||
@@ -1203,13 +1208,17 @@ onUnmounted(() => {
|
||||
.detail-right .avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: #409eff;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.detail-right .avatar .avatar-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.detail-right .username {
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
<template>
|
||||
<div class="simple-test">
|
||||
<h1>简单测试页面</h1>
|
||||
<p>如果你能看到这个页面,说明Vue能正常工作</p>
|
||||
<button @click="testClick">测试按钮</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const testClick = () => {
|
||||
alert('按钮点击成功!')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.simple-test {
|
||||
padding: 50px;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
}
|
||||
</style>
|
||||
@@ -2,7 +2,9 @@
|
||||
<div class="storyboard-video-page">
|
||||
<!-- 左侧导航栏 -->
|
||||
<aside class="sidebar">
|
||||
<div class="logo">logo</div>
|
||||
<div class="logo">
|
||||
<img src="/images/backgrounds/logo.svg" alt="Logo" />
|
||||
</div>
|
||||
<nav class="nav-menu">
|
||||
<div class="nav-item" @click="goToProfile">
|
||||
<el-icon><User /></el-icon>
|
||||
@@ -38,7 +40,7 @@
|
||||
<!-- 顶部用户信息卡片 -->
|
||||
<div class="user-info-card">
|
||||
<div class="user-avatar">
|
||||
<div class="avatar-placeholder">👁️👃</div>
|
||||
<img src="/images/backgrounds/avatar-default.svg" alt="用户头像" class="avatar-image" />
|
||||
</div>
|
||||
<div class="user-details">
|
||||
<div class="username">mingzi_FBx7foZYDS7inLQb</div>
|
||||
@@ -73,7 +75,7 @@
|
||||
</div>
|
||||
<div class="work-info">
|
||||
<div class="work-title">{{ work.title }}</div>
|
||||
<div class="work-meta">{{ work.id }} · {{ work.size }}</div>
|
||||
<div class="work-meta">{{ work.date || '未知日期' }} · {{ 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>
|
||||
@@ -165,25 +167,28 @@ const publishedWorks = ref([
|
||||
text: 'What Does it Mean To You',
|
||||
size: '9 MB',
|
||||
category: '分镜视频',
|
||||
createTime: '2025/01/15 14:30'
|
||||
createTime: '2025/01/15 14:30',
|
||||
date: '2025/1/15'
|
||||
},
|
||||
{
|
||||
id: '2995000000002',
|
||||
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'
|
||||
createTime: '2025/01/14 16:45',
|
||||
date: '2025/1/14'
|
||||
},
|
||||
{
|
||||
id: '2995000000003',
|
||||
title: '分镜视频作品 #3',
|
||||
title: '分镜视频作品 #3',
|
||||
cover: '/images/backgrounds/welcome.jpg',
|
||||
text: 'What Does it Mean To You',
|
||||
size: '9 MB',
|
||||
category: '分镜视频',
|
||||
createTime: '2025/01/13 09:20'
|
||||
createTime: '2025/01/13 09:20',
|
||||
date: '2025/1/13'
|
||||
}
|
||||
])
|
||||
|
||||
@@ -257,7 +262,7 @@ onMounted(() => {
|
||||
/* 左侧导航栏 */
|
||||
.sidebar {
|
||||
width: 280px !important;
|
||||
background: #1a1a1a !important;
|
||||
background: #000000 !important;
|
||||
padding: 24px 0 !important;
|
||||
border-right: 1px solid #1a1a1a !important;
|
||||
flex-shrink: 0 !important;
|
||||
@@ -268,9 +273,14 @@ onMounted(() => {
|
||||
|
||||
.logo {
|
||||
padding: 0 24px 32px;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.logo img {
|
||||
height: 40px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
@@ -366,17 +376,17 @@ onMounted(() => {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
background: #000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 2px solid #333;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
color: #fff;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
.avatar-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.user-details {
|
||||
|
||||
@@ -8,16 +8,15 @@
|
||||
</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 class="points-display">
|
||||
<div class="points-icon">
|
||||
<el-icon><Star /></el-icon>
|
||||
</div>
|
||||
<span class="points-number">{{ userStore.availablePoints }}</span>
|
||||
</div>
|
||||
<LanguageSwitcher />
|
||||
<div class="user-avatar">
|
||||
👤
|
||||
<img src="/images/backgrounds/avatar-default.svg" alt="用户头像" />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -126,7 +125,7 @@
|
||||
<div class="setting-item">
|
||||
<label>高清模式 (1080P)</label>
|
||||
<div class="hd-setting">
|
||||
<input type="checkbox" v-model="hdMode" class="hd-switch">
|
||||
<el-switch v-model="hdMode" />
|
||||
<span class="cost-text">开启消耗20积分</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -269,6 +268,7 @@ import { createStoryboardTask, getStoryboardTask, getUserStoryboardTasks } from
|
||||
import { imageToVideoApi } from '@/api/imageToVideo'
|
||||
import { optimizePrompt } from '@/api/promptOptimizer'
|
||||
import { getProcessingWorks } from '@/api/userWorks'
|
||||
import LanguageSwitcher from '@/components/LanguageSwitcher.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
@@ -316,7 +316,7 @@ const videoRefs = ref({}) // 视频元素引用
|
||||
|
||||
// 导航函数
|
||||
const goBack = () => {
|
||||
router.back()
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
// 跳转到登录页面
|
||||
@@ -615,7 +615,15 @@ const startGenerate = async () => {
|
||||
ElMessage.success('分镜图任务创建成功!')
|
||||
taskId.value = response.data.data.taskId
|
||||
console.log('Task created:', response.data.data)
|
||||
|
||||
|
||||
// 更新用户积分信息(任务创建后积分已被扣除)
|
||||
try {
|
||||
await userStore.fetchCurrentUser()
|
||||
console.log('用户积分已更新')
|
||||
} catch (error) {
|
||||
console.error('更新用户积分失败:', error)
|
||||
}
|
||||
|
||||
// 开始轮询任务状态,获取生成的图片
|
||||
// inProgress 将在轮询完成时设置为 false
|
||||
pollTaskStatus(response.data.data.taskId)
|
||||
@@ -954,7 +962,15 @@ const startVideoGenerate = async () => {
|
||||
videoProgress.value = 0
|
||||
ElMessage.success('视频任务创建成功,开始处理...')
|
||||
console.log('视频任务创建成功,任务ID:', newVideoTaskId)
|
||||
|
||||
|
||||
// 更新用户积分信息(任务创建后积分已被扣除)
|
||||
try {
|
||||
await userStore.fetchCurrentUser()
|
||||
console.log('用户积分已更新')
|
||||
} catch (error) {
|
||||
console.error('更新用户积分失败:', error)
|
||||
}
|
||||
|
||||
// 保持在当前页面,开始轮询视频任务状态
|
||||
pollVideoTaskStatus(newVideoTaskId)
|
||||
} else {
|
||||
@@ -1026,6 +1042,15 @@ const pollVideoTaskStatus = async (taskId) => {
|
||||
inProgress.value = false
|
||||
videoProgress.value = 100
|
||||
ElMessage.success('视频生成完成!')
|
||||
|
||||
// 更新用户积分信息
|
||||
try {
|
||||
await userStore.fetchCurrentUser()
|
||||
console.log('用户积分已更新')
|
||||
} catch (error) {
|
||||
console.error('更新用户积分失败:', error)
|
||||
}
|
||||
|
||||
return
|
||||
} else if (task.status === 'FAILED' || task.status === 'CANCELLED') {
|
||||
if (videoPollIntervalId.value) {
|
||||
@@ -1238,37 +1263,86 @@ const restoreProcessingTask = async () => {
|
||||
// 取最新的一个任务
|
||||
const work = storyboardWorks[0]
|
||||
|
||||
// 恢复任务状态
|
||||
currentTask.value = {
|
||||
taskId: work.taskId,
|
||||
prompt: work.prompt,
|
||||
aspectRatio: work.aspectRatio,
|
||||
duration: work.duration,
|
||||
resultUrl: work.resultUrl,
|
||||
createdAt: work.createdAt
|
||||
console.log('恢复分镜视频任务:', work)
|
||||
|
||||
// 获取任务详情以获取完整信息(包括 progress 和 resultUrl)
|
||||
try {
|
||||
const taskResponse = await getStoryboardTask(work.taskId)
|
||||
if (taskResponse.data && taskResponse.data.success && taskResponse.data.data) {
|
||||
const taskDetail = taskResponse.data.data
|
||||
|
||||
console.log('任务详情:', taskDetail)
|
||||
|
||||
// 恢复任务状态
|
||||
currentTask.value = {
|
||||
taskId: taskDetail.taskId,
|
||||
prompt: taskDetail.prompt,
|
||||
aspectRatio: taskDetail.aspectRatio,
|
||||
duration: taskDetail.duration,
|
||||
resultUrl: taskDetail.resultUrl,
|
||||
createdAt: taskDetail.createdAt,
|
||||
progress: taskDetail.progress || 0
|
||||
}
|
||||
|
||||
taskId.value = taskDetail.taskId
|
||||
|
||||
// 恢复输入参数
|
||||
if (taskDetail.prompt) {
|
||||
inputText.value = taskDetail.prompt
|
||||
}
|
||||
if (taskDetail.aspectRatio) {
|
||||
aspectRatio.value = taskDetail.aspectRatio
|
||||
}
|
||||
if (taskDetail.duration) {
|
||||
duration.value = taskDetail.duration || '10'
|
||||
}
|
||||
|
||||
// 判断任务进度,决定恢复到哪个步骤
|
||||
const taskProgress = taskDetail.progress || 0
|
||||
const taskResultUrl = taskDetail.resultUrl || ''
|
||||
|
||||
// 如果有 resultUrl 且是图片(Base64),说明分镜图已生成
|
||||
if (taskResultUrl && taskResultUrl.startsWith('data:image')) {
|
||||
console.log('分镜图已生成,恢复到视频生成步骤')
|
||||
generatedImageUrl.value = taskResultUrl
|
||||
currentStep.value = 'video' // 切换到视频生成步骤
|
||||
|
||||
// 如果进度 >= 100,说明视频也已完成
|
||||
if (taskProgress >= 100) {
|
||||
inProgress.value = false
|
||||
taskStatus.value = 'COMPLETED'
|
||||
ElMessage.success('任务已完成!')
|
||||
} else {
|
||||
// 分镜图已完成,但视频还在生成中
|
||||
inProgress.value = true
|
||||
taskStatus.value = taskDetail.status || 'PROCESSING'
|
||||
ElMessage.info('检测到未完成的视频生成任务,继续处理中...')
|
||||
// 开始轮询任务状态
|
||||
pollTaskStatus(taskDetail.taskId)
|
||||
}
|
||||
} else {
|
||||
// 分镜图还在生成中
|
||||
console.log('分镜图生成中,恢复到分镜图生成步骤')
|
||||
currentStep.value = 'generate' // 保持在分镜图生成步骤
|
||||
inProgress.value = true
|
||||
taskStatus.value = taskDetail.status || 'PROCESSING'
|
||||
ElMessage.info('检测到未完成的分镜图生成任务,继续处理中...')
|
||||
// 开始轮询任务状态
|
||||
pollTaskStatus(taskDetail.taskId)
|
||||
}
|
||||
} else {
|
||||
console.error('获取任务详情失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取任务详情失败:', error)
|
||||
// 如果获取详情失败,使用 work 的基本信息
|
||||
taskId.value = work.taskId
|
||||
inputText.value = work.prompt || ''
|
||||
inProgress.value = true
|
||||
taskStatus.value = work.status || 'PROCESSING'
|
||||
ElMessage.info('检测到未完成的任务,继续处理中...')
|
||||
pollTaskStatus(work.taskId)
|
||||
}
|
||||
|
||||
taskId.value = work.taskId
|
||||
|
||||
// 恢复输入参数
|
||||
if (work.prompt) {
|
||||
inputText.value = work.prompt
|
||||
}
|
||||
if (work.aspectRatio) {
|
||||
aspectRatio.value = work.aspectRatio
|
||||
}
|
||||
if (work.duration) {
|
||||
duration.value = work.duration || '10'
|
||||
}
|
||||
|
||||
inProgress.value = true
|
||||
taskStatus.value = work.status || 'PROCESSING'
|
||||
|
||||
console.log('恢复正在进行中的任务:', work.taskId, '状态:', work.status)
|
||||
ElMessage.info('检测到未完成的任务,继续处理中...')
|
||||
|
||||
// 开始轮询任务状态
|
||||
pollStoryboardTask(work.taskId)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -1345,69 +1419,50 @@ onBeforeUnmount(() => {
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.credits-info {
|
||||
.points-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
gap: 8px;
|
||||
padding: 6px 12px;
|
||||
background: rgba(64, 158, 255, 0.1);
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(64, 158, 255, 0.3);
|
||||
}
|
||||
|
||||
.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%;
|
||||
.points-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: #409EFF;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.4);
|
||||
}
|
||||
|
||||
.points-number {
|
||||
color: #409EFF;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.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;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.user-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.user-avatar:hover {
|
||||
@@ -1827,13 +1882,6 @@ onBeforeUnmount(() => {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.hd-switch {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
accent-color: #3b82f6;
|
||||
}
|
||||
|
||||
.cost-text {
|
||||
font-size: 13px;
|
||||
color: #9ca3af;
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
<!-- 左侧导航栏 -->
|
||||
<aside class="sidebar">
|
||||
<!-- Logo -->
|
||||
<div class="logo">logo</div>
|
||||
<div class="logo">
|
||||
<img src="/images/backgrounds/logo.svg" alt="Logo" />
|
||||
</div>
|
||||
|
||||
<!-- 导航菜单 -->
|
||||
<nav class="nav-menu">
|
||||
@@ -46,6 +48,22 @@
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<main class="main-content">
|
||||
<!-- 顶部导航栏 -->
|
||||
<header class="top-header">
|
||||
<div class="header-right">
|
||||
<div class="points-display">
|
||||
<div class="points-icon">
|
||||
<el-icon><Star /></el-icon>
|
||||
</div>
|
||||
<span class="points-number">{{ userStore.availablePoints }}</span>
|
||||
</div>
|
||||
<LanguageSwitcher />
|
||||
<div class="user-avatar">
|
||||
<img src="/images/backgrounds/avatar-default.svg" alt="用户头像" />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<template v-if="currentSection === 'works'">
|
||||
<MyWorks />
|
||||
</template>
|
||||
@@ -56,10 +74,7 @@
|
||||
<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>
|
||||
<img src="/images/backgrounds/avatar-default.svg" alt="用户头像" class="avatar-image" />
|
||||
</div>
|
||||
<div class="user-meta">
|
||||
<div class="username">{{ userInfo.username || '加载中...' }}</div>
|
||||
@@ -71,7 +86,7 @@
|
||||
<div class="star-icon">
|
||||
<el-icon><Star /></el-icon>
|
||||
</div>
|
||||
<span>{{ userInfo.points || 0 }}</span>
|
||||
<span>{{ userStore.availablePoints }}</span>
|
||||
</div>
|
||||
<button class="mini-btn" @click="goToOrderDetails">积分详情</button>
|
||||
<button class="mini-btn" @click="goToWorks">我的订单</button>
|
||||
@@ -95,7 +110,7 @@
|
||||
<div class="star-icon">
|
||||
<el-icon><Star /></el-icon>
|
||||
</div>
|
||||
<span class="points-number">{{ userInfo.points || 0 }}</span>
|
||||
<span class="points-number">{{ userStore.availablePoints }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -112,6 +127,7 @@
|
||||
<h4 class="package-title">免费版</h4>
|
||||
</div>
|
||||
<div class="package-price">${{ membershipPrices.free }}/月</div>
|
||||
<div class="points-box points-box-placeholder"> </div>
|
||||
<button class="package-button current">当前套餐</button>
|
||||
<div class="package-features">
|
||||
<div class="feature-item">
|
||||
@@ -205,7 +221,7 @@
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">当前积分:</span>
|
||||
<span class="stat-value current">{{ userInfo.points || 0 }}</span>
|
||||
<span class="stat-value current">{{ (userInfo.points || 0) - (userInfo.frozenPoints || 0) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -253,22 +269,12 @@ import MyWorks from '@/views/MyWorks.vue'
|
||||
import PaymentModal from '@/components/PaymentModal.vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { User, Document, VideoPlay, Picture, Film, Compass, Star, Check, Plus } from '@element-plus/icons-vue'
|
||||
import { createPayment, createAlipayPayment, getUserSubscriptionInfo } from '@/api/payments'
|
||||
import { getPointsHistory } from '@/api/points'
|
||||
import { getMembershipLevels } from '@/api/members'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import {
|
||||
User,
|
||||
Document,
|
||||
User as Plus,
|
||||
Bell,
|
||||
Check,
|
||||
Compass,
|
||||
VideoPlay,
|
||||
Picture,
|
||||
Film,
|
||||
Star
|
||||
} from '@element-plus/icons-vue'
|
||||
import LanguageSwitcher from '@/components/LanguageSwitcher.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
@@ -278,6 +284,7 @@ const userInfo = ref({
|
||||
username: '',
|
||||
userId: null,
|
||||
points: 0,
|
||||
frozenPoints: 0,
|
||||
email: '',
|
||||
nickname: ''
|
||||
})
|
||||
@@ -343,6 +350,7 @@ const loadUserSubscriptionInfo = async () => {
|
||||
username: data.username || '',
|
||||
userId: data.userId || null,
|
||||
points: data.points || 0,
|
||||
frozenPoints: data.frozenPoints || 0,
|
||||
email: data.email || '',
|
||||
nickname: data.nickname || ''
|
||||
}
|
||||
@@ -366,6 +374,7 @@ const loadUserSubscriptionInfo = async () => {
|
||||
username: data.username || '',
|
||||
userId: data.userId || null,
|
||||
points: data.points || 0,
|
||||
frozenPoints: data.frozenPoints || 0,
|
||||
email: data.email || '',
|
||||
nickname: data.nickname || ''
|
||||
}
|
||||
@@ -457,6 +466,7 @@ onMounted(async () => {
|
||||
username: userStore.user.username || '',
|
||||
userId: userStore.user.id || null,
|
||||
points: userStore.user.points || 0,
|
||||
frozenPoints: userStore.user.frozenPoints || 0,
|
||||
email: userStore.user.email || '',
|
||||
nickname: userStore.user.nickname || ''
|
||||
}
|
||||
@@ -810,7 +820,7 @@ const createSubscriptionOrder = async (planType, planInfo) => {
|
||||
/* 左侧导航栏 */
|
||||
.sidebar {
|
||||
width: 280px !important; /* 放大侧边栏 */
|
||||
background: #1a1a1a !important;
|
||||
background: #000000 !important;
|
||||
padding: 24px 0 !important;
|
||||
border-right: 1px solid #1a1a1a !important; /* 弱化分割线,与背景融为一体 */
|
||||
flex-shrink: 0 !important;
|
||||
@@ -821,9 +831,14 @@ const createSubscriptionOrder = async (planType, planInfo) => {
|
||||
|
||||
.logo {
|
||||
padding: 0 24px 32px;
|
||||
font-size: 20px; /* 放大标题 */
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.logo img {
|
||||
height: 40px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.nav-menu, .tools-menu {
|
||||
@@ -910,6 +925,70 @@ const createSubscriptionOrder = async (planType, planInfo) => {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 顶部导航栏 */
|
||||
.top-header {
|
||||
height: 80px;
|
||||
padding: 0 30px;
|
||||
border-bottom: 1px solid #333;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
background: #0a0a0a;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.points-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 12px;
|
||||
background: rgba(64, 158, 255, 0.1);
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(64, 158, 255, 0.3);
|
||||
}
|
||||
|
||||
.points-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: #409EFF;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.points-number {
|
||||
color: #409EFF;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
cursor: pointer;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.user-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.user-avatar:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* 套餐选择 */
|
||||
.subscription-packages {
|
||||
padding: 0 30px 30px; /* 与顶部盒子保持一致的左右留白 */
|
||||
@@ -1011,6 +1090,12 @@ const createSubscriptionOrder = async (planType, planInfo) => {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.subscription-packages .points-box-placeholder {
|
||||
background: transparent;
|
||||
color: transparent;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.subscription-packages .package-button {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
@@ -1110,10 +1195,22 @@ const createSubscriptionOrder = async (planType, planInfo) => {
|
||||
|
||||
/* 顶部行内的用户信息与按钮样式(沿用原样式类) */
|
||||
.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; }
|
||||
.avatar-wrap {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.avatar-wrap .avatar-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.user-meta { display:flex; flex-direction:column; }
|
||||
.username { font-size:19px; font-weight:600; color:#e5e7eb; }
|
||||
.user-id { font-size:15px; color:#9ca3af; }
|
||||
|
||||
@@ -3,41 +3,56 @@
|
||||
<!-- 左侧导航栏 -->
|
||||
<aside class="sidebar">
|
||||
<div class="logo">
|
||||
<div class="logo-icon"></div>
|
||||
<span>LOGO</span>
|
||||
<div class="logo-icon">
|
||||
<svg viewBox="0 0 194 41" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M54.6165 13.6062L60.593 32.3932H60.8224L66.8111 13.6062H72.6065L64.0824 38.3335H57.3452L48.8089 13.6062H54.6165ZM75.4862 38.3335V19.788H80.6296V38.3335H75.4862ZM78.07 17.3974C77.3053 17.3974 76.6493 17.1439 76.1019 16.6368C75.5626 16.1216 75.293 15.5058 75.293 14.7895C75.293 14.0811 75.5626 13.4734 76.1019 12.9663C76.6493 12.4512 77.3053 12.1936 78.07 12.1936C78.8346 12.1936 79.4866 12.4512 80.0259 12.9663C80.5733 13.4734 80.8469 14.0811 80.8469 14.7895C80.8469 15.5058 80.5733 16.1216 80.0259 16.6368C79.4866 17.1439 78.8346 17.3974 78.07 17.3974ZM91.5836 38.6353C90.175 38.6353 88.8992 38.2731 87.7562 37.5487C86.6213 36.8162 85.7198 35.7416 85.0517 34.325C84.3916 32.9003 84.0616 31.1536 84.0616 29.0849C84.0616 26.9599 84.4037 25.1931 85.0879 23.7845C85.7721 22.3678 86.6816 21.3093 87.8166 20.6091C88.9596 19.9007 90.2112 19.5466 91.5716 19.5466C92.6099 19.5466 93.4752 19.7236 94.1674 20.0778C94.8677 20.4239 95.4312 20.8586 95.8578 21.3818C96.2924 21.8969 96.6225 22.404 96.8478 22.9031H97.0048V13.6062H102.136V38.3335H97.0652V35.3633H96.8478C96.6064 35.8785 96.2643 36.3896 95.8216 36.8967C95.3869 37.3958 94.8194 37.8103 94.1191 38.1403C93.4269 38.4703 92.5817 38.6353 91.5836 38.6353ZM93.2136 34.5423C94.0427 34.5423 94.743 34.3169 95.3145 33.8662C95.894 33.4074 96.3367 32.7674 96.6426 31.9464C96.9565 31.1254 97.1135 30.1635 97.1135 29.0608C97.1135 27.958 96.9605 27.0002 96.6547 26.1872C96.3488 25.3742 95.9061 24.7464 95.3265 24.3037C94.747 23.861 94.0427 23.6396 93.2136 23.6396C92.3684 23.6396 91.6561 23.869 91.0765 24.3278C90.497 24.7866 90.0583 25.4225 89.7605 26.2355C89.4627 27.0485 89.3137 27.9902 89.3137 29.0608C89.3137 30.1394 89.4627 31.0932 89.7605 31.9223C90.0663 32.7433 90.505 33.3872 91.0765 33.8541C91.6561 34.3129 92.3684 34.5423 93.2136 34.5423ZM106.462 38.3335V13.6062H122.834V17.9166H111.69V23.8086H121.747V28.119H111.69V38.3335H106.462ZM131.397 13.6062V38.3335H126.254V13.6062H131.397ZM143.897 38.6957C142.021 38.6957 140.399 38.2973 139.031 37.5004C137.671 36.6955 136.62 35.5766 135.88 34.1439C135.139 32.7031 134.769 31.0328 134.769 29.1332C134.769 27.2175 135.139 25.5432 135.88 24.1105C136.62 22.6697 137.671 21.5508 139.031 20.7539C140.399 19.949 142.021 19.5466 143.897 19.5466C145.772 19.5466 147.39 19.949 148.75 20.7539C150.119 21.5508 151.173 22.6697 151.914 24.1105C152.654 25.5432 153.025 27.2175 153.025 29.1332C153.025 31.0328 152.654 32.7031 151.914 34.1439C151.173 35.5766 150.119 36.6955 148.75 37.5004C147.39 38.2973 145.772 38.6957 143.897 38.6957ZM143.921 34.7113C144.774 34.7113 145.486 34.4699 146.058 33.9869C146.629 33.4959 147.06 32.8278 147.35 31.9826C147.648 31.1375 147.797 30.1756 147.797 29.097C147.797 28.0184 147.648 27.0565 147.35 26.2113C147.06 25.3662 146.629 24.6981 146.058 24.2071C145.486 23.7161 144.774 23.4706 143.921 23.4706C143.06 23.4706 142.335 23.7161 141.748 24.2071C141.168 24.6981 140.729 25.3662 140.431 26.2113C140.142 27.0565 139.997 28.0184 139.997 29.097C139.997 30.1756 140.142 31.1375 140.431 31.9826C140.729 32.8278 141.168 33.4959 141.748 33.9869C142.335 34.4699 143.06 34.7113 143.921 34.7113ZM159.562 38.3335L154.516 19.788H159.719L162.593 32.2483H162.762L165.756 19.788H170.864L173.906 32.1758H174.063L176.888 19.788H182.08L177.045 38.3335H171.6L168.413 26.6701H168.183L164.996 38.3335H159.562Z" fill="white"/>
|
||||
<g clip-path="url(#clip0)">
|
||||
<path d="M5.7406 1.64568C2.43935 1.64568 0.00938034 1.57298 0.000366208 1.64568C0.000366202 1.71906 2.11292 5.37389 4.70642 9.58611C7.28533 13.813 12.3557 22.0905 15.9545 27.9611L20.8685 35.9875C21.8796 37.6388 23.6773 38.6457 25.6136 38.6457L26.4301 38.6457C26.4301 38.6457 31.0568 38.7204 31.9965 37.2228C32.2255 36.8288 32.346 36.3807 32.3461 35.925L32.3461 21.8996L31.1801 21.8996L31.1801 26.5969C31.1801 31.1005 31.1655 31.3074 30.889 31.5861C30.7288 31.7476 30.4663 31.8801 30.306 31.8801C29.9855 31.8801 29.7811 31.5866 27.1439 27.257C26.2551 25.8039 24.0844 22.2371 22.2924 19.3312C20.5148 16.4253 17.3534 11.2596 15.2699 7.85467L11.4672 1.64568L5.7406 1.64568ZM31.6615 2.99627C31.443 4.1703 30.525 6.06354 29.6654 7.10564C28.4561 8.60263 26.4304 9.83531 24.5072 10.2316C23.4727 10.4518 23.196 10.5988 23.808 10.5988C24.0267 10.5989 24.5801 10.7012 25.0316 10.8332C28.2662 11.7578 31.0495 14.7674 31.6468 17.9816L31.8363 18.9797L32.0111 18.1135C32.5648 15.3249 34.4443 12.8151 36.9066 11.5676C38.0139 10.9952 39.2815 10.5988 39.9808 10.5988C40.6217 10.5988 40.403 10.4957 39.2084 10.2463C37.46 9.87934 36.1049 9.11623 34.6625 7.66326C33.2638 6.26901 32.5496 5.02127 32.0834 3.17205L31.8217 2.11541L31.6615 2.99627Z" fill="url(#paint0)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0" x1="23.4862" y1="5.3947" x2="32.9093" y2="11.1248" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.0001" stop-color="#33DDE5"/>
|
||||
<stop offset="1" stop-color="#0F9CFF"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0">
|
||||
<rect width="40.3334" height="40.3334" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="nav-menu">
|
||||
<div class="nav-item" @click="goToDashboard">
|
||||
<el-icon><Grid /></el-icon>
|
||||
<span>数据仪表台</span>
|
||||
<span>{{ $t('nav.dashboard') }}</span>
|
||||
</div>
|
||||
<div class="nav-item" @click="goToMembers">
|
||||
<el-icon><User /></el-icon>
|
||||
<span>会员管理</span>
|
||||
<span>{{ $t('nav.members') }}</span>
|
||||
</div>
|
||||
<div class="nav-item" @click="goToOrders">
|
||||
<el-icon><ShoppingCart /></el-icon>
|
||||
<span>订单管理</span>
|
||||
<span>{{ $t('nav.orders') }}</span>
|
||||
</div>
|
||||
<div class="nav-item" @click="goToAPI">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>API管理</span>
|
||||
<span>{{ $t('nav.apiManagement') }}</span>
|
||||
</div>
|
||||
<div class="nav-item" @click="goToTasks">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>生成任务记录</span>
|
||||
<span>{{ $t('nav.tasks') }}</span>
|
||||
</div>
|
||||
<div class="nav-item active">
|
||||
<el-icon><Setting /></el-icon>
|
||||
<span>系统设置</span>
|
||||
<span>{{ $t('nav.systemSettings') }}</span>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<div class="online-users">
|
||||
当前在线用户: <span class="highlight">87/500</span>
|
||||
{{ $t('nav.onlineUsers') }}: <span class="highlight">{{ onlineUsers }}</span>
|
||||
</div>
|
||||
<div class="system-uptime">
|
||||
系统运行时间: <span class="highlight">48小时32分</span>
|
||||
{{ $t('nav.systemUptime') }}: <span class="highlight">{{ systemUptime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -48,15 +63,12 @@
|
||||
<header class="top-header">
|
||||
<div class="search-bar">
|
||||
<el-icon class="search-icon"><User /></el-icon>
|
||||
<input type="text" placeholder="搜索你想要的内容" class="search-input">
|
||||
<input type="text" :placeholder="$t('common.searchPlaceholder')" class="search-input">
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<div class="notification-icon-wrapper">
|
||||
<el-icon class="notification-icon"><Bell /></el-icon>
|
||||
<span class="notification-badge"></span>
|
||||
</div>
|
||||
<LanguageSwitcher />
|
||||
<div class="user-avatar">
|
||||
<img src="/images/backgrounds/welcome.jpg" alt="用户头像" />
|
||||
<img src="/images/backgrounds/avatar-default.svg" alt="用户头像" />
|
||||
<el-icon class="arrow-down"><ArrowDown /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
@@ -65,38 +77,38 @@
|
||||
<!-- 设置选项卡 -->
|
||||
<div class="settings-tabs">
|
||||
<div class="tab-nav">
|
||||
<div
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'membership' }"
|
||||
<div
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'membership' }"
|
||||
@click="activeTab = 'membership'"
|
||||
>
|
||||
<el-icon><User /></el-icon>
|
||||
<span>会员收费标准</span>
|
||||
<span>{{ $t('systemSettings.membership') }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'cleanup' }"
|
||||
<div
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'cleanup' }"
|
||||
@click="activeTab = 'cleanup'"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
<span>任务清理管理</span>
|
||||
<span>{{ $t('systemSettings.cleanup') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 会员收费标准选项卡 -->
|
||||
<div v-if="activeTab === 'membership'" class="tab-content">
|
||||
<h2 class="page-title">会员收费标准</h2>
|
||||
<h2 class="page-title">{{ $t('systemSettings.membership') }}</h2>
|
||||
<div class="membership-cards">
|
||||
<el-card v-for="level in membershipLevels" :key="level.id" class="membership-card">
|
||||
<div class="card-header">
|
||||
<h3>{{ level.name }}</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="price">${{ level.price || 0 }}/月</p>
|
||||
<p class="description">{{ level.description || `包含${level.resourcePoints || 0}资源点/月` }}</p>
|
||||
<p class="price">${{ level.price || 0 }}{{ $t('systemSettings.perMonth') }}</p>
|
||||
<p class="description">{{ level.description || $t('systemSettings.includesPoints', { points: level.resourcePoints || 0 }) }}</p>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<el-button type="primary" @click="editLevel(level)">编辑</el-button>
|
||||
<el-button type="primary" @click="editLevel(level)">{{ $t('common.edit') }}</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
@@ -104,45 +116,45 @@
|
||||
|
||||
<!-- 任务清理管理选项卡 -->
|
||||
<div v-if="activeTab === 'cleanup'" class="tab-content">
|
||||
<h2 class="page-title">任务清理管理</h2>
|
||||
|
||||
<h2 class="page-title">{{ $t('systemSettings.cleanup') }}</h2>
|
||||
|
||||
<!-- 清理统计信息 -->
|
||||
<div class="cleanup-stats">
|
||||
<el-card class="stats-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<h3>清理统计信息</h3>
|
||||
<h3>{{ $t('systemSettings.cleanupStatsInfo') }}</h3>
|
||||
<el-button type="primary" @click="refreshStats" :loading="loadingStats">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
{{ $t('systemSettings.refresh') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<div class="stats-content" v-if="cleanupStats">
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">当前任务总数</div>
|
||||
<div class="stat-label">{{ $t('systemSettings.currentTotalTasks') }}</div>
|
||||
<div class="stat-value">{{ cleanupStats.currentTasks?.textToVideo?.total + cleanupStats.currentTasks?.imageToVideo?.total || 0 }}</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">已完成任务</div>
|
||||
<div class="stat-label">{{ $t('systemSettings.completedTasks') }}</div>
|
||||
<div class="stat-value">{{ cleanupStats.currentTasks?.textToVideo?.completed + cleanupStats.currentTasks?.imageToVideo?.completed || 0 }}</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">失败任务</div>
|
||||
<div class="stat-label">{{ $t('systemSettings.failedTasks') }}</div>
|
||||
<div class="stat-value">{{ cleanupStats.currentTasks?.textToVideo?.failed + cleanupStats.currentTasks?.imageToVideo?.failed || 0 }}</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">已归档任务</div>
|
||||
<div class="stat-label">{{ $t('systemSettings.archivedTasks') }}</div>
|
||||
<div class="stat-value">{{ cleanupStats.archives?.completedTasks || 0 }}</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">清理日志数</div>
|
||||
<div class="stat-label">{{ $t('systemSettings.cleanupLogsCount') }}</div>
|
||||
<div class="stat-value">{{ cleanupStats.archives?.cleanupLogs || 0 }}</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">保留天数</div>
|
||||
<div class="stat-value">{{ cleanupStats.config?.retentionDays || 30 }}天</div>
|
||||
<div class="stat-label">{{ $t('systemSettings.retentionDays') }}</div>
|
||||
<div class="stat-value">{{ cleanupStats.config?.retentionDays || 30 }}{{ $t('systemSettings.days') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -154,32 +166,32 @@
|
||||
<el-card class="actions-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<h3>清理操作</h3>
|
||||
<h3>{{ $t('systemSettings.cleanupActions') }}</h3>
|
||||
</div>
|
||||
</template>
|
||||
<div class="actions-content">
|
||||
<div class="action-buttons">
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="performFullCleanup"
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="performFullCleanup"
|
||||
:loading="loadingCleanup"
|
||||
class="action-btn"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
执行完整清理
|
||||
{{ $t('systemSettings.performFullCleanup') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
type="warning"
|
||||
<el-button
|
||||
type="warning"
|
||||
@click="showUserCleanupDialog = true"
|
||||
class="action-btn"
|
||||
>
|
||||
<el-icon><User /></el-icon>
|
||||
清理指定用户任务
|
||||
{{ $t('systemSettings.cleanupUserTasks') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="action-description">
|
||||
<p><strong>完整清理:</strong>将成功任务导出到归档表,删除失败任务</p>
|
||||
<p><strong>用户清理:</strong>清理指定用户的所有任务</p>
|
||||
<p><strong>{{ $t('systemSettings.fullCleanupDesc') }}:</strong>{{ $t('systemSettings.fullCleanupDescDetail') }}</p>
|
||||
<p><strong>{{ $t('systemSettings.userCleanupDesc') }}:</strong>{{ $t('systemSettings.userCleanupDescDetail') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
@@ -190,32 +202,32 @@
|
||||
<el-card class="config-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<h3>清理配置</h3>
|
||||
<h3>{{ $t('systemSettings.cleanupConfig') }}</h3>
|
||||
</div>
|
||||
</template>
|
||||
<div class="config-content">
|
||||
<el-form :model="cleanupConfig" label-width="120px">
|
||||
<el-form-item label="任务保留天数">
|
||||
<el-input-number
|
||||
v-model="cleanupConfig.retentionDays"
|
||||
:min="1"
|
||||
<el-form-item :label="$t('systemSettings.taskRetentionDays')">
|
||||
<el-input-number
|
||||
v-model="cleanupConfig.retentionDays"
|
||||
:min="1"
|
||||
:max="365"
|
||||
controls-position="right"
|
||||
/>
|
||||
<span class="config-tip">任务完成后保留的天数</span>
|
||||
<span class="config-tip">{{ $t('systemSettings.taskRetentionTip') }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="归档保留天数">
|
||||
<el-input-number
|
||||
v-model="cleanupConfig.archiveRetentionDays"
|
||||
:min="30"
|
||||
<el-form-item :label="$t('systemSettings.archiveRetentionDays')">
|
||||
<el-input-number
|
||||
v-model="cleanupConfig.archiveRetentionDays"
|
||||
:min="30"
|
||||
:max="3650"
|
||||
controls-position="right"
|
||||
/>
|
||||
<span class="config-tip">归档数据保留的天数</span>
|
||||
<span class="config-tip">{{ $t('systemSettings.archiveRetentionTip') }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="saveCleanupConfig" :loading="loadingConfig">
|
||||
保存配置
|
||||
{{ $t('common.save') }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
@@ -236,89 +248,89 @@
|
||||
>
|
||||
<template #header>
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">会员收费标准</h2>
|
||||
<h2 class="modal-title">{{ $t('systemSettings.membership') }}</h2>
|
||||
<button class="close-btn" @click="handleCloseEditDialog">×</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<div class="modal-content">
|
||||
<el-form :model="editForm" :rules="editRules" ref="editFormRef">
|
||||
<div class="form-group">
|
||||
<label class="form-label">会员等级</label>
|
||||
<el-select v-model="editForm.level" placeholder="请选择会员等级" style="width: 100%;">
|
||||
<el-option label="免费版会员" value="free"></el-option>
|
||||
<el-option label="标准版会员" value="standard"></el-option>
|
||||
<el-option label="专业版会员" value="professional"></el-option>
|
||||
<label class="form-label">{{ $t('systemSettings.membershipLevel') }}</label>
|
||||
<el-select v-model="editForm.level" :placeholder="$t('systemSettings.selectLevelPlaceholder')" style="width: 100%;">
|
||||
<el-option :label="$t('systemSettings.freeMembership')" value="free"></el-option>
|
||||
<el-option :label="$t('systemSettings.standardMembership')" value="standard"></el-option>
|
||||
<el-option :label="$t('systemSettings.professionalMembership')" value="professional"></el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">会员价格</label>
|
||||
<label class="form-label">{{ $t('systemSettings.membershipPrice') }}</label>
|
||||
<div class="price-input">
|
||||
<span class="price-prefix">$</span>
|
||||
<input
|
||||
type="text"
|
||||
v-model="editForm.price"
|
||||
placeholder="0.00"
|
||||
<input
|
||||
type="text"
|
||||
v-model="editForm.price"
|
||||
placeholder="0.00"
|
||||
class="form-control"
|
||||
@input="handlePriceInput"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">资源点数量</label>
|
||||
<input
|
||||
type="number"
|
||||
v-model="editForm.resourcePoints"
|
||||
placeholder="0"
|
||||
<label class="form-label">{{ $t('systemSettings.resourcePointsAmount') }}</label>
|
||||
<input
|
||||
type="number"
|
||||
v-model="editForm.resourcePoints"
|
||||
placeholder="0"
|
||||
min="0"
|
||||
class="form-control"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">会员有效期</label>
|
||||
<label class="form-label">{{ $t('systemSettings.validityPeriod') }}</label>
|
||||
<div class="radio-group">
|
||||
<div class="radio-option">
|
||||
<input
|
||||
type="radio"
|
||||
id="monthly"
|
||||
v-model="editForm.validityPeriod"
|
||||
value="monthly"
|
||||
<input
|
||||
type="radio"
|
||||
id="monthly"
|
||||
v-model="editForm.validityPeriod"
|
||||
value="monthly"
|
||||
class="radio-input"
|
||||
>
|
||||
<label for="monthly" class="radio-label">月付</label>
|
||||
<label for="monthly" class="radio-label">{{ $t('systemSettings.monthly') }}</label>
|
||||
</div>
|
||||
<div class="radio-option">
|
||||
<input
|
||||
type="radio"
|
||||
id="quarterly"
|
||||
v-model="editForm.validityPeriod"
|
||||
value="quarterly"
|
||||
<input
|
||||
type="radio"
|
||||
id="quarterly"
|
||||
v-model="editForm.validityPeriod"
|
||||
value="quarterly"
|
||||
class="radio-input"
|
||||
>
|
||||
<label for="quarterly" class="radio-label">季付</label>
|
||||
<label for="quarterly" class="radio-label">{{ $t('systemSettings.quarterly') }}</label>
|
||||
</div>
|
||||
<div class="radio-option">
|
||||
<input
|
||||
type="radio"
|
||||
id="yearly"
|
||||
v-model="editForm.validityPeriod"
|
||||
value="yearly"
|
||||
<input
|
||||
type="radio"
|
||||
id="yearly"
|
||||
v-model="editForm.validityPeriod"
|
||||
value="yearly"
|
||||
class="radio-input"
|
||||
>
|
||||
<label for="yearly" class="radio-label">年付</label>
|
||||
<label for="yearly" class="radio-label">{{ $t('systemSettings.yearly') }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
|
||||
<template #footer>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-cancel" @click="handleCloseEditDialog">取消</button>
|
||||
<button class="btn btn-save" @click="saveEdit">保存</button>
|
||||
<button class="btn btn-cancel" @click="handleCloseEditDialog">{{ $t('common.cancel') }}</button>
|
||||
<button class="btn btn-save" @click="saveEdit">{{ $t('common.save') }}</button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@@ -326,49 +338,49 @@
|
||||
<!-- 用户清理对话框 -->
|
||||
<el-dialog
|
||||
v-model="showUserCleanupDialog"
|
||||
title="清理指定用户任务"
|
||||
:title="$t('systemSettings.cleanupUserTasks')"
|
||||
width="480px"
|
||||
:before-close="handleCloseUserCleanupDialog"
|
||||
>
|
||||
<div class="user-cleanup-content">
|
||||
<el-form :model="userCleanupForm" :rules="userCleanupRules" ref="userCleanupFormRef">
|
||||
<el-form-item label="用户名" prop="username">
|
||||
<el-input
|
||||
v-model="userCleanupForm.username"
|
||||
placeholder="请输入要清理的用户名"
|
||||
<el-form-item :label="$t('members.username')" prop="username">
|
||||
<el-input
|
||||
v-model="userCleanupForm.username"
|
||||
:placeholder="$t('systemSettings.enterUsername')"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-alert
|
||||
title="警告"
|
||||
:title="$t('systemSettings.warning')"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
>
|
||||
<template #default>
|
||||
<p>此操作将清理该用户的所有任务,包括:</p>
|
||||
<p>{{ $t('systemSettings.cleanupWarning') }}</p>
|
||||
<ul>
|
||||
<li>成功任务将导出到归档表</li>
|
||||
<li>失败任务将记录到清理日志</li>
|
||||
<li>原始任务记录将被删除</li>
|
||||
<li>{{ $t('systemSettings.successTasksArchived') }}</li>
|
||||
<li>{{ $t('systemSettings.failedTasksLogged') }}</li>
|
||||
<li>{{ $t('systemSettings.originalTasksDeleted') }}</li>
|
||||
</ul>
|
||||
<p><strong>此操作不可撤销,请谨慎操作!</strong></p>
|
||||
<p><strong>{{ $t('systemSettings.irreversibleWarning') }}</strong></p>
|
||||
</template>
|
||||
</el-alert>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleCloseUserCleanupDialog">取消</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
@click="performUserCleanup"
|
||||
<el-button @click="handleCloseUserCleanupDialog">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
@click="performUserCleanup"
|
||||
:loading="loadingUserCleanup"
|
||||
>
|
||||
确认清理
|
||||
{{ $t('systemSettings.confirmCleanup') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -387,19 +399,23 @@ import {
|
||||
Document,
|
||||
Setting,
|
||||
User as Search,
|
||||
Bell,
|
||||
User as ArrowDown,
|
||||
Delete,
|
||||
Refresh
|
||||
} from '@element-plus/icons-vue'
|
||||
import cleanupApi from '@/api/cleanup'
|
||||
import { getMembershipLevels, updateMembershipLevel } from '@/api/members'
|
||||
import LanguageSwitcher from '@/components/LanguageSwitcher.vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 选项卡状态
|
||||
const activeTab = ref('membership')
|
||||
|
||||
// 系统状态数据
|
||||
const onlineUsers = ref('0/500')
|
||||
const systemUptime = ref('加载中...')
|
||||
|
||||
// 会员收费标准相关
|
||||
const membershipLevels = ref([])
|
||||
const loadingLevels = ref(false)
|
||||
@@ -706,7 +722,27 @@ const saveCleanupConfig = async () => {
|
||||
onMounted(() => {
|
||||
refreshStats()
|
||||
loadMembershipLevels()
|
||||
fetchSystemStats()
|
||||
})
|
||||
|
||||
// 获取系统统计数据
|
||||
const fetchSystemStats = async () => {
|
||||
try {
|
||||
// 临时使用计算值,后续可以从API获取
|
||||
// 计算在线用户数(这里简化处理)
|
||||
const randomOnline = Math.floor(Math.random() * 50) + 10
|
||||
onlineUsers.value = `${randomOnline}/500`
|
||||
|
||||
// 计算系统运行时间(基于当前时间简单模拟)
|
||||
const hours = new Date().getHours()
|
||||
const minutes = new Date().getMinutes()
|
||||
systemUptime.value = `${hours}小时${minutes}分`
|
||||
} catch (error) {
|
||||
console.error('获取系统统计失败:', error)
|
||||
onlineUsers.value = '0/500'
|
||||
systemUptime.value = '未知'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -731,22 +767,23 @@ onMounted(() => {
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
padding: 0 50px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: #3b82f6;
|
||||
border-radius: 4px;
|
||||
margin-right: 12px;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.logo span {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
.logo-icon svg, .logo-icon img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
@@ -853,33 +890,6 @@ onMounted(() => {
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.notification-icon-wrapper {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.notification-icon-wrapper:hover {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.notification-icon {
|
||||
font-size: 20px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.notification-badge {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #ef4444;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
440
demo/frontend/src/views/TermsOfService.vue
Normal file
440
demo/frontend/src/views/TermsOfService.vue
Normal file
@@ -0,0 +1,440 @@
|
||||
<template>
|
||||
<div class="terms-of-service">
|
||||
<el-page-header @back="$router.go(-1)" content="Vionow 服务条款">
|
||||
</el-page-header>
|
||||
|
||||
<el-card class="terms-card">
|
||||
<div class="terms-content">
|
||||
<!-- 中文版本 -->
|
||||
<div class="language-section">
|
||||
<h1>Vionow 服务条款</h1>
|
||||
<p class="intro">
|
||||
欢迎来到 Vionow。本服务条款(下称"条款")适用于对网站 <a href="https://vionow.com" target="_blank">https://vionow.com</a> 及AI视频创作软件(下称"服务"或"平台")的访问和使用。通过访问、注册或使用 Vionow,用户(下称"用户")同意本条款并承诺完全遵守。
|
||||
</p>
|
||||
|
||||
<section>
|
||||
<h2>法律信息和所有权</h2>
|
||||
<p>根据适用法规,向用户告知有关服务提供商的以下详细信息:</p>
|
||||
<ul>
|
||||
<li><strong>所有者:</strong>Vionow Team</li>
|
||||
<li><strong>联系邮箱:</strong><a href="mailto:contact@vionow.com">contact@vionow.com</a></li>
|
||||
<li><strong>网站:</strong><a href="https://vionow.com" target="_blank">https://vionow.com</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>服务说明</h2>
|
||||
<p>Vionow 是一个基于人工智能的视频创作平台。访问该服务需要注册,并采用基于代币的模式运行:</p>
|
||||
<ul>
|
||||
<li>用户必须注册才能访问平台。</li>
|
||||
<li>代币通过付费订阅计划获得。</li>
|
||||
<li>代币不可累积;它们在每个计费周期结束时到期。</li>
|
||||
<li>服务按"原样"提供,不保证适用于任何特定目的。</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>用户注册和账户</h2>
|
||||
<p>用户必须提供有效的电子邮件地址进行注册。注册即表示用户同意:</p>
|
||||
<ul>
|
||||
<li>提供准确、最新和完整的信息。</li>
|
||||
<li>对登录凭据保密,不与第三方共享。</li>
|
||||
<li>避免任何欺诈或滥用本服务的行为。</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>通讯</h2>
|
||||
<p>注册即表示用户同意接收有关服务的通讯,包括更新、教程和宣传材料。用户可以随时通过邮件中的退订链接选择退订。</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>订阅计划和自动续订</h2>
|
||||
<ul>
|
||||
<li>除非提前取消,否则订阅将自动续订。</li>
|
||||
<li>付款按周期性处理。</li>
|
||||
<li>代币每月更新,不会结转。</li>
|
||||
<li>用户可随时取消;取消将在当前计费周期结束时生效。未使用的代币不予退款。</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>退款政策和有限保证</h2>
|
||||
<p>我们提供7天保证,前提是用户渲染的视频项目不超过一个。如果渲染的视频项目超过一个,则用户将丧失申请退款的权利。对于因不满意结果、平台期望或未使用服务而提出的退款请求,将不予受理。用户承担使用本服务的所有风险。</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>退款保证</h2>
|
||||
<p>除一般退款政策外,Vionow 还为首次购买客户提供一次性退款保证,但需满足以下条件:</p>
|
||||
<ul>
|
||||
<li>用户使用的积分不得超过所购套餐中包含积分的30%。例如,对于尝试套餐(500积分),申请退款的资格要求是消耗的积分不超过150个。</li>
|
||||
<li>退款请求必须通过填写一份强制性反馈表提交,并详细说明不满意的原因。该表格必须在购买之日起15天内提交。</li>
|
||||
<li>在以下情况下将不予退款:
|
||||
<ul>
|
||||
<li>使用的积分超过30%。</li>
|
||||
<li>表格不完整或在15天窗口期后提交。</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>退款将通过原始支付方式进行,处理时间最多可能需要10个工作日。</li>
|
||||
</ul>
|
||||
<p>使用本服务即表示用户承认并同意这些条件。</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>服务的正当使用</h2>
|
||||
<p>用户同意根据适用法律和道德标准使用本平台。禁止的活动包括但不限于:</p>
|
||||
<ul>
|
||||
<li>上传非法、冒犯、色情、诽谤或未经适当授权的第三方版权材料。</li>
|
||||
<li>试图访问源代码或对软件进行逆向工程。</li>
|
||||
<li>利用漏洞或从事系统滥用行为。</li>
|
||||
</ul>
|
||||
<p>公司保留暂停或终止任何违反这些规定的账户的权利。</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>数据保护和隐私</h2>
|
||||
<p>Vionow 遵守相关数据保护法规。个人数据将根据我们的隐私政策进行处理,该政策概述了:</p>
|
||||
<ul>
|
||||
<li>数据收集的目的</li>
|
||||
<li>处理的法律依据</li>
|
||||
<li>用户在访问、纠正、删除、反对和可移植性方面的权利</li>
|
||||
</ul>
|
||||
<p>有关数据相关的请求,请联系:<a href="mailto:contact@vionow.com">contact@vionow.com</a>。</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>上传内容的所有权和许可授予</h2>
|
||||
<p>用户保留上传至本服务内容(如视频、素材)的完全所有权。使用本服务即表示用户授予公司非排他性、免版税、全球性、永久性的许可,以仅为推广、营销或演示目的(例如在网站、社交媒体或新闻通讯中)使用、复制、修改和公开展示该等内容,前提是该内容不包含可识别的个人或机密信息。用户可随时通过联系 <a href="mailto:contact@vionow.com">contact@vionow.com</a> 撤销此许可。收到请求后,公司将停止使用并从营销渠道中删除该等内容。</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>责任免除</h2>
|
||||
<p>本服务不提供任何形式的保证。Vionow 不保证:</p>
|
||||
<ul>
|
||||
<li>服务将不间断、安全或无错误。</li>
|
||||
<li>输出结果将满足用户的主观期望。</li>
|
||||
<li>与第三方格式、软件或平台的兼容性。</li>
|
||||
</ul>
|
||||
<p>用户承认并接受与使用本服务相关的所有风险。</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>条款和条件的修改</h2>
|
||||
<p>我们保留随时修改本条款的权利。重大变更将通过电子邮件或网站公告进行通知。继续使用本服务即表示接受更新后的条款。</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>适用法律和司法管辖权</h2>
|
||||
<p>本条款受香港法律管辖。因解释或执行本条款而产生的任何争议,除非适用法律另有规定,否则应提交至香港法院。</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>联系方式</h2>
|
||||
<p>如有法律或运营方面的疑问,请联系:<a href="mailto:contact@vionow.com">contact@vionow.com</a></p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Cookies 政策</h2>
|
||||
<p>我们的网站使用 cookies 以改善用户体验。Cookies 是存储在您设备上的小型文本文件,允许我们收集有关浏览行为和网站使用情况的信息。</p>
|
||||
<h3>使用的 Cookies 类型</h3>
|
||||
<p>我们仅使用根据适用法律免于征求同意的基本和分析性 cookies:</p>
|
||||
<ul>
|
||||
<li><strong>purecookieDismiss:</strong>记录用户是否已关闭 cookie 横幅。为确保正常运行所必需。</li>
|
||||
<li><strong>__gsas, _ga_GMVSXDG494, _ga:</strong>用于汇总统计洞察的 Google Analytics 4 cookies。这些 cookies 不记录或存储 IP 地址。</li>
|
||||
</ul>
|
||||
<p>Cookies 不用于广告或用户画像目的。用户可以配置其浏览器设置来管理或删除 cookies。</p>
|
||||
</section>
|
||||
|
||||
<section class="conclusion">
|
||||
<p>本文件构成用户与 Vionow 就使用本服务达成的完整协议,并取代任何先前的协议或通讯。</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- 英文版本 -->
|
||||
<div class="language-section">
|
||||
<h1>Vionow Terms of Service</h1>
|
||||
<p class="intro">
|
||||
Welcome to Vionow. These Terms of Service ("Terms") govern access to and use of the website <a href="https://vionow.com" target="_blank">https://vionow.com</a> and the AI-powered video creation software (hereinafter, the "Service" or "Platform"). By accessing, registering, or using Vionow, the user (hereinafter, the "User") agrees to these Terms and commits to complying with them in full.
|
||||
</p>
|
||||
|
||||
<section>
|
||||
<h2>Legal Information and Ownership</h2>
|
||||
<p>In accordance with applicable regulations, Users are informed of the following details regarding the Service provider:</p>
|
||||
<ul>
|
||||
<li><strong>Owner:</strong> Vionow Team</li>
|
||||
<li><strong>Contact email:</strong> <a href="mailto:contact@vionow.com">contact@vionow.com</a></li>
|
||||
<li><strong>Website:</strong> <a href="https://vionow.com" target="_blank">https://vionow.com</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Description of the Service</h2>
|
||||
<p>Vionow is an AI-based platform for video creation. Access to the Service requires registration and operates on a token-based model:</p>
|
||||
<ul>
|
||||
<li>Users must register to access the Platform.</li>
|
||||
<li>Tokens are acquired through paid subscription plans.</li>
|
||||
<li>Tokens are non-cumulative; they expire at the end of each billing cycle.</li>
|
||||
<li>The Service is provided "as is" without guarantees of suitability for any specific purpose.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>User Registration and Account</h2>
|
||||
<p>Users must provide a valid email address to register. By registering, Users agree to:</p>
|
||||
<ul>
|
||||
<li>Provide accurate, current, and complete information.</li>
|
||||
<li>Keep login credentials confidential and not share them with third parties.</li>
|
||||
<li>Avoid any fraudulent or abusive use of the Service.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Communications</h2>
|
||||
<p>By registering, the User consents to receiving communications regarding the Service, including updates, tutorials, and promotional materials. Users may opt out at any time by following the unsubscribe link in such emails.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Subscription Plans and Automatic Renewal</h2>
|
||||
<ul>
|
||||
<li>Subscriptions renew automatically unless canceled in advance.</li>
|
||||
<li>Payments are processed on a recurring basis.</li>
|
||||
<li>Tokens are renewed monthly and do not carry over.</li>
|
||||
<li>Users may cancel at any time; cancellations take effect at the end of the current billing cycle. Refunds are not issued for unused tokens.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Refund Policy and Limited Guarantees</h2>
|
||||
<p>A 7-day guarantee is offered, provided the User has not rendered more than one video project. If more than one video project has been rendered, the right to request a refund is forfeited. No refunds will be given for dissatisfaction with results, platform expectations, or lack of usage. The User assumes all risk for using the Service.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Money-Back Guarantee</h2>
|
||||
<p>In addition to the general refund policy, Vionow offers a one-time money-back guarantee for first-time customers under the following conditions:</p>
|
||||
<ul>
|
||||
<li>The User must not have used more than 30% of the credits included in the purchased plan. For example, for the Try Plan (500 credits), eligibility for a refund requires that no more than 150 credits have been consumed.</li>
|
||||
<li>The refund request must be submitted by completing a mandatory feedback form, providing detailed reasons for dissatisfaction. This form must be submitted within 15 days of the purchase date.</li>
|
||||
<li>Refunds will not be granted:
|
||||
<ul>
|
||||
<li>If more than 30% of the credits have been used.</li>
|
||||
<li>If the form is incomplete or submitted after the 15-day window.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Refunds will be issued using the original payment method and may take up to 10 business days to be processed.</li>
|
||||
</ul>
|
||||
<p>By using the Service, the User acknowledges and agrees to these conditions.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Proper Use of the Service</h2>
|
||||
<p>Users agree to use the Platform in accordance with applicable law and ethical standards. Prohibited activities include, but are not limited to:</p>
|
||||
<ul>
|
||||
<li>Uploading illegal, offensive, pornographic, defamatory, or third-party copyrighted material without proper authorization.</li>
|
||||
<li>Attempting to access the source code or reverse engineer the software.</li>
|
||||
<li>Exploiting vulnerabilities or engaging in system abuse.</li>
|
||||
</ul>
|
||||
<p>The Company reserves the right to suspend or terminate any account that violates these provisions.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Data Protection and Privacy</h2>
|
||||
<p>Vionow complies with relevant data protection regulations. Personal data is processed according to our Privacy Policy, which outlines:</p>
|
||||
<ul>
|
||||
<li>Purpose of data collection</li>
|
||||
<li>Legal basis for processing</li>
|
||||
<li>User rights regarding access, rectification, erasure, objection, and portability</li>
|
||||
</ul>
|
||||
<p>Contact: <a href="mailto:contact@vionow.com">contact@vionow.com</a> for data-related requests.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Ownership of Uploaded Content and License Grant</h2>
|
||||
<p>Users retain full ownership of content (e.g., videos, assets) uploaded to the Service. By using the Service, the User grants the Company a non-exclusive, royalty-free, worldwide, perpetual license to use, reproduce, modify, and publicly display such content solely for promotional, marketing, or demonstration purposes (e.g., on the Website, social media, or newsletters), provided such content does not contain identifiable individuals or confidential information. Users may withdraw this license at any time by contacting <a href="mailto:contact@vionow.com">contact@vionow.com</a>. Upon request, the Company will cease using and remove such content from marketing channels.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Liability Disclaimer</h2>
|
||||
<p>The Service is provided without warranties of any kind. Vionow does not guarantee:</p>
|
||||
<ul>
|
||||
<li>That the Service will be uninterrupted, secure, or error-free.</li>
|
||||
<li>That the output will meet the User's subjective expectations.</li>
|
||||
<li>Compatibility with third-party formats, software, or platforms.</li>
|
||||
</ul>
|
||||
<p>The User acknowledges and accepts all risks related to the use of the Service.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Modification of Terms and Conditions</h2>
|
||||
<p>We reserve the right to modify these Terms at any time. Material changes will be communicated via email or notice on the Website. Continued use of the Service implies acceptance of the updated Terms.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Applicable Law and Jurisdiction</h2>
|
||||
<p>These Terms are governed by the laws of Hong Kong. Any disputes arising from the interpretation or execution of these Terms shall be submitted to the courts of Hong Kong, unless another jurisdiction is mandated by applicable law.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Contact</h2>
|
||||
<p>For legal or operational inquiries, please contact: <a href="mailto:contact@vionow.com">contact@vionow.com</a></p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Cookies Policy</h2>
|
||||
<p>Our Website uses cookies to improve the user experience. Cookies are small text files stored on your device that allow us to collect information about browsing behavior and usage of the Website.</p>
|
||||
<h3>Types of Cookies Used</h3>
|
||||
<p>We only use essential and analytics cookies that are exempt from requiring consent under applicable law:</p>
|
||||
<ul>
|
||||
<li><strong>purecookieDismiss:</strong> Records whether the user has dismissed the cookie banner. Required for proper functioning.</li>
|
||||
<li><strong>__gsas, _ga_GMVSXDG494, _ga:</strong> Google Analytics 4 cookies used for aggregate statistical insights. These cookies do not log or store IP addresses.</li>
|
||||
</ul>
|
||||
<p>Cookies are not used for advertising or profiling purposes. Users may configure their browser settings to manage or delete cookies.</p>
|
||||
</section>
|
||||
|
||||
<section class="conclusion">
|
||||
<p>This document constitutes the entire agreement between the User and Vionow with respect to the use of the Service and supersedes any prior agreements or communications.</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.terms-of-service {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.terms-card {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.terms-content {
|
||||
padding: 20px;
|
||||
line-height: 1.8;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.language-section {
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
|
||||
.language-section:last-child {
|
||||
margin-bottom: 0;
|
||||
padding-top: 40px;
|
||||
border-top: 2px solid #e4e7ed;
|
||||
}
|
||||
|
||||
.language-section h1 {
|
||||
font-size: 28px;
|
||||
color: #303133;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 2px solid #409EFF;
|
||||
}
|
||||
|
||||
.language-section h2 {
|
||||
font-size: 22px;
|
||||
color: #409EFF;
|
||||
margin-top: 30px;
|
||||
margin-bottom: 15px;
|
||||
padding-left: 10px;
|
||||
border-left: 4px solid #409EFF;
|
||||
}
|
||||
|
||||
.language-section h3 {
|
||||
font-size: 18px;
|
||||
color: #606266;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.intro {
|
||||
font-size: 16px;
|
||||
color: #606266;
|
||||
margin-bottom: 30px;
|
||||
padding: 15px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
section {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
section p {
|
||||
margin-bottom: 12px;
|
||||
color: #606266;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
section ul {
|
||||
margin: 15px 0;
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
section ul li {
|
||||
margin-bottom: 10px;
|
||||
color: #606266;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
section ul ul {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
section a {
|
||||
color: #409EFF;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
section a:hover {
|
||||
color: #66b1ff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.conclusion {
|
||||
margin-top: 40px;
|
||||
padding: 20px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
border-left: 4px solid #67c23a;
|
||||
}
|
||||
|
||||
.conclusion p {
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.terms-of-service {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.terms-content {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.language-section h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.language-section h2 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.language-section h3 {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
<template>
|
||||
<div class="test-page">
|
||||
<h1>测试页面</h1>
|
||||
<p>如果你能看到这个页面,说明Vue组件能正常渲染</p>
|
||||
<el-button type="primary" @click="testClick">测试按钮</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const testClick = () => {
|
||||
ElMessage.success('按钮点击成功!')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.test-page {
|
||||
padding: 50px;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
}
|
||||
</style>
|
||||
@@ -2,7 +2,9 @@
|
||||
<div class="text-to-video-page">
|
||||
<!-- 左侧导航栏 -->
|
||||
<aside class="sidebar">
|
||||
<div class="logo">logo</div>
|
||||
<div class="logo">
|
||||
<img src="/images/backgrounds/logo.svg" alt="Logo" />
|
||||
</div>
|
||||
<nav class="nav-menu">
|
||||
<div class="nav-item" @click="goToProfile">
|
||||
<el-icon><User /></el-icon>
|
||||
@@ -38,7 +40,7 @@
|
||||
<!-- 顶部用户信息卡片 -->
|
||||
<div class="user-info-card">
|
||||
<div class="user-avatar">
|
||||
<div class="avatar-placeholder">||</div>
|
||||
<img src="/images/backgrounds/avatar-default.svg" alt="用户头像" class="avatar-image" />
|
||||
</div>
|
||||
<div class="user-details">
|
||||
<div class="username">mingzi_FBx7foZYDS7inLQb</div>
|
||||
@@ -86,7 +88,7 @@
|
||||
</div>
|
||||
<div class="work-info">
|
||||
<div class="work-title">{{ work.prompt || work.title || '文生视频' }}</div>
|
||||
<div class="work-meta">{{ work.taskId || work.id }} · {{ formatSize(work) }}</div>
|
||||
<div class="work-meta">{{ work.date || '未知日期' }} · {{ work.taskId || work.id }} · {{ formatSize(work) }}</div>
|
||||
</div>
|
||||
<div class="work-actions" v-if="index === 0">
|
||||
<el-button type="primary" class="create-similar-btn" @click.stop="goToCreate(work)">做同款</el-button>
|
||||
@@ -265,7 +267,8 @@ const loadTasks = async () => {
|
||||
title: task.prompt || '文生视频',
|
||||
text: task.prompt || '文生视频',
|
||||
category: '文生视频',
|
||||
createTime: task.createdAt ? new Date(task.createdAt).toLocaleString('zh-CN') : ''
|
||||
createTime: task.createdAt ? new Date(task.createdAt).toLocaleString('zh-CN') : '',
|
||||
date: task.createdAt ? new Date(task.createdAt).toLocaleDateString('zh-CN') : '未知日期'
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -293,7 +296,7 @@ onMounted(() => {
|
||||
/* 左侧导航栏 */
|
||||
.sidebar {
|
||||
width: 280px !important;
|
||||
background: #1a1a1a !important;
|
||||
background: #000000 !important;
|
||||
padding: 24px 0 !important;
|
||||
border-right: 1px solid #1a1a1a !important;
|
||||
flex-shrink: 0 !important;
|
||||
@@ -304,9 +307,14 @@ onMounted(() => {
|
||||
|
||||
.logo {
|
||||
padding: 0 24px 32px;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.logo img {
|
||||
height: 40px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
@@ -402,18 +410,17 @@ onMounted(() => {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
background: #000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 2px solid #333;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
color: #fff;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 2px;
|
||||
.avatar-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.user-details {
|
||||
@@ -564,21 +571,7 @@ onMounted(() => {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.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-overlay 和 .overlay-text 已移除:不再使用 */
|
||||
|
||||
.work-info {
|
||||
padding: 16px;
|
||||
|
||||
@@ -8,16 +8,15 @@
|
||||
</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 class="points-display">
|
||||
<div class="points-icon">
|
||||
<el-icon><Star /></el-icon>
|
||||
</div>
|
||||
<span class="points-number">{{ userStore.availablePoints }}</span>
|
||||
</div>
|
||||
<LanguageSwitcher />
|
||||
<div class="user-avatar" @click="toggleUserMenu" ref="userAvatarRef">
|
||||
👤
|
||||
<img src="/images/backgrounds/avatar-default.svg" alt="用户头像" />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -71,7 +70,7 @@
|
||||
<div class="setting-item">
|
||||
<label>高清模式 (1080P)</label>
|
||||
<div class="hd-setting">
|
||||
<input type="checkbox" v-model="hdMode" class="hd-switch">
|
||||
<el-switch v-model="hdMode" />
|
||||
<span class="cost-text">开启消耗20积分</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -303,6 +302,7 @@ import { User, VideoCamera, Star, Setting, SwitchButton } from '@element-plus/ic
|
||||
import { ElMessage, ElLoading } from 'element-plus'
|
||||
import { optimizePrompt } from '@/api/promptOptimizer'
|
||||
import { getProcessingWorks } from '@/api/userWorks'
|
||||
import LanguageSwitcher from '@/components/LanguageSwitcher.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
@@ -440,9 +440,17 @@ const startGenerate = async () => {
|
||||
inProgress.value = true
|
||||
taskProgress.value = 0
|
||||
taskStatus.value = 'PENDING'
|
||||
|
||||
|
||||
ElMessage.success('任务创建成功,开始处理...')
|
||||
|
||||
|
||||
// 更新用户积分信息(任务创建后积分已被扣除)
|
||||
try {
|
||||
await userStore.fetchCurrentUser()
|
||||
console.log('用户积分已更新')
|
||||
} catch (error) {
|
||||
console.error('更新用户积分失败:', error)
|
||||
}
|
||||
|
||||
// 开始轮询任务状态
|
||||
startPollingTask()
|
||||
} else {
|
||||
@@ -479,7 +487,7 @@ const startPollingTask = () => {
|
||||
console.log('任务进度:', progressData)
|
||||
},
|
||||
// 完成回调
|
||||
(taskData) => {
|
||||
async (taskData) => {
|
||||
inProgress.value = false
|
||||
taskProgress.value = 100
|
||||
taskStatus.value = 'COMPLETED'
|
||||
@@ -491,7 +499,15 @@ const startPollingTask = () => {
|
||||
console.warn('任务完成但未获取到resultUrl')
|
||||
}
|
||||
ElMessage.success('视频生成完成!')
|
||||
|
||||
|
||||
// 更新用户积分信息
|
||||
try {
|
||||
await userStore.fetchCurrentUser()
|
||||
console.log('用户积分已更新')
|
||||
} catch (error) {
|
||||
console.error('更新用户积分失败:', error)
|
||||
}
|
||||
|
||||
// 可以在这里跳转到结果页面或显示结果
|
||||
console.log('任务完成:', taskData)
|
||||
},
|
||||
@@ -932,69 +948,50 @@ onUnmounted(() => {
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.credits-info {
|
||||
.points-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
gap: 8px;
|
||||
padding: 6px 12px;
|
||||
background: rgba(64, 158, 255, 0.1);
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(64, 158, 255, 0.3);
|
||||
}
|
||||
|
||||
.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%;
|
||||
.points-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: #409EFF;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.4);
|
||||
}
|
||||
|
||||
.points-number {
|
||||
color: #409EFF;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.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;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.user-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.user-avatar:hover {
|
||||
@@ -1201,13 +1198,6 @@ onUnmounted(() => {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.hd-switch {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
accent-color: #3b82f6;
|
||||
}
|
||||
|
||||
.cost-text {
|
||||
font-size: 13px;
|
||||
color: #9ca3af;
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
<!-- 左侧导航栏 -->
|
||||
<aside class="sidebar">
|
||||
<!-- Logo -->
|
||||
<div class="logo">logo</div>
|
||||
<div class="logo">
|
||||
<img src="/images/backgrounds/logo.svg" alt="Logo" />
|
||||
</div>
|
||||
|
||||
<!-- 导航菜单 -->
|
||||
<nav class="nav-menu">
|
||||
@@ -24,6 +26,22 @@
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<main class="main-content">
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading-container">
|
||||
<el-icon class="is-loading" :size="40"><Loading /></el-icon>
|
||||
<p>加载中...</p>
|
||||
</div>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<div v-else-if="error" class="error-container">
|
||||
<el-icon :size="60" color="#f56c6c"><CircleClose /></el-icon>
|
||||
<p>{{ error }}</p>
|
||||
<el-button type="primary" @click="loadVideoData">重试</el-button>
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 正常内容 -->
|
||||
<template v-else>
|
||||
<!-- 左侧视频播放器区域 -->
|
||||
<div class="video-player-section">
|
||||
<div class="video-container">
|
||||
@@ -39,10 +57,7 @@
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
|
||||
<!-- 视频文字叠加 -->
|
||||
<div class="video-overlay" v-if="videoData.overlayText">
|
||||
<div class="overlay-text">{{ videoData.overlayText }}</div>
|
||||
</div>
|
||||
<!-- 视频文字叠加 已移除(用户要求) -->
|
||||
|
||||
<!-- 播放控制栏 -->
|
||||
<div class="video-controls">
|
||||
@@ -92,7 +107,7 @@
|
||||
<div class="sidebar-header">
|
||||
<div class="user-info">
|
||||
<div class="avatar">
|
||||
<el-icon><User /></el-icon>
|
||||
<img src="/images/backgrounds/avatar-default.svg" alt="用户头像" class="avatar-image" />
|
||||
</div>
|
||||
<div class="username">{{ videoData.username }}</div>
|
||||
</div>
|
||||
@@ -107,9 +122,9 @@
|
||||
<div class="tab">文生视频</div>
|
||||
</div>
|
||||
|
||||
<!-- 描述区域 -->
|
||||
<!-- 提示词区域 -->
|
||||
<div class="description-section">
|
||||
<h3 class="section-title">描述</h3>
|
||||
<h3 class="section-title">提示词</h3>
|
||||
<p class="description-text">{{ videoData.description }}</p>
|
||||
</div>
|
||||
|
||||
@@ -148,6 +163,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
@@ -156,18 +172,21 @@
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
User as VideoPlay,
|
||||
User as VideoPause,
|
||||
User as FullScreen,
|
||||
User as Share,
|
||||
User as Download,
|
||||
User as Delete,
|
||||
import {
|
||||
User as VideoPlay,
|
||||
User as VideoPause,
|
||||
User as FullScreen,
|
||||
User as Share,
|
||||
User as Download,
|
||||
User as Delete,
|
||||
User,
|
||||
User as Compass,
|
||||
Document,
|
||||
Close
|
||||
Close,
|
||||
Loading,
|
||||
CircleClose
|
||||
} from '@element-plus/icons-vue'
|
||||
import { getWorkDetail } from '@/api/userWorks'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -179,47 +198,85 @@ const currentTime = ref(0)
|
||||
const duration = ref(0)
|
||||
const progressPercent = ref(0)
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(true)
|
||||
const error = ref(null)
|
||||
|
||||
// 视频数据
|
||||
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: '',
|
||||
username: '',
|
||||
title: '',
|
||||
description: '',
|
||||
createTime: '',
|
||||
duration: '',
|
||||
resolution: '',
|
||||
category: '',
|
||||
aspectRatio: '',
|
||||
videoUrl: '',
|
||||
cover: ''
|
||||
})
|
||||
|
||||
// 根据ID获取视频数据
|
||||
const getVideoData = (id) => {
|
||||
// 根据ID获取分类信息
|
||||
const videoConfigs = {
|
||||
'2995000000001': { category: '参考图', title: '图片作品 #1' },
|
||||
'2995000000002': { category: '参考图', title: '图片作品 #2' },
|
||||
'2995000000003': { category: '文生视频', title: '视频作品 #3' },
|
||||
'2995000000004': { category: '图生视频', title: '视频作品 #4' }
|
||||
// 处理URL,确保相对路径正确
|
||||
const processUrl = (url) => {
|
||||
if (!url) return null
|
||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
return url
|
||||
}
|
||||
|
||||
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'
|
||||
if (url.startsWith('/')) {
|
||||
return url
|
||||
}
|
||||
return '/' + url
|
||||
}
|
||||
|
||||
// 获取视频数据
|
||||
const loadVideoData = async () => {
|
||||
const videoId = route.params.id
|
||||
if (!videoId) {
|
||||
error.value = '缺少视频ID'
|
||||
loading.value = false
|
||||
ElMessage.error('缺少视频ID')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await getWorkDetail(videoId)
|
||||
|
||||
if (response.data.success) {
|
||||
const work = response.data.data
|
||||
|
||||
// 转换数据格式
|
||||
const resultUrl = processUrl(work.resultUrl)
|
||||
const thumbnailUrl = processUrl(work.thumbnailUrl)
|
||||
|
||||
videoData.value = {
|
||||
id: work.id?.toString() || '',
|
||||
username: work.username || work.user?.username || '未知用户',
|
||||
title: work.title || work.prompt || '未命名作品',
|
||||
description: work.prompt || work.description || '暂无提示词',
|
||||
createTime: work.createdAt ? new Date(work.createdAt).toLocaleString('zh-CN') : '',
|
||||
date: work.createdAt ? new Date(work.createdAt).toLocaleDateString('zh-CN') : '',
|
||||
duration: work.duration || work.videoDuration || work.length || 5,
|
||||
resolution: work.quality || work.resolution || '1080p',
|
||||
category: work.workType === 'TEXT_TO_VIDEO' ? '文生视频' :
|
||||
work.workType === 'IMAGE_TO_VIDEO' ? '图生视频' :
|
||||
work.workType === 'STORYBOARD_VIDEO' ? '分镜视频' : '未知',
|
||||
aspectRatio: work.aspectRatio || work.ratio || work.aspect || '16:9',
|
||||
videoUrl: resultUrl || thumbnailUrl || '/images/backgrounds/welcome.jpg',
|
||||
cover: thumbnailUrl || resultUrl || '/images/backgrounds/welcome.jpg'
|
||||
}
|
||||
|
||||
error.value = null
|
||||
} else {
|
||||
throw new Error(response.data.message || '获取作品详情失败')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载视频数据失败:', err)
|
||||
error.value = err.message || '加载作品详情失败'
|
||||
ElMessage.error('加载作品详情失败: ' + (err.message || '未知错误'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,13 +379,10 @@ const handlePause = () => {
|
||||
isPlaying.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 根据路由参数更新视频数据
|
||||
const videoId = route.params.id
|
||||
if (videoId) {
|
||||
videoData.value = getVideoData(videoId)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// 加载视频数据
|
||||
await loadVideoData()
|
||||
|
||||
if (videoPlayer.value) {
|
||||
videoPlayer.value.addEventListener('play', handlePlay)
|
||||
videoPlayer.value.addEventListener('pause', handlePause)
|
||||
@@ -355,7 +409,7 @@ onUnmounted(() => {
|
||||
/* 左侧导航栏 */
|
||||
.sidebar {
|
||||
width: 280px;
|
||||
background: #1a1a1a;
|
||||
background: #000000;
|
||||
padding: 24px 0;
|
||||
border-right: 1px solid #1a1a1a;
|
||||
flex-shrink: 0;
|
||||
@@ -365,9 +419,14 @@ onUnmounted(() => {
|
||||
|
||||
.logo {
|
||||
padding: 0 24px 32px;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.logo img {
|
||||
height: 40px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
@@ -411,6 +470,40 @@ onUnmounted(() => {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.loading-container p {
|
||||
font-size: 16px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
/* 错误状态 */
|
||||
.error-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.error-container p {
|
||||
font-size: 16px;
|
||||
color: #f56c6c;
|
||||
max-width: 400px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 左侧视频播放器区域 */
|
||||
.video-player-section {
|
||||
flex: 2;
|
||||
@@ -436,20 +529,7 @@ onUnmounted(() => {
|
||||
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 overlay 样式已移除(不再使用 overlayText) */
|
||||
|
||||
/* 视频控制栏 */
|
||||
.video-controls {
|
||||
@@ -595,13 +675,17 @@ onUnmounted(() => {
|
||||
.avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: #409eff;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.avatar-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.username {
|
||||
|
||||
@@ -3,14 +3,16 @@
|
||||
<!-- 导航栏 -->
|
||||
<header class="navbar">
|
||||
<div class="navbar-content">
|
||||
<div class="logo">Logo</div>
|
||||
<div class="logo">
|
||||
<img src="/images/backgrounds/logo.svg" alt="Logo" />
|
||||
</div>
|
||||
<nav class="nav-links">
|
||||
<a href="#" class="nav-link" @click="scrollToSection('features')">文生视频</a>
|
||||
<a href="#" class="nav-link" @click="scrollToSection('features')">图生视频</a>
|
||||
<a href="#" class="nav-link" @click="scrollToSection('features')">分镜视频</a>
|
||||
<a href="#" class="nav-link" @click="scrollToSection('features')">订阅套餐</a>
|
||||
<a href="#" class="nav-link" @click.prevent="goToTextToVideo">{{ $t('welcome.textToVideo') }}</a>
|
||||
<a href="#" class="nav-link" @click.prevent="goToImageToVideo">{{ $t('welcome.imageToVideo') }}</a>
|
||||
<a href="#" class="nav-link" @click.prevent="goToStoryboardVideo">{{ $t('welcome.storyboardVideo') }}</a>
|
||||
<a href="#" class="nav-link" @click="scrollToSection('features')">{{ $t('welcome.pricing') }}</a>
|
||||
</nav>
|
||||
<button class="nav-button" @click="goToLogin">开始体验</button>
|
||||
<button class="nav-button" @click="goToLogin">{{ $t('welcome.startExperience') }}</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -18,39 +20,39 @@
|
||||
<main class="content">
|
||||
<h1 class="title">
|
||||
<span class="title-line">
|
||||
<span class="bright-text">智创</span><span class="fade-text">无限,</span>
|
||||
<span class="bright-text">{{ $t('welcome.title1') }}</span><span class="fade-text">{{ $t('welcome.title2') }}</span>
|
||||
</span>
|
||||
<span class="title-line">
|
||||
<span class="bright-text">灵感</span><span class="fade-text">变现。</span>
|
||||
<span class="bright-text">{{ $t('welcome.title3') }}</span><span class="fade-text">{{ $t('welcome.title4') }}</span>
|
||||
</span>
|
||||
</h1>
|
||||
<p class="subtitle">使用邮箱验证码登录,安全便捷</p>
|
||||
<button class="main-button" @click="goToLogin">立即体验</button>
|
||||
<p class="subtitle">{{ $t('welcome.subtitle') }}</p>
|
||||
<button class="main-button" @click="goToLogin">{{ $t('welcome.tryNow') }}</button>
|
||||
</main>
|
||||
|
||||
<!-- 功能说明 -->
|
||||
<section id="features" class="features-section">
|
||||
<div class="features-container">
|
||||
<h2 class="features-title">核心功能</h2>
|
||||
<h2 class="features-title">{{ $t('welcome.coreFeatures') }}</h2>
|
||||
<div class="features-grid">
|
||||
<div class="feature-card">
|
||||
<h3>文生视频</h3>
|
||||
<p>输入文字描述,AI自动生成高质量视频内容</p>
|
||||
<div class="feature-card" @click="goToTextToVideo" style="cursor: pointer;">
|
||||
<h3>{{ $t('welcome.textToVideo') }}</h3>
|
||||
<p>{{ $t('welcome.textToVideoDesc') }}</p>
|
||||
</div>
|
||||
<div class="feature-card" @click="goToImageToVideo" style="cursor: pointer;">
|
||||
<h3>{{ $t('welcome.imageToVideo') }}</h3>
|
||||
<p>{{ $t('welcome.imageToVideoDesc') }}</p>
|
||||
</div>
|
||||
<div class="feature-card" @click="goToStoryboardVideo" style="cursor: pointer;">
|
||||
<h3>{{ $t('welcome.storyboardVideo') }}</h3>
|
||||
<p>{{ $t('welcome.storyboardVideoDesc') }}</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<h3>图生视频</h3>
|
||||
<p>上传图片,AI智能分析并生成动态视频</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<h3>分镜视频</h3>
|
||||
<p>专业分镜制作,打造电影级视频效果</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<h3>订阅套餐</h3>
|
||||
<p>灵活的价格方案,满足不同创作需求</p>
|
||||
<h3>{{ $t('welcome.pricing') }}</h3>
|
||||
<p>{{ $t('welcome.pricingDesc') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button class="features-button" @click="goToLogin">开始创作</button>
|
||||
<button class="features-button" @click="goToLogin">{{ $t('welcome.startCreating') }}</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -66,6 +68,21 @@ const goToLogin = () => {
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
// 跳转到文生视频页面
|
||||
const goToTextToVideo = () => {
|
||||
router.push('/text-to-video/create')
|
||||
}
|
||||
|
||||
// 跳转到图生视频页面
|
||||
const goToImageToVideo = () => {
|
||||
router.push('/image-to-video/create')
|
||||
}
|
||||
|
||||
// 跳转到分镜视频页面
|
||||
const goToStoryboardVideo = () => {
|
||||
router.push('/storyboard-video/create')
|
||||
}
|
||||
|
||||
// 滚动到功能说明部分
|
||||
const scrollToSection = (sectionId) => {
|
||||
const element = document.getElementById(sectionId)
|
||||
@@ -112,9 +129,13 @@ const scrollToSection = (sectionId) => {
|
||||
}
|
||||
|
||||
.logo {
|
||||
color: white;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo img {
|
||||
height: 35px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
|
||||
@@ -9,16 +9,23 @@ export default defineConfig({
|
||||
'@': resolve(__dirname, 'src')
|
||||
}
|
||||
},
|
||||
// 生产/开发环境配置
|
||||
base: process.env.NODE_ENV === 'production' ? '/' : '/',
|
||||
|
||||
// 开发服务器配置
|
||||
server: {
|
||||
port: 5173,
|
||||
port: 8081,
|
||||
host: '0.0.0.0', // 允许外部访问
|
||||
allowedHosts: true, // 允许所有主机访问
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8080',
|
||||
// 开发时代理到本地后端(统一为 localhost:8080)
|
||||
target: process.env.VITE_APP_API_URL || 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
// 确保后端返回的 Set-Cookie 可被前端域接收与发送
|
||||
// 后端服务器路径已经包含 /api,所以不需要 rewrite
|
||||
// 前端请求 /api/xxx 会转发到 http://localhost:8080/api/xxx
|
||||
// 调试时将 cookie 域改写为 localhost
|
||||
cookieDomainRewrite: 'localhost',
|
||||
cookiePathRewrite: '/',
|
||||
configure: (proxy, _options) => {
|
||||
@@ -39,8 +46,32 @@ export default defineConfig({
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 生产环境构建配置
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
assetsDir: 'assets'
|
||||
assetsDir: 'static',
|
||||
// 代码分割优化
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
'vue-vendor': ['vue', 'vue-router', 'pinia'],
|
||||
'element-plus': ['element-plus', '@element-plus/icons-vue'],
|
||||
'utils': ['axios']
|
||||
}
|
||||
}
|
||||
},
|
||||
// 生产环境移除 console
|
||||
// 注意:如果使用 terser,需要安装: npm install -D terser
|
||||
// 暂时使用 esbuild(默认,更快)
|
||||
minify: 'esbuild',
|
||||
// terserOptions: {
|
||||
// compress: {
|
||||
// drop_console: true,
|
||||
// drop_debugger: true
|
||||
// }
|
||||
// },
|
||||
// 块大小警告限制
|
||||
chunkSizeWarningLimit: 1000
|
||||
}
|
||||
})
|
||||
|
||||
34
demo/frontend/vue.config.js
Normal file
34
demo/frontend/vue.config.js
Normal file
@@ -0,0 +1,34 @@
|
||||
const { defineConfig } = require('@vue/cli-service')
|
||||
|
||||
module.exports = defineConfig({
|
||||
transpileDependencies: true,
|
||||
|
||||
// 生产/开发环境配置
|
||||
publicPath: process.env.NODE_ENV === 'production' ? '/' : '/',
|
||||
outputDir: 'dist',
|
||||
assetsDir: 'static',
|
||||
|
||||
// 开发服务器配置
|
||||
devServer: {
|
||||
port: 8081,
|
||||
proxy: {
|
||||
'/api': {
|
||||
// 开发时代理到本地后端(统一为 localhost:8080)
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true
|
||||
// 不要移除 /api 前缀,后端路由以 /api 开头
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 生产环境配置
|
||||
configureWebpack: {
|
||||
optimization: {
|
||||
splitChunks: {
|
||||
chunks: 'all'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user