first commit
This commit is contained in:
parent
e2b0aff5f3
commit
a3c0fba87a
1
.env.development
Normal file
1
.env.development
Normal file
@ -0,0 +1 @@
|
||||
VITE_API_BASE_URL = 'https://opencomputing.ai'
|
||||
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
13
index.html
Normal file
13
index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/public/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>开元云元境 - 一站式AI创作平台</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
3653
package-lock.json
generated
Normal file
3653
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
35
package.json
Normal file
35
package.json
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "yuanjing",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@yeger/vue-masonry-wall": "^6.1.0",
|
||||
"antd": "^6.3.3",
|
||||
"axios": "^1.13.6",
|
||||
"element-plus": "^2.13.5",
|
||||
"less": "^4.5.1",
|
||||
"less-loader": "^12.3.1",
|
||||
"pinia": "^3.0.4",
|
||||
"pinia-plugin-persistedstate": "^4.7.1",
|
||||
"router": "^2.2.0",
|
||||
"swiper": "^12.1.2",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^5.0.3",
|
||||
"vue-waterfall-plugin-next": "^3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.4.0",
|
||||
"@types/swiper": "^6.0.0",
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"typescript": "~5.8.3",
|
||||
"vite": "^6.3.5",
|
||||
"vue-tsc": "^2.2.12"
|
||||
}
|
||||
}
|
||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
27
src/App.vue
Normal file
27
src/App.vue
Normal file
@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue';
|
||||
import { useLoginStore } from './store/LoginStore';
|
||||
|
||||
const loginStore = useLoginStore();
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化自动登出计时器
|
||||
loginStore.initAutoLogout();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<router-view>
|
||||
</router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less">
|
||||
*{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
</style>
|
||||
15
src/apis/Model.ts
Normal file
15
src/apis/Model.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import {get, post} from '../utlis/request'
|
||||
|
||||
// 文生视频
|
||||
export const textvideo = <T,D>(data:D):Promise<T> =>post('/api/llmage/vidu_inference.dspy',data)
|
||||
// 获取所有模型
|
||||
export const AllModel = <T,D>(data:D):Promise<T> =>get('/api/llmage/get_type_llms.dspy',data)
|
||||
|
||||
// 查询当前模型价格
|
||||
export const queryModelPrice = <T,D>(data:D):Promise<T> =>post('/api/platformbiz/product_query_price.dspy',data)
|
||||
|
||||
// 获取当天任务列表
|
||||
export const getTodayTask = <T>():Promise<T> =>get('/api/llmage/get_my_asynctasks.dspy')
|
||||
|
||||
// 获取任务状态
|
||||
export const getAsyncTaskStatus = <T,D>(data:D):Promise<T> =>get('/api/llmage/get_asynctask_status.dspy',data)
|
||||
12
src/apis/home.ts
Normal file
12
src/apis/home.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import {get, post} from '../utlis/request'
|
||||
|
||||
// 获取视频列表
|
||||
export const getVideos = <T,D>(data:D):Promise<T> =>get('http://180.76.111.84:9028/v1/feed/list',data)
|
||||
|
||||
// 充值
|
||||
export const recharge = <T,D>(data:D):Promise<T> =>post('/api/unipay/user_recharge.dspy',data)
|
||||
|
||||
// 开户
|
||||
export const openAccount = <T>():Promise<T> =>post('/api/accounting/open_customer_accounts.dspy')
|
||||
// 余额
|
||||
export const balance = <T>():Promise<T> =>get('/api/accounting/mybalance.dspy')
|
||||
5
src/apis/login.ts
Normal file
5
src/apis/login.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import {get, post} from '@/utlis/request'
|
||||
|
||||
export const Code = <T,D>(data:D):Promise<T> =>get('/api/rbac/gen_sms_code.dspy',data)
|
||||
|
||||
export const LoginApi = <T,D>(data:D):Promise<T> =>post('/api/rbac/phone_login.dspy',data)
|
||||
539
src/assets/css/iconfont/demo.css
Normal file
539
src/assets/css/iconfont/demo.css
Normal file
@ -0,0 +1,539 @@
|
||||
/* Logo 字体 */
|
||||
@font-face {
|
||||
font-family: "iconfont logo";
|
||||
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834');
|
||||
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834#iefix') format('embedded-opentype'),
|
||||
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.woff?t=1545807318834') format('woff'),
|
||||
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834') format('truetype'),
|
||||
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont') format('svg');
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-family: "iconfont logo";
|
||||
font-size: 160px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* tabs */
|
||||
.nav-tabs {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-more {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 42px;
|
||||
line-height: 42px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
#tabs {
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
#tabs li {
|
||||
cursor: pointer;
|
||||
width: 100px;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
border-bottom: 2px solid transparent;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin-bottom: -1px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
|
||||
#tabs .active {
|
||||
border-bottom-color: #f00;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.tab-container .content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 页面布局 */
|
||||
.main {
|
||||
padding: 30px 100px;
|
||||
width: 960px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.main .logo {
|
||||
color: #333;
|
||||
text-align: left;
|
||||
margin-bottom: 30px;
|
||||
line-height: 1;
|
||||
height: 110px;
|
||||
margin-top: -50px;
|
||||
overflow: hidden;
|
||||
*zoom: 1;
|
||||
}
|
||||
|
||||
.main .logo a {
|
||||
font-size: 160px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.helps {
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.helps pre {
|
||||
padding: 20px;
|
||||
margin: 10px 0;
|
||||
border: solid 1px #e7e1cd;
|
||||
background-color: #fffdef;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.icon_lists {
|
||||
width: 100% !important;
|
||||
overflow: hidden;
|
||||
*zoom: 1;
|
||||
}
|
||||
|
||||
.icon_lists li {
|
||||
width: 100px;
|
||||
margin-bottom: 10px;
|
||||
margin-right: 20px;
|
||||
text-align: center;
|
||||
list-style: none !important;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.icon_lists li .code-name {
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.icon_lists .icon {
|
||||
display: block;
|
||||
height: 100px;
|
||||
line-height: 100px;
|
||||
font-size: 42px;
|
||||
margin: 10px auto;
|
||||
color: #333;
|
||||
-webkit-transition: font-size 0.25s linear, width 0.25s linear;
|
||||
-moz-transition: font-size 0.25s linear, width 0.25s linear;
|
||||
transition: font-size 0.25s linear, width 0.25s linear;
|
||||
}
|
||||
|
||||
.icon_lists .icon:hover {
|
||||
font-size: 100px;
|
||||
}
|
||||
|
||||
.icon_lists .svg-icon {
|
||||
/* 通过设置 font-size 来改变图标大小 */
|
||||
width: 1em;
|
||||
/* 图标和文字相邻时,垂直对齐 */
|
||||
vertical-align: -0.15em;
|
||||
/* 通过设置 color 来改变 SVG 的颜色/fill */
|
||||
fill: currentColor;
|
||||
/* path 和 stroke 溢出 viewBox 部分在 IE 下会显示
|
||||
normalize.css 中也包含这行 */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.icon_lists li .name,
|
||||
.icon_lists li .code-name {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* markdown 样式 */
|
||||
.markdown {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.markdown img {
|
||||
vertical-align: middle;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.markdown h1 {
|
||||
color: #404040;
|
||||
font-weight: 500;
|
||||
line-height: 40px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.markdown h2,
|
||||
.markdown h3,
|
||||
.markdown h4,
|
||||
.markdown h5,
|
||||
.markdown h6 {
|
||||
color: #404040;
|
||||
margin: 1.6em 0 0.6em 0;
|
||||
font-weight: 500;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.markdown h1 {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.markdown h2 {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.markdown h3 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.markdown h4 {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.markdown h5 {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.markdown h6 {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.markdown hr {
|
||||
height: 1px;
|
||||
border: 0;
|
||||
background: #e9e9e9;
|
||||
margin: 16px 0;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.markdown p {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.markdown>p,
|
||||
.markdown>blockquote,
|
||||
.markdown>.highlight,
|
||||
.markdown>ol,
|
||||
.markdown>ul {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.markdown ul>li {
|
||||
list-style: circle;
|
||||
}
|
||||
|
||||
.markdown>ul li,
|
||||
.markdown blockquote ul>li {
|
||||
margin-left: 20px;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.markdown>ul li p,
|
||||
.markdown>ol li p {
|
||||
margin: 0.6em 0;
|
||||
}
|
||||
|
||||
.markdown ol>li {
|
||||
list-style: decimal;
|
||||
}
|
||||
|
||||
.markdown>ol li,
|
||||
.markdown blockquote ol>li {
|
||||
margin-left: 20px;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.markdown code {
|
||||
margin: 0 3px;
|
||||
padding: 0 5px;
|
||||
background: #eee;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.markdown strong,
|
||||
.markdown b {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.markdown>table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0px;
|
||||
empty-cells: show;
|
||||
border: 1px solid #e9e9e9;
|
||||
width: 95%;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.markdown>table th {
|
||||
white-space: nowrap;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.markdown>table th,
|
||||
.markdown>table td {
|
||||
border: 1px solid #e9e9e9;
|
||||
padding: 8px 16px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.markdown>table th {
|
||||
background: #F7F7F7;
|
||||
}
|
||||
|
||||
.markdown blockquote {
|
||||
font-size: 90%;
|
||||
color: #999;
|
||||
border-left: 4px solid #e9e9e9;
|
||||
padding-left: 0.8em;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.markdown blockquote p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.markdown .anchor {
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.markdown .waiting {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.markdown h1:hover .anchor,
|
||||
.markdown h2:hover .anchor,
|
||||
.markdown h3:hover .anchor,
|
||||
.markdown h4:hover .anchor,
|
||||
.markdown h5:hover .anchor,
|
||||
.markdown h6:hover .anchor {
|
||||
opacity: 1;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.markdown>br,
|
||||
.markdown>p>br {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
|
||||
.hljs {
|
||||
display: block;
|
||||
background: white;
|
||||
padding: 0.5em;
|
||||
color: #333333;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.hljs-comment,
|
||||
.hljs-meta {
|
||||
color: #969896;
|
||||
}
|
||||
|
||||
.hljs-string,
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-strong,
|
||||
.hljs-emphasis,
|
||||
.hljs-quote {
|
||||
color: #df5000;
|
||||
}
|
||||
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag,
|
||||
.hljs-type {
|
||||
color: #a71d5d;
|
||||
}
|
||||
|
||||
.hljs-literal,
|
||||
.hljs-symbol,
|
||||
.hljs-bullet,
|
||||
.hljs-attribute {
|
||||
color: #0086b3;
|
||||
}
|
||||
|
||||
.hljs-section,
|
||||
.hljs-name {
|
||||
color: #63a35c;
|
||||
}
|
||||
|
||||
.hljs-tag {
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.hljs-title,
|
||||
.hljs-attr,
|
||||
.hljs-selector-id,
|
||||
.hljs-selector-class,
|
||||
.hljs-selector-attr,
|
||||
.hljs-selector-pseudo {
|
||||
color: #795da3;
|
||||
}
|
||||
|
||||
.hljs-addition {
|
||||
color: #55a532;
|
||||
background-color: #eaffea;
|
||||
}
|
||||
|
||||
.hljs-deletion {
|
||||
color: #bd2c00;
|
||||
background-color: #ffecec;
|
||||
}
|
||||
|
||||
.hljs-link {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* 代码高亮 */
|
||||
/* PrismJS 1.15.0
|
||||
https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */
|
||||
/**
|
||||
* prism.js default theme for JavaScript, CSS and HTML
|
||||
* Based on dabblet (http://dabblet.com)
|
||||
* @author Lea Verou
|
||||
*/
|
||||
code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
color: black;
|
||||
background: none;
|
||||
text-shadow: 0 1px white;
|
||||
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
word-wrap: normal;
|
||||
line-height: 1.5;
|
||||
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
|
||||
-webkit-hyphens: none;
|
||||
-moz-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
hyphens: none;
|
||||
}
|
||||
|
||||
pre[class*="language-"]::-moz-selection,
|
||||
pre[class*="language-"] ::-moz-selection,
|
||||
code[class*="language-"]::-moz-selection,
|
||||
code[class*="language-"] ::-moz-selection {
|
||||
text-shadow: none;
|
||||
background: #b3d4fc;
|
||||
}
|
||||
|
||||
pre[class*="language-"]::selection,
|
||||
pre[class*="language-"] ::selection,
|
||||
code[class*="language-"]::selection,
|
||||
code[class*="language-"] ::selection {
|
||||
text-shadow: none;
|
||||
background: #b3d4fc;
|
||||
}
|
||||
|
||||
@media print {
|
||||
|
||||
code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
text-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
pre[class*="language-"] {
|
||||
padding: 1em;
|
||||
margin: .5em 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
:not(pre)>code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
background: #f5f2f0;
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
:not(pre)>code[class*="language-"] {
|
||||
padding: .1em;
|
||||
border-radius: .3em;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.token.comment,
|
||||
.token.prolog,
|
||||
.token.doctype,
|
||||
.token.cdata {
|
||||
color: slategray;
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.namespace {
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
.token.property,
|
||||
.token.tag,
|
||||
.token.boolean,
|
||||
.token.number,
|
||||
.token.constant,
|
||||
.token.symbol,
|
||||
.token.deleted {
|
||||
color: #905;
|
||||
}
|
||||
|
||||
.token.selector,
|
||||
.token.attr-name,
|
||||
.token.string,
|
||||
.token.char,
|
||||
.token.builtin,
|
||||
.token.inserted {
|
||||
color: #690;
|
||||
}
|
||||
|
||||
.token.operator,
|
||||
.token.entity,
|
||||
.token.url,
|
||||
.language-css .token.string,
|
||||
.style .token.string {
|
||||
color: #9a6e3a;
|
||||
background: hsla(0, 0%, 100%, .5);
|
||||
}
|
||||
|
||||
.token.atrule,
|
||||
.token.attr-value,
|
||||
.token.keyword {
|
||||
color: #07a;
|
||||
}
|
||||
|
||||
.token.function,
|
||||
.token.class-name {
|
||||
color: #DD4A68;
|
||||
}
|
||||
|
||||
.token.regex,
|
||||
.token.important,
|
||||
.token.variable {
|
||||
color: #e90;
|
||||
}
|
||||
|
||||
.token.important,
|
||||
.token.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.token.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.token.entity {
|
||||
cursor: help;
|
||||
}
|
||||
579
src/assets/css/iconfont/demo_index.html
Normal file
579
src/assets/css/iconfont/demo_index.html
Normal file
@ -0,0 +1,579 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>iconfont Demo</title>
|
||||
<link rel="shortcut icon" href="//img.alicdn.com/imgextra/i4/O1CN01Z5paLz1O0zuCC7osS_!!6000000001644-55-tps-83-82.svg" type="image/x-icon"/>
|
||||
<link rel="icon" type="image/svg+xml" href="//img.alicdn.com/imgextra/i4/O1CN01Z5paLz1O0zuCC7osS_!!6000000001644-55-tps-83-82.svg"/>
|
||||
<link rel="stylesheet" href="https://g.alicdn.com/thx/cube/1.3.2/cube.min.css">
|
||||
<link rel="stylesheet" href="demo.css">
|
||||
<link rel="stylesheet" href="iconfont.css">
|
||||
<script src="iconfont.js"></script>
|
||||
<!-- jQuery -->
|
||||
<script src="https://a1.alicdn.com/oss/uploads/2018/12/26/7bfddb60-08e8-11e9-9b04-53e73bb6408b.js"></script>
|
||||
<!-- 代码高亮 -->
|
||||
<script src="https://a1.alicdn.com/oss/uploads/2018/12/26/a3f714d0-08e6-11e9-8a15-ebf944d7534c.js"></script>
|
||||
<style>
|
||||
.main .logo {
|
||||
margin-top: 0;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.main .logo a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.main .logo .sub-title {
|
||||
margin-left: 0.5em;
|
||||
font-size: 22px;
|
||||
color: #fff;
|
||||
background: linear-gradient(-45deg, #3967FF, #B500FE);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="main">
|
||||
<h1 class="logo"><a href="https://www.iconfont.cn/" title="iconfont 首页" target="_blank">
|
||||
<img width="200" src="https://img.alicdn.com/imgextra/i3/O1CN01Mn65HV1FfSEzR6DKv_!!6000000000514-55-tps-228-59.svg">
|
||||
|
||||
</a></h1>
|
||||
<div class="nav-tabs">
|
||||
<ul id="tabs" class="dib-box">
|
||||
<li class="dib active"><span>Unicode</span></li>
|
||||
<li class="dib"><span>Font class</span></li>
|
||||
<li class="dib"><span>Symbol</span></li>
|
||||
</ul>
|
||||
|
||||
<a href="https://www.iconfont.cn/manage/index?manage_type=myprojects&projectId=5136943" target="_blank" class="nav-more">查看项目</a>
|
||||
|
||||
</div>
|
||||
<div class="tab-container">
|
||||
<div class="content unicode" style="display: block;">
|
||||
<ul class="icon_lists dib-box">
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">余额</div>
|
||||
<div class="code-name">&#xe615;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">支付宝支付</div>
|
||||
<div class="code-name">&#xe634;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">警告</div>
|
||||
<div class="code-name">&#xe84f;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">加</div>
|
||||
<div class="code-name">&#xe716;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">验证码</div>
|
||||
<div class="code-name">&#xe623;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">003_手机号-53</div>
|
||||
<div class="code-name">&#xe61c;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">数字人</div>
|
||||
<div class="code-name">&#xe603;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">音频生成</div>
|
||||
<div class="code-name">&#xe604;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">左</div>
|
||||
<div class="code-name">&#xe622;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">右</div>
|
||||
<div class="code-name">&#xe60f;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">img2video</div>
|
||||
<div class="code-name">&#xe600;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">image-edit</div>
|
||||
<div class="code-name">&#xe601;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">无限画布</div>
|
||||
<div class="code-name">&#xe602;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">图片生成</div>
|
||||
<div class="code-name">&#xe636;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">首页</div>
|
||||
<div class="code-name">&#xe62e;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">个人中心</div>
|
||||
<div class="code-name">&#xe63c;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">发现</div>
|
||||
<div class="code-name">&#xe605;</div>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
<div class="article markdown">
|
||||
<h2 id="unicode-">Unicode 引用</h2>
|
||||
<hr>
|
||||
|
||||
<p>Unicode 是字体在网页端最原始的应用方式,特点是:</p>
|
||||
<ul>
|
||||
<li>支持按字体的方式去动态调整图标大小,颜色等等。</li>
|
||||
<li>默认情况下不支持多色,直接添加多色图标会自动去色。</li>
|
||||
</ul>
|
||||
<blockquote>
|
||||
<p>注意:新版 iconfont 支持两种方式引用多色图标:SVG symbol 引用方式和彩色字体图标模式。(使用彩色字体图标需要在「编辑项目」中开启「彩色」选项后并重新生成。)</p>
|
||||
</blockquote>
|
||||
<p>Unicode 使用步骤如下:</p>
|
||||
<h3 id="-font-face">第一步:拷贝项目下面生成的 <code>@font-face</code></h3>
|
||||
<pre><code class="language-css"
|
||||
>@font-face {
|
||||
font-family: 'iconfont';
|
||||
src: url('iconfont.woff2?t=1774432601927') format('woff2'),
|
||||
url('iconfont.woff?t=1774432601927') format('woff'),
|
||||
url('iconfont.ttf?t=1774432601927') format('truetype');
|
||||
}
|
||||
</code></pre>
|
||||
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
|
||||
<pre><code class="language-css"
|
||||
>.iconfont {
|
||||
font-family: "iconfont" !important;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
</code></pre>
|
||||
<h3 id="-">第三步:挑选相应图标并获取字体编码,应用于页面</h3>
|
||||
<pre>
|
||||
<code class="language-html"
|
||||
><span class="iconfont">&#x33;</span>
|
||||
</code></pre>
|
||||
<blockquote>
|
||||
<p>"iconfont" 是你项目下的 font-family。可以通过编辑项目查看,默认是 "iconfont"。</p>
|
||||
</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content font-class">
|
||||
<ul class="icon_lists dib-box">
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont icon-yue"></span>
|
||||
<div class="name">
|
||||
余额
|
||||
</div>
|
||||
<div class="code-name">.icon-yue
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont icon-zhifubaozhifu"></span>
|
||||
<div class="name">
|
||||
支付宝支付
|
||||
</div>
|
||||
<div class="code-name">.icon-zhifubaozhifu
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont icon-jinggao"></span>
|
||||
<div class="name">
|
||||
警告
|
||||
</div>
|
||||
<div class="code-name">.icon-jinggao
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont icon-jia"></span>
|
||||
<div class="name">
|
||||
加
|
||||
</div>
|
||||
<div class="code-name">.icon-jia
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont icon-yanzhengma"></span>
|
||||
<div class="name">
|
||||
验证码
|
||||
</div>
|
||||
<div class="code-name">.icon-yanzhengma
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont icon-a-003_shoujihao-53"></span>
|
||||
<div class="name">
|
||||
003_手机号-53
|
||||
</div>
|
||||
<div class="code-name">.icon-a-003_shoujihao-53
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont icon-a-387845729"></span>
|
||||
<div class="name">
|
||||
数字人
|
||||
</div>
|
||||
<div class="code-name">.icon-a-387845729
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont icon-a-387846145"></span>
|
||||
<div class="name">
|
||||
音频生成
|
||||
</div>
|
||||
<div class="code-name">.icon-a-387846145
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont icon-zuo"></span>
|
||||
<div class="name">
|
||||
左
|
||||
</div>
|
||||
<div class="code-name">.icon-zuo
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont icon-you"></span>
|
||||
<div class="name">
|
||||
右
|
||||
</div>
|
||||
<div class="code-name">.icon-you
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont icon-img2video"></span>
|
||||
<div class="name">
|
||||
img2video
|
||||
</div>
|
||||
<div class="code-name">.icon-img2video
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont icon-image-edit"></span>
|
||||
<div class="name">
|
||||
image-edit
|
||||
</div>
|
||||
<div class="code-name">.icon-image-edit
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont icon-a-386885351"></span>
|
||||
<div class="name">
|
||||
无限画布
|
||||
</div>
|
||||
<div class="code-name">.icon-a-386885351
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont icon-tupianshengcheng"></span>
|
||||
<div class="name">
|
||||
图片生成
|
||||
</div>
|
||||
<div class="code-name">.icon-tupianshengcheng
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont icon-shouye"></span>
|
||||
<div class="name">
|
||||
首页
|
||||
</div>
|
||||
<div class="code-name">.icon-shouye
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont icon-gerenzhongxin"></span>
|
||||
<div class="name">
|
||||
个人中心
|
||||
</div>
|
||||
<div class="code-name">.icon-gerenzhongxin
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont icon-faxian"></span>
|
||||
<div class="name">
|
||||
发现
|
||||
</div>
|
||||
<div class="code-name">.icon-faxian
|
||||
</div>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
<div class="article markdown">
|
||||
<h2 id="font-class-">font-class 引用</h2>
|
||||
<hr>
|
||||
|
||||
<p>font-class 是 Unicode 使用方式的一种变种,主要是解决 Unicode 书写不直观,语意不明确的问题。</p>
|
||||
<p>与 Unicode 使用方式相比,具有如下特点:</p>
|
||||
<ul>
|
||||
<li>相比于 Unicode 语意明确,书写更直观。可以很容易分辨这个 icon 是什么。</li>
|
||||
<li>因为使用 class 来定义图标,所以当要替换图标时,只需要修改 class 里面的 Unicode 引用。</li>
|
||||
</ul>
|
||||
<p>使用步骤如下:</p>
|
||||
<h3 id="-fontclass-">第一步:引入项目下面生成的 fontclass 代码:</h3>
|
||||
<pre><code class="language-html"><link rel="stylesheet" href="./iconfont.css">
|
||||
</code></pre>
|
||||
<h3 id="-">第二步:挑选相应图标并获取类名,应用于页面:</h3>
|
||||
<pre><code class="language-html"><span class="iconfont icon-xxx"></span>
|
||||
</code></pre>
|
||||
<blockquote>
|
||||
<p>"
|
||||
iconfont" 是你项目下的 font-family。可以通过编辑项目查看,默认是 "iconfont"。</p>
|
||||
</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content symbol">
|
||||
<ul class="icon_lists dib-box">
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-yue"></use>
|
||||
</svg>
|
||||
<div class="name">余额</div>
|
||||
<div class="code-name">#icon-yue</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-zhifubaozhifu"></use>
|
||||
</svg>
|
||||
<div class="name">支付宝支付</div>
|
||||
<div class="code-name">#icon-zhifubaozhifu</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-jinggao"></use>
|
||||
</svg>
|
||||
<div class="name">警告</div>
|
||||
<div class="code-name">#icon-jinggao</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-jia"></use>
|
||||
</svg>
|
||||
<div class="name">加</div>
|
||||
<div class="code-name">#icon-jia</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-yanzhengma"></use>
|
||||
</svg>
|
||||
<div class="name">验证码</div>
|
||||
<div class="code-name">#icon-yanzhengma</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-a-003_shoujihao-53"></use>
|
||||
</svg>
|
||||
<div class="name">003_手机号-53</div>
|
||||
<div class="code-name">#icon-a-003_shoujihao-53</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-a-387845729"></use>
|
||||
</svg>
|
||||
<div class="name">数字人</div>
|
||||
<div class="code-name">#icon-a-387845729</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-a-387846145"></use>
|
||||
</svg>
|
||||
<div class="name">音频生成</div>
|
||||
<div class="code-name">#icon-a-387846145</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-zuo"></use>
|
||||
</svg>
|
||||
<div class="name">左</div>
|
||||
<div class="code-name">#icon-zuo</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-you"></use>
|
||||
</svg>
|
||||
<div class="name">右</div>
|
||||
<div class="code-name">#icon-you</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-img2video"></use>
|
||||
</svg>
|
||||
<div class="name">img2video</div>
|
||||
<div class="code-name">#icon-img2video</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-image-edit"></use>
|
||||
</svg>
|
||||
<div class="name">image-edit</div>
|
||||
<div class="code-name">#icon-image-edit</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-a-386885351"></use>
|
||||
</svg>
|
||||
<div class="name">无限画布</div>
|
||||
<div class="code-name">#icon-a-386885351</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-tupianshengcheng"></use>
|
||||
</svg>
|
||||
<div class="name">图片生成</div>
|
||||
<div class="code-name">#icon-tupianshengcheng</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-shouye"></use>
|
||||
</svg>
|
||||
<div class="name">首页</div>
|
||||
<div class="code-name">#icon-shouye</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-gerenzhongxin"></use>
|
||||
</svg>
|
||||
<div class="name">个人中心</div>
|
||||
<div class="code-name">#icon-gerenzhongxin</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-faxian"></use>
|
||||
</svg>
|
||||
<div class="name">发现</div>
|
||||
<div class="code-name">#icon-faxian</div>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
<div class="article markdown">
|
||||
<h2 id="symbol-">Symbol 引用</h2>
|
||||
<hr>
|
||||
|
||||
<p>这是一种全新的使用方式,应该说这才是未来的主流,也是平台目前推荐的用法。相关介绍可以参考这篇<a href="">文章</a>
|
||||
这种用法其实是做了一个 SVG 的集合,与另外两种相比具有如下特点:</p>
|
||||
<ul>
|
||||
<li>支持多色图标了,不再受单色限制。</li>
|
||||
<li>通过一些技巧,支持像字体那样,通过 <code>font-size</code>, <code>color</code> 来调整样式。</li>
|
||||
<li>兼容性较差,支持 IE9+,及现代浏览器。</li>
|
||||
<li>浏览器渲染 SVG 的性能一般,还不如 png。</li>
|
||||
</ul>
|
||||
<p>使用步骤如下:</p>
|
||||
<h3 id="-symbol-">第一步:引入项目下面生成的 symbol 代码:</h3>
|
||||
<pre><code class="language-html"><script src="./iconfont.js"></script>
|
||||
</code></pre>
|
||||
<h3 id="-css-">第二步:加入通用 CSS 代码(引入一次就行):</h3>
|
||||
<pre><code class="language-html"><style>
|
||||
.icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: -0.15em;
|
||||
fill: currentColor;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</code></pre>
|
||||
<h3 id="-">第三步:挑选相应图标并获取类名,应用于页面:</h3>
|
||||
<pre><code class="language-html"><svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-xxx"></use>
|
||||
</svg>
|
||||
</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$('.tab-container .content:first').show()
|
||||
|
||||
$('#tabs li').click(function (e) {
|
||||
var tabContent = $('.tab-container .content')
|
||||
var index = $(this).index()
|
||||
|
||||
if ($(this).hasClass('active')) {
|
||||
return
|
||||
} else {
|
||||
$('#tabs li').removeClass('active')
|
||||
$(this).addClass('active')
|
||||
|
||||
tabContent.hide().eq(index).fadeIn()
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
83
src/assets/css/iconfont/iconfont.css
Normal file
83
src/assets/css/iconfont/iconfont.css
Normal file
@ -0,0 +1,83 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 5136943 */
|
||||
src: url('iconfont.woff2?t=1774432601927') format('woff2'),
|
||||
url('iconfont.woff?t=1774432601927') format('woff'),
|
||||
url('iconfont.ttf?t=1774432601927') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-family: "iconfont" !important;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-yue:before {
|
||||
content: "\e615";
|
||||
}
|
||||
|
||||
.icon-zhifubaozhifu:before {
|
||||
content: "\e634";
|
||||
}
|
||||
|
||||
.icon-jinggao:before {
|
||||
content: "\e84f";
|
||||
}
|
||||
|
||||
.icon-jia:before {
|
||||
content: "\e716";
|
||||
}
|
||||
|
||||
.icon-yanzhengma:before {
|
||||
content: "\e623";
|
||||
}
|
||||
|
||||
.icon-a-003_shoujihao-53:before {
|
||||
content: "\e61c";
|
||||
}
|
||||
|
||||
.icon-a-387845729:before {
|
||||
content: "\e603";
|
||||
}
|
||||
|
||||
.icon-a-387846145:before {
|
||||
content: "\e604";
|
||||
}
|
||||
|
||||
.icon-zuo:before {
|
||||
content: "\e622";
|
||||
}
|
||||
|
||||
.icon-you:before {
|
||||
content: "\e60f";
|
||||
}
|
||||
|
||||
.icon-img2video:before {
|
||||
content: "\e600";
|
||||
}
|
||||
|
||||
.icon-image-edit:before {
|
||||
content: "\e601";
|
||||
}
|
||||
|
||||
.icon-a-386885351:before {
|
||||
content: "\e602";
|
||||
}
|
||||
|
||||
.icon-tupianshengcheng:before {
|
||||
content: "\e636";
|
||||
}
|
||||
|
||||
.icon-shouye:before {
|
||||
content: "\e62e";
|
||||
}
|
||||
|
||||
.icon-gerenzhongxin:before {
|
||||
content: "\e63c";
|
||||
}
|
||||
|
||||
.icon-faxian:before {
|
||||
content: "\e605";
|
||||
}
|
||||
|
||||
1
src/assets/css/iconfont/iconfont.js
Normal file
1
src/assets/css/iconfont/iconfont.js
Normal file
File diff suppressed because one or more lines are too long
128
src/assets/css/iconfont/iconfont.json
Normal file
128
src/assets/css/iconfont/iconfont.json
Normal file
@ -0,0 +1,128 @@
|
||||
{
|
||||
"id": "5136943",
|
||||
"name": "yuanjing",
|
||||
"font_family": "iconfont",
|
||||
"css_prefix_text": "icon-",
|
||||
"description": "",
|
||||
"glyphs": [
|
||||
{
|
||||
"icon_id": "2681721",
|
||||
"name": "余额",
|
||||
"font_class": "yue",
|
||||
"unicode": "e615",
|
||||
"unicode_decimal": 58901
|
||||
},
|
||||
{
|
||||
"icon_id": "1486848",
|
||||
"name": "支付宝支付",
|
||||
"font_class": "zhifubaozhifu",
|
||||
"unicode": "e634",
|
||||
"unicode_decimal": 58932
|
||||
},
|
||||
{
|
||||
"icon_id": "8288853",
|
||||
"name": "警告",
|
||||
"font_class": "jinggao",
|
||||
"unicode": "e84f",
|
||||
"unicode_decimal": 59471
|
||||
},
|
||||
{
|
||||
"icon_id": "16350239",
|
||||
"name": "加",
|
||||
"font_class": "jia",
|
||||
"unicode": "e716",
|
||||
"unicode_decimal": 59158
|
||||
},
|
||||
{
|
||||
"icon_id": "5022863",
|
||||
"name": "验证码",
|
||||
"font_class": "yanzhengma",
|
||||
"unicode": "e623",
|
||||
"unicode_decimal": 58915
|
||||
},
|
||||
{
|
||||
"icon_id": "36126728",
|
||||
"name": "003_手机号-53",
|
||||
"font_class": "a-003_shoujihao-53",
|
||||
"unicode": "e61c",
|
||||
"unicode_decimal": 58908
|
||||
},
|
||||
{
|
||||
"icon_id": "47059243",
|
||||
"name": "数字人",
|
||||
"font_class": "a-387845729",
|
||||
"unicode": "e603",
|
||||
"unicode_decimal": 58883
|
||||
},
|
||||
{
|
||||
"icon_id": "47059246",
|
||||
"name": "音频生成",
|
||||
"font_class": "a-387846145",
|
||||
"unicode": "e604",
|
||||
"unicode_decimal": 58884
|
||||
},
|
||||
{
|
||||
"icon_id": "8483536",
|
||||
"name": "左",
|
||||
"font_class": "zuo",
|
||||
"unicode": "e622",
|
||||
"unicode_decimal": 58914
|
||||
},
|
||||
{
|
||||
"icon_id": "13000530",
|
||||
"name": "右",
|
||||
"font_class": "you",
|
||||
"unicode": "e60f",
|
||||
"unicode_decimal": 58895
|
||||
},
|
||||
{
|
||||
"icon_id": "45643295",
|
||||
"name": "img2video",
|
||||
"font_class": "img2video",
|
||||
"unicode": "e600",
|
||||
"unicode_decimal": 58880
|
||||
},
|
||||
{
|
||||
"icon_id": "46889707",
|
||||
"name": "image-edit",
|
||||
"font_class": "image-edit",
|
||||
"unicode": "e601",
|
||||
"unicode_decimal": 58881
|
||||
},
|
||||
{
|
||||
"icon_id": "47046108",
|
||||
"name": "无限画布",
|
||||
"font_class": "a-386885351",
|
||||
"unicode": "e602",
|
||||
"unicode_decimal": 58882
|
||||
},
|
||||
{
|
||||
"icon_id": "45825147",
|
||||
"name": "图片生成",
|
||||
"font_class": "tupianshengcheng",
|
||||
"unicode": "e636",
|
||||
"unicode_decimal": 58934
|
||||
},
|
||||
{
|
||||
"icon_id": "2675009",
|
||||
"name": "首页",
|
||||
"font_class": "shouye",
|
||||
"unicode": "e62e",
|
||||
"unicode_decimal": 58926
|
||||
},
|
||||
{
|
||||
"icon_id": "5532917",
|
||||
"name": "个人中心",
|
||||
"font_class": "gerenzhongxin",
|
||||
"unicode": "e63c",
|
||||
"unicode_decimal": 58940
|
||||
},
|
||||
{
|
||||
"icon_id": "11200583",
|
||||
"name": "发现",
|
||||
"font_class": "faxian",
|
||||
"unicode": "e605",
|
||||
"unicode_decimal": 58885
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
src/assets/css/iconfont/iconfont.ttf
Normal file
BIN
src/assets/css/iconfont/iconfont.ttf
Normal file
Binary file not shown.
BIN
src/assets/css/iconfont/iconfont.woff
Normal file
BIN
src/assets/css/iconfont/iconfont.woff
Normal file
Binary file not shown.
BIN
src/assets/css/iconfont/iconfont.woff2
Normal file
BIN
src/assets/css/iconfont/iconfont.woff2
Normal file
Binary file not shown.
BIN
src/assets/images/favicon.png
Normal file
BIN
src/assets/images/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
BIN
src/assets/images/login.mp4
Normal file
BIN
src/assets/images/login.mp4
Normal file
Binary file not shown.
BIN
src/assets/images/preparation.png
Normal file
BIN
src/assets/images/preparation.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
262
src/assets/less/Header/header.css
Normal file
262
src/assets/less/Header/header.css
Normal file
@ -0,0 +1,262 @@
|
||||
.header-container {
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
}
|
||||
.header {
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-right: 2rem;
|
||||
background: rgba(10, 14, 20, 0.8);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.header {
|
||||
padding: 0 1rem;
|
||||
height: 70px;
|
||||
}
|
||||
}
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.logo img {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.logo .header-title {
|
||||
font-family: "Microsoft Yahei", "PingFang SC", sans-serif;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
transform: skew(-8deg);
|
||||
-webkit-transform: skew(-12deg);
|
||||
text-shadow: 0 0 10px rgba(0, 255, 204, 0.3);
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.logo .header-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.header-actions {
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
.balance-recharge-combo {
|
||||
height: 44px;
|
||||
padding: 0 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
background: rgba(255, 142, 83, 0.1);
|
||||
border: 1px solid rgba(255, 142, 83, 0.4);
|
||||
border-radius: 22px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.balance-recharge-combo .iconfont {
|
||||
font-size: 16px;
|
||||
color: #ff8e53;
|
||||
}
|
||||
.balance-recharge-combo .balance-text {
|
||||
font-weight: 600;
|
||||
color: #ff8e53;
|
||||
}
|
||||
.balance-recharge-combo .recharge-text {
|
||||
height: 100%;
|
||||
width: 60px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 10px;
|
||||
font-size: 13px;
|
||||
color: #ff8e53;
|
||||
font-weight: 600;
|
||||
}
|
||||
.balance-recharge-combo:hover {
|
||||
background: rgba(255, 142, 83, 0.15);
|
||||
box-shadow: 0 4px 12px rgba(255, 142, 83, 0.2);
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.balance-recharge-combo {
|
||||
height: 36px;
|
||||
padding: 0 14px;
|
||||
font-size: 13px;
|
||||
gap: 6px;
|
||||
}
|
||||
.balance-recharge-combo .recharge-text {
|
||||
padding: 1px 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
.btn {
|
||||
min-width: 120px;
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
background-image: linear-gradient(270deg, #00f2fe 11.11%, #4facfe);
|
||||
border-radius: 24px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
color: #0a1428;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 12px rgba(0, 242, 254, 0.3);
|
||||
}
|
||||
.btn:hover {
|
||||
background-image: linear-gradient(270deg, #4facfe 11.11%, #00f2fe);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(0, 242, 254, 0.4);
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.btn {
|
||||
min-width: 100px;
|
||||
height: 40px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
.user-profile {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
.user-trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 0 20px;
|
||||
height: 44px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 22px;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
.user-trigger:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-color: rgba(0, 255, 204, 0.3);
|
||||
box-shadow: 0 0 15px rgba(0, 255, 204, 0.2);
|
||||
}
|
||||
.user-trigger .user-name {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
max-width: 120px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.user-trigger .dropdown-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
transition: transform 0.3s ease;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
.user-trigger:hover .dropdown-icon {
|
||||
transform: rotate(180deg);
|
||||
color: #00ffcc;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.user-trigger {
|
||||
height: 36px;
|
||||
padding: 0 14px;
|
||||
}
|
||||
.user-trigger .user-name {
|
||||
max-width: 80px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
.user-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
margin-top: 8px;
|
||||
width: 220px;
|
||||
background: rgba(10, 14, 20, 0.95);
|
||||
backdrop-filter: blur(12px);
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
overflow: hidden;
|
||||
z-index: 1000;
|
||||
}
|
||||
.user-dropdown::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: 20px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 8px solid transparent;
|
||||
border-right: 8px solid transparent;
|
||||
border-bottom: 8px solid rgba(10, 14, 20, 0.95);
|
||||
}
|
||||
.dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
color: #fff;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
.dropdown-item:hover {
|
||||
background: rgba(0, 255, 204, 0.1);
|
||||
}
|
||||
.dropdown-item.user-id {
|
||||
justify-content: space-between;
|
||||
font-size: 14px;
|
||||
}
|
||||
.dropdown-item.user-id .item-label {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
.dropdown-item.user-id .item-value {
|
||||
color: #00ffcc;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
.dropdown-item.logout {
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.dropdown-item.logout .item-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
.dropdown-item.logout:hover {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
.dropdown-item.logout:hover .item-icon {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
.dropdown-divider {
|
||||
height: 1px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
margin: 4px 0;
|
||||
}
|
||||
.dropdown-enter-active,
|
||||
.dropdown-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.dropdown-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px) scale(0.95);
|
||||
}
|
||||
.dropdown-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px) scale(0.95);
|
||||
}
|
||||
316
src/assets/less/Header/header.less
Normal file
316
src/assets/less/Header/header.less
Normal file
@ -0,0 +1,316 @@
|
||||
// 头部容器
|
||||
.header-container {
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
// 头部主容器
|
||||
.header {
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-right: 2rem;
|
||||
background: rgba(10, 14, 20, 0.8);
|
||||
backdrop-filter: blur(12px);
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 0 1rem;
|
||||
height: 70px;
|
||||
}
|
||||
}
|
||||
|
||||
// Logo样式
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
img {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-family: "Microsoft Yahei", "PingFang SC", sans-serif;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
transform: skew(-8deg);
|
||||
-webkit-transform: skew(-12deg);
|
||||
text-shadow: 0 0 10px rgba(0, 255, 204, 0.3);
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 操作区
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// 【一体化】余额 + 充值按钮(核心修改)
|
||||
// ==============================================
|
||||
.balance-recharge-combo {
|
||||
height: 44px;
|
||||
padding: 0 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
background: rgba(255, 142, 83, 0.1);
|
||||
border: 1px solid rgba(255, 142, 83, 0.4);
|
||||
border-radius: 22px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
// 图标
|
||||
.iconfont {
|
||||
font-size: 16px;
|
||||
color: #ff8e53;
|
||||
}
|
||||
|
||||
// 余额数字
|
||||
.balance-text {
|
||||
font-weight: 600;
|
||||
color: #ff8e53;
|
||||
}
|
||||
|
||||
// 充值文字
|
||||
.recharge-text {
|
||||
height: 100%;
|
||||
width: 60px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 10px;
|
||||
font-size: 13px;
|
||||
color: #ff8e53;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 142, 83, 0.15);
|
||||
box-shadow: 0 4px 12px rgba(255, 142, 83, 0.2);
|
||||
|
||||
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
height: 36px;
|
||||
padding: 0 14px;
|
||||
font-size: 13px;
|
||||
gap: 6px;
|
||||
|
||||
.recharge-text {
|
||||
padding: 1px 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 登录按钮
|
||||
.btn {
|
||||
min-width: 120px;
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
background-image: linear-gradient(270deg, #00f2fe 11.11%, #4facfe);
|
||||
border-radius: 24px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
color: #0a1428;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 12px rgba(0, 242, 254, 0.3);
|
||||
|
||||
&:hover {
|
||||
background-image: linear-gradient(270deg, #4facfe 11.11%, #00f2fe);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(0, 242, 254, 0.4);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
min-width: 100px;
|
||||
height: 40px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
// 用户个人信息区域
|
||||
.user-profile {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
// 下拉菜单打开时,箭头保持指向上
|
||||
.user-profile.dropdown-open .user-trigger .dropdown-icon {
|
||||
transform: rotate(180deg);
|
||||
color: #00ffcc;
|
||||
}
|
||||
|
||||
// 用户触发区域
|
||||
.user-trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 0 20px;
|
||||
height: 44px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 22px;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid transparent;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-color: rgba(0, 255, 204, 0.3);
|
||||
box-shadow: 0 0 15px rgba(0, 255, 204, 0.2);
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
max-width: 120px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
transition: transform 0.3s ease;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
&:hover .dropdown-icon {
|
||||
transform: rotate(180deg);
|
||||
color: #00ffcc;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
height: 36px;
|
||||
padding: 0 14px;
|
||||
|
||||
.user-name {
|
||||
max-width: 80px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 用户下拉菜单
|
||||
.user-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
margin-top: 8px;
|
||||
width: 220px;
|
||||
background: rgba(10, 14, 20, 0.95);
|
||||
backdrop-filter: blur(12px);
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
overflow: hidden;
|
||||
z-index: 1000;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: 20px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 8px solid transparent;
|
||||
border-right: 8px solid transparent;
|
||||
border-bottom: 8px solid rgba(10, 14, 20, 0.95);
|
||||
}
|
||||
}
|
||||
|
||||
// 下拉菜单项
|
||||
.dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
color: #fff;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 255, 204, 0.1);
|
||||
}
|
||||
|
||||
&.user-id {
|
||||
justify-content: space-between;
|
||||
font-size: 14px;
|
||||
|
||||
.item-label {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.item-value {
|
||||
color: #00ffcc;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
&.logout {
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
|
||||
.item-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #ff6b6b;
|
||||
|
||||
.item-icon {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 下拉菜单分隔线
|
||||
.dropdown-divider {
|
||||
height: 1px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
// 下拉菜单动画
|
||||
.dropdown-enter-active,
|
||||
.dropdown-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.dropdown-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px) scale(0.95);
|
||||
}
|
||||
|
||||
.dropdown-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px) scale(0.95);
|
||||
}
|
||||
144
src/assets/less/Img/ImgBox.css
Normal file
144
src/assets/less/Img/ImgBox.css
Normal file
@ -0,0 +1,144 @@
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.Topcard {
|
||||
background-color: #1a1c20;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
border: 0;
|
||||
}
|
||||
.Bottomcard {
|
||||
background-color: #1a1c20;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
border: 0;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.card-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.card-top img {
|
||||
width: 163px;
|
||||
}
|
||||
.card-top .top-left h2 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
.card-top .top-left .desc {
|
||||
font-size: 14px;
|
||||
margin-top: 4px;
|
||||
color: #ffffff7a;
|
||||
}
|
||||
.card-top .top-left .iconfont {
|
||||
font-size: 12px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
.card-top .top-left .add {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.card-top .top-left .add-item {
|
||||
font-size: 14px;
|
||||
gap: 4px;
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
padding: 6px 12px;
|
||||
background-color: #ffffff0a;
|
||||
}
|
||||
.card-bottom {
|
||||
margin-top: 16px;
|
||||
}
|
||||
:deep(.el-textarea) .el-textarea__inner {
|
||||
border: none !important;
|
||||
background-color: rgba(0, 0, 0, 0.32) !important;
|
||||
box-shadow: none !important;
|
||||
border-radius: 0 !important;
|
||||
padding: 12px 0 !important;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: #fff !important;
|
||||
resize: vertical;
|
||||
outline: none !important;
|
||||
border-radius: 10px !important;
|
||||
color: #fff;
|
||||
backdrop-filter: blur(30px);
|
||||
padding: 14px 16px !important;
|
||||
}
|
||||
:deep(.el-textarea) .el-textarea__inner:focus {
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
outline: none !important;
|
||||
}
|
||||
:deep(.el-textarea) .el-textarea__inner::placeholder {
|
||||
color: #ffffff7a !important;
|
||||
font-size: 14px;
|
||||
}
|
||||
:deep(.el-textarea) .el-input__count {
|
||||
background: transparent;
|
||||
color: #ffffff7a;
|
||||
}
|
||||
:deep(.el-select__wrapper) {
|
||||
width: 100% !important;
|
||||
height: 100%!important;
|
||||
border: none !important;
|
||||
background-color: rgba(0, 0, 0, 0.32) !important;
|
||||
box-shadow: none !important;
|
||||
border-radius: 0 !important;
|
||||
font-size: 14px;
|
||||
color: #fff !important;
|
||||
resize: vertical;
|
||||
outline: none !important;
|
||||
border-radius: 10px !important;
|
||||
color: #fff;
|
||||
}
|
||||
:deep(.el-select__selected-item) {
|
||||
color: #fff !important;
|
||||
}
|
||||
.select {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.select .select-item {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.select .select-item .title {
|
||||
width: 50%;
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
padding-right: 12px;
|
||||
}
|
||||
.select .select-item .content {
|
||||
flex: 1;
|
||||
}
|
||||
.border {
|
||||
height: 1px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
.quantity-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.quantity-buttons .quantity-btn {
|
||||
flex: 1;
|
||||
padding: 8px 0;
|
||||
background-color: rgba(0, 0, 0, 0.32);
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.quantity-buttons .quantity-btn:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.quantity-buttons .quantity-btn.active {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
}
|
||||
177
src/assets/less/Img/ImgBox.less
Normal file
177
src/assets/less/Img/ImgBox.less
Normal file
@ -0,0 +1,177 @@
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.Topcard {
|
||||
background-color: #1a1c20;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.Bottomcard {
|
||||
background-color: #1a1c20;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
border: 0;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.card-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
img {
|
||||
width: 163px;
|
||||
}
|
||||
|
||||
.top-left {
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 14px;
|
||||
margin-top: 4px;
|
||||
color: #ffffff7a;
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-size: 12px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.add {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.add-item {
|
||||
font-size: 14px;
|
||||
gap: 4px;
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
padding: 6px 12px;
|
||||
background-color: #ffffff0a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-bottom {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
:deep(.el-textarea) {
|
||||
|
||||
.el-textarea__inner {
|
||||
|
||||
border: none !important;
|
||||
background-color: rgba(0, 0, 0, 0.32) !important;
|
||||
box-shadow: none !important;
|
||||
border-radius: 0 !important;
|
||||
padding: 12px 0 !important;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: #fff !important;
|
||||
resize: vertical;
|
||||
outline: none !important;
|
||||
border-radius: 10px !important;
|
||||
color: #fff;
|
||||
backdrop-filter: blur(30px);
|
||||
padding: 14px 16px !important;
|
||||
|
||||
&:focus {
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: #ffffff7a !important;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
// 确保没有任何附加的边框或装饰出现
|
||||
.el-input__count {
|
||||
background: transparent;
|
||||
color: #ffffff7a;
|
||||
}
|
||||
}
|
||||
:deep(.el-select__wrapper) {
|
||||
|
||||
width: 100% !important;
|
||||
height: 100%!important;
|
||||
border: none !important;
|
||||
background-color: rgba(0, 0, 0, 0.32) !important;
|
||||
box-shadow: none !important;
|
||||
border-radius: 0 !important;
|
||||
font-size: 14px;
|
||||
color: #fff !important;
|
||||
resize: vertical;
|
||||
outline: none !important;
|
||||
border-radius: 10px !important;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
:deep(.el-select__selected-item) {
|
||||
color: #fff !important;
|
||||
}
|
||||
.select {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.select-item {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.title {
|
||||
width: 50%;
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.border {
|
||||
height: 1px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.quantity-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
.quantity-btn {
|
||||
flex: 1;
|
||||
padding: 8px 0;
|
||||
background-color: rgba(0, 0, 0, 0.32);
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
151
src/assets/less/Login/Login.css
Normal file
151
src/assets/less/Login/Login.css
Normal file
@ -0,0 +1,151 @@
|
||||
.login {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.background-video-stack {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
}
|
||||
.background-video {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
opacity: 1;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.4s ease-in-out;
|
||||
}
|
||||
.form-card {
|
||||
position: relative;
|
||||
left: 25%;
|
||||
z-index: 1;
|
||||
width: 400px;
|
||||
max-width: 90%;
|
||||
padding: 40px 30px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border-radius: 24px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
color: #fff;
|
||||
}
|
||||
.form-top {
|
||||
text-align: center;
|
||||
margin-bottom: 10%;
|
||||
}
|
||||
.form-title {
|
||||
margin-bottom: 8px;
|
||||
font-weight: 400;
|
||||
letter-spacing: 2px;
|
||||
color: #fff;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.form-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
.login-form :deep(.el-input__wrapper) {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.3) inset;
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.login-form :deep(.el-input__wrapper):hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
.login-form :deep(.el-input__wrapper).is-focus {
|
||||
background: rgba(255, 255, 255, 0.35);
|
||||
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.5) inset;
|
||||
}
|
||||
.login-form :deep(.el-input__inner) {
|
||||
color: #fff;
|
||||
}
|
||||
.login-form :deep(.el-input__inner)::placeholder {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
.login-form :deep(.el-input__prefix) {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
.captcha-wrapper {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
.captcha-wrapper .captcha-input {
|
||||
flex: 1;
|
||||
}
|
||||
.glass-btn {
|
||||
background: rgba(255, 255, 255, 0.2) !important;
|
||||
backdrop-filter: blur(10px) !important;
|
||||
-webkit-backdrop-filter: blur(10px) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3) !important;
|
||||
color: #fff !important;
|
||||
font-weight: 400;
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.glass-btn:hover:not(:disabled) {
|
||||
background: rgba(255, 255, 255, 0.35) !important;
|
||||
border-color: rgba(255, 255, 255, 0.5) !important;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.glass-btn:active:not(:disabled) {
|
||||
transform: translateY(0);
|
||||
background: rgba(255, 255, 255, 0.45) !important;
|
||||
}
|
||||
.glass-btn:disabled {
|
||||
background: rgba(255, 255, 255, 0.1) !important;
|
||||
border-color: rgba(255, 255, 255, 0.15) !important;
|
||||
color: rgba(255, 255, 255, 0.5) !important;
|
||||
backdrop-filter: blur(5px) !important;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.captcha-btn {
|
||||
min-width: 160px;
|
||||
font-size: 14px;
|
||||
padding: 12px 16px;
|
||||
height: 40px;
|
||||
}
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
margin-top: 30px;
|
||||
background: rgba(255, 255, 255, 0.25) !important;
|
||||
font-size: 16px;
|
||||
padding: 14px 0;
|
||||
}
|
||||
.form-bottom {
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
margin-top: 20%;
|
||||
}
|
||||
/* 账号选择弹窗样式 */
|
||||
.account-list .account-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
.account-list .account-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.account-list .account-item .account-info .account-name {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.account-list .account-item .account-info .account-phone {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
}
|
||||
.account-list .account-item:hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
183
src/assets/less/Login/Login.less
Normal file
183
src/assets/less/Login/Login.less
Normal file
@ -0,0 +1,183 @@
|
||||
.login {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.background-video-stack {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.background-video {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
opacity: 1;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.4s ease-in-out;
|
||||
}
|
||||
|
||||
.form-card {
|
||||
position: relative;
|
||||
left: 25%;
|
||||
z-index: 1;
|
||||
width: 400px;
|
||||
max-width: 90%;
|
||||
padding: 40px 30px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border-radius: 24px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.form-top {
|
||||
text-align: center;
|
||||
margin-bottom: 10%;
|
||||
}
|
||||
|
||||
.form-title {
|
||||
margin-bottom: 8px;
|
||||
font-weight: 400;
|
||||
letter-spacing: 2px;
|
||||
color: #fff;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.form-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
:deep(.el-input__wrapper) {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.3) inset;
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
&.is-focus {
|
||||
background: rgba(255, 255, 255, 0.35);
|
||||
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.5) inset;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
color: #fff;
|
||||
|
||||
&::placeholder {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-input__prefix) {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
.captcha-wrapper {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
|
||||
.captcha-input {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.glass-btn {
|
||||
background: rgba(255, 255, 255, 0.2) !important;
|
||||
backdrop-filter: blur(10px) !important;
|
||||
-webkit-backdrop-filter: blur(10px) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3) !important;
|
||||
color: #fff !important;
|
||||
font-weight: 400;
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: rgba(255, 255, 255, 0.35) !important;
|
||||
border-color: rgba(255, 255, 255, 0.5) !important;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
transform: translateY(0);
|
||||
background: rgba(255, 255, 255, 0.45) !important;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: rgba(255, 255, 255, 0.1) !important;
|
||||
border-color: rgba(255, 255, 255, 0.15) !important;
|
||||
color: rgba(255, 255, 255, 0.5) !important;
|
||||
backdrop-filter: blur(5px) !important;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.captcha-btn {
|
||||
min-width: 160px;
|
||||
font-size: 14px;
|
||||
padding: 12px 16px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
margin-top: 30px;
|
||||
background: rgba(255, 255, 255, 0.25) !important;
|
||||
font-size: 16px;
|
||||
padding: 14px 0;
|
||||
}
|
||||
|
||||
.form-bottom {
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
margin-top: 20%;
|
||||
}
|
||||
|
||||
/* 账号选择弹窗样式 */
|
||||
.account-list {
|
||||
.account-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.account-info {
|
||||
.account-name {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.account-phone {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
}
|
||||
}
|
||||
278
src/assets/less/RightResult/RightResult.css
Normal file
278
src/assets/less/RightResult/RightResult.css
Normal file
@ -0,0 +1,278 @@
|
||||
.rig {
|
||||
padding: 14px;
|
||||
width: 100%;
|
||||
height: calc(100vh - 100px);
|
||||
border: 0;
|
||||
background-color: #1a1c20;
|
||||
color: #fff;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.rig :deep(.el-card__body) {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
padding: 0 0 12px 0;
|
||||
}
|
||||
.top :deep(.el-select__wrapper) {
|
||||
border: none;
|
||||
background-color: rgba(0, 0, 0, 0.32);
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
}
|
||||
.top :deep(.el-checkbox__label) {
|
||||
color: #fff;
|
||||
}
|
||||
.content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
background-color: #1E1E22;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
}
|
||||
.content::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
.content::-webkit-scrollbar-track {
|
||||
background: #0F0F19;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.content::-webkit-scrollbar-thumb {
|
||||
background: #2A2A32;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
gap: 16px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
.empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
gap: 20px;
|
||||
}
|
||||
.empty img {
|
||||
width: 30%;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.empty .title {
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
/* 任务卡片样式 - 完全对齐参考 */
|
||||
.task-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
.task-item {
|
||||
background: #1A1C20;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.task-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.task-header .task-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
}
|
||||
.task-header .task-title .title-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
.task-header .task-time {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
.task-prompt-wrapper {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.task-prompt-wrapper .prompt-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.task-prompt-wrapper .video-preview {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.task-prompt-wrapper .video-preview .preview-bg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: #343C48;
|
||||
}
|
||||
.task-prompt-wrapper .video-preview .preview-bg::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: var(--bg-image-url);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
opacity: 0.48;
|
||||
filter: blur(30px);
|
||||
z-index: 0;
|
||||
}
|
||||
.task-prompt-wrapper .video-preview .preview-bg .preview-video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.task-prompt-wrapper .prompt-text {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.task-prompt-wrapper .prompt-text .tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 6px;
|
||||
padding: 2px 6px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.task-prompt-wrapper .prompt-text .tag .tag-icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
color: #AB8DFF;
|
||||
}
|
||||
.task-prompt-wrapper .model-info {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.task-prompt-wrapper .model-info .split {
|
||||
color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.video-player {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.video-player .player-container {
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
background: #000;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.video-player .player-container .task-video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
.task-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.task-actions .action-left {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.task-actions .action-left .action-btn {
|
||||
height: 32px;
|
||||
background: #2A2A32;
|
||||
border: none;
|
||||
color: #fff;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.task-actions .action-left .action-btn:hover {
|
||||
background: #3A3A42;
|
||||
}
|
||||
.task-actions .action-left .action-btn .action-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
.task-actions .action-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
background: #2A2A32;
|
||||
border-radius: 8px;
|
||||
padding: 4px;
|
||||
}
|
||||
.task-actions .action-right .icon-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.task-actions .action-right .icon-btn:hover {
|
||||
background: #3A3A42;
|
||||
color: #fff;
|
||||
}
|
||||
.task-actions .action-right .icon-btn svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
/* 未登录样式 */
|
||||
.login-require {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.login-require-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
.login-require-content .message {
|
||||
font-size: 16px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
.login-require-content .login-btn {
|
||||
width: 140px;
|
||||
height: 44px;
|
||||
border-radius: 12px;
|
||||
font-size: 16px;
|
||||
background: linear-gradient(270deg, #94d8ff 11.11%, #e0ddff);
|
||||
border: none;
|
||||
color: #000;
|
||||
}
|
||||
378
src/assets/less/RightResult/RightResult.less
Normal file
378
src/assets/less/RightResult/RightResult.less
Normal file
@ -0,0 +1,378 @@
|
||||
|
||||
.rig {
|
||||
padding: 14px;
|
||||
width: 100%;
|
||||
height: calc(100vh - 100px);
|
||||
border: 0;
|
||||
background-color: #1a1c20;
|
||||
color: #fff;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
padding: 0 0 12px 0;
|
||||
|
||||
:deep(.el-select__wrapper) {
|
||||
border: none;
|
||||
background-color: rgba(0, 0, 0, 0.32);
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox__label) {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
background-color: #1E1E22;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
|
||||
&::-webkit-scrollbar { width: 6px; }
|
||||
&::-webkit-scrollbar-track { background: #0F0F19; border-radius: 3px; }
|
||||
&::-webkit-scrollbar-thumb { background: #2A2A32; border-radius: 3px; }
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
gap: 16px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
gap: 20px;
|
||||
|
||||
img {
|
||||
width: 30%;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.title {
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
/* 任务卡片样式 - 完全对齐参考 */
|
||||
.task-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.task-item {
|
||||
background: #1A1C20;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.task-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.task-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
|
||||
.title-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: rgba(255,255,255,0.7);
|
||||
}
|
||||
}
|
||||
|
||||
.task-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.task-time {
|
||||
font-size: 12px;
|
||||
color: rgba(255,255,255,0.4);
|
||||
}
|
||||
}
|
||||
|
||||
.task-prompt-wrapper {
|
||||
margin-bottom: 12px;
|
||||
|
||||
.prompt-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.video-preview {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.preview-bg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: #343C48;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: var(--bg-image-url);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
opacity: 0.48;
|
||||
filter: blur(30px);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.preview-video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.prompt-text {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
color: rgba(255,255,255,0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
background: rgba(0,0,0,0.4);
|
||||
border: 1px solid rgba(255,255,255,0.12);
|
||||
border-radius: 6px;
|
||||
padding: 2px 6px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
|
||||
.tag-icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
color: #AB8DFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.model-info {
|
||||
font-size: 12px;
|
||||
color: rgba(255,255,255,0.4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.split {
|
||||
color: rgba(255,255,255,0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.task-meta {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.split {
|
||||
color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.task-time {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.video-player {
|
||||
margin-bottom: 12px;
|
||||
|
||||
.player-container {
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
background: #000;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
.task-video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.task-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.action-left {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
.action-btn {
|
||||
height: 32px;
|
||||
background: #2A2A32;
|
||||
border: none;
|
||||
color: #fff;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
&:hover {
|
||||
background: #3A3A42;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
background: #2A2A32;
|
||||
border-radius: 8px;
|
||||
padding: 4px;
|
||||
|
||||
.icon-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
color: rgba(255,255,255,0.7);
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #3A3A42;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 未登录样式 */
|
||||
.login-require {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.login-require-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
|
||||
.message {
|
||||
font-size: 16px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
width: 140px;
|
||||
height: 44px;
|
||||
border-radius: 12px;
|
||||
font-size: 16px;
|
||||
background: linear-gradient(270deg, #94d8ff 11.11%, #e0ddff);
|
||||
border: none;
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
|
||||
/* 状态徽章样式 */
|
||||
.status-badge {
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
|
||||
&.status-created {
|
||||
background-color: rgba(148, 216, 255, 0.2);
|
||||
color: #94D8FF;
|
||||
}
|
||||
|
||||
&.status-processing {
|
||||
background-color: rgba(255, 193, 7, 0.2);
|
||||
color: #FFC107;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
&.status-queueing {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
&.status-succeeded,
|
||||
&.status-completed {
|
||||
background-color: rgba(76, 175, 80, 0.2);
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
&.status-failed {
|
||||
background-color: rgba(244, 67, 54, 0.2);
|
||||
color: #F44336;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
186
src/assets/less/Video/text.css
Normal file
186
src/assets/less/Video/text.css
Normal file
@ -0,0 +1,186 @@
|
||||
.reference-to-video {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
/* 卡片间距,可按需调整 */
|
||||
}
|
||||
/* 提示词卡片:自动填充剩余高度 */
|
||||
.card {
|
||||
flex: 1;
|
||||
/* 关键:占据剩余空间 */
|
||||
min-height: 0;
|
||||
/* 允许收缩,防止溢出 */
|
||||
background-color: #1a1c20;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding: 16px;
|
||||
}
|
||||
.card :deep(.el-card__body) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 12px 16px;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.reference-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
color: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
.input {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.input :deep(.el-textarea) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.input :deep(.el-textarea__inner) {
|
||||
flex: 1;
|
||||
min-height: 150px;
|
||||
resize: vertical;
|
||||
}
|
||||
/* 底部卡片:高度由内容决定,不伸缩 */
|
||||
.btmcard {
|
||||
flex-shrink: 0;
|
||||
/* 不参与剩余空间分配 */
|
||||
background-color: #1a1c20;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-top: 0;
|
||||
/* 间距已由 .reference-to-video 的 gap 控制 */
|
||||
}
|
||||
.btmcard :deep(.el-card__body) {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
/* 其余原有样式保持不变 */
|
||||
.select {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
.select-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
}
|
||||
.select-title {
|
||||
width: 50%;
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
padding-right: 12px;
|
||||
}
|
||||
.select-content {
|
||||
width: 50%;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.required-star {
|
||||
color: #f56c6c;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.audio-wrapper {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
.audio-wrapper .el-input {
|
||||
flex: 1;
|
||||
}
|
||||
.audio-wrapper .upload-btn {
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
border: none;
|
||||
color: #fff;
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
.audio-wrapper .upload-btn:hover {
|
||||
background-color: rgba(255, 255, 255, 0.16);
|
||||
}
|
||||
/* 全局深度样式 */
|
||||
:deep(.el-select__wrapper) {
|
||||
width: 100% !important;
|
||||
border: none !important;
|
||||
background-color: rgba(0, 0, 0, 0.32) !important;
|
||||
box-shadow: none !important;
|
||||
border-radius: 10px !important;
|
||||
font-size: 14px;
|
||||
color: #fff !important;
|
||||
outline: none !important;
|
||||
}
|
||||
:deep(.el-select__selected-item) {
|
||||
color: #fff !important;
|
||||
}
|
||||
:deep(.el-textarea) .el-textarea__inner {
|
||||
border: none !important;
|
||||
background-color: rgba(0, 0, 0, 0.32) !important;
|
||||
box-shadow: none !important;
|
||||
border-radius: 10px !important;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: #fff !important;
|
||||
resize: vertical;
|
||||
outline: none !important;
|
||||
backdrop-filter: blur(30px);
|
||||
padding: 14px 16px !important;
|
||||
}
|
||||
:deep(.el-textarea) .el-textarea__inner:focus {
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
outline: none !important;
|
||||
}
|
||||
:deep(.el-textarea) .el-textarea__inner::placeholder {
|
||||
color: #ffffff7a !important;
|
||||
font-size: 14px;
|
||||
}
|
||||
:deep(.el-textarea) .el-input__count {
|
||||
background: transparent;
|
||||
color: #ffffff7a;
|
||||
}
|
||||
:deep(.el-input-number) {
|
||||
width: 100%;
|
||||
}
|
||||
:deep(.el-input-number) .el-input__wrapper {
|
||||
background-color: rgba(0, 0, 0, 0.32) !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
border-radius: 10px !important;
|
||||
}
|
||||
:deep(.el-input-number) .el-input__inner {
|
||||
color: #fff !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
:deep(.el-input-number) .el-input-number__decrease,
|
||||
:deep(.el-input-number) .el-input-number__increase {
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
color: #fff;
|
||||
border: none;
|
||||
}
|
||||
:deep(.el-input__wrapper) {
|
||||
width: 100% !important;
|
||||
border: none !important;
|
||||
background-color: rgba(0, 0, 0, 0.32) !important;
|
||||
box-shadow: none !important;
|
||||
border-radius: 10px !important;
|
||||
font-size: 14px;
|
||||
color: #fff !important;
|
||||
outline: none !important;
|
||||
}
|
||||
.offpeak-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 32px;
|
||||
}
|
||||
213
src/assets/less/Video/text.less
Normal file
213
src/assets/less/Video/text.less
Normal file
@ -0,0 +1,213 @@
|
||||
|
||||
.reference-to-video {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px; /* 卡片间距,可按需调整 */
|
||||
}
|
||||
|
||||
/* 提示词卡片:自动填充剩余高度 */
|
||||
.card {
|
||||
flex: 1; /* 关键:占据剩余空间 */
|
||||
min-height: 0; /* 允许收缩,防止溢出 */
|
||||
background-color: #1a1c20;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.card :deep(.el-card__body) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 12px 16px;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.reference-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
color: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.input {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.input :deep(.el-textarea) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.input :deep(.el-textarea__inner) {
|
||||
flex: 1;
|
||||
min-height: 150px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/* 底部卡片:高度由内容决定,不伸缩 */
|
||||
.btmcard {
|
||||
flex-shrink: 0; /* 不参与剩余空间分配 */
|
||||
background-color: #1a1c20;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-top: 0; /* 间距已由 .reference-to-video 的 gap 控制 */
|
||||
}
|
||||
|
||||
.btmcard :deep(.el-card__body) {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
/* 其余原有样式保持不变 */
|
||||
.select {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.select-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.select-title {
|
||||
width: 50%;
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.select-content {
|
||||
width: 50%;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.required-star {
|
||||
color: #f56c6c;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.audio-wrapper {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
|
||||
.el-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.upload-btn {
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
border: none;
|
||||
color: #fff;
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.16);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 全局深度样式 */
|
||||
:deep(.el-select__wrapper) {
|
||||
width: 100% !important;
|
||||
border: none !important;
|
||||
background-color: rgba(0, 0, 0, 0.32) !important;
|
||||
box-shadow: none !important;
|
||||
border-radius: 10px !important;
|
||||
font-size: 14px;
|
||||
color: #fff !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
:deep(.el-select__selected-item) {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
:deep(.el-textarea) {
|
||||
.el-textarea__inner {
|
||||
border: none !important;
|
||||
background-color: rgba(0, 0, 0, 0.32) !important;
|
||||
box-shadow: none !important;
|
||||
border-radius: 10px !important;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: #fff !important;
|
||||
resize: vertical;
|
||||
outline: none !important;
|
||||
backdrop-filter: blur(30px);
|
||||
padding: 14px 16px !important;
|
||||
|
||||
&:focus {
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: #ffffff7a !important;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-input__count {
|
||||
background: transparent;
|
||||
color: #ffffff7a;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-input-number) {
|
||||
width: 100%;
|
||||
|
||||
.el-input__wrapper {
|
||||
background-color: rgba(0, 0, 0, 0.32) !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
border-radius: 10px !important;
|
||||
}
|
||||
|
||||
.el-input__inner {
|
||||
color: #fff !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.el-input-number__decrease,
|
||||
.el-input-number__increase {
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
color: #fff;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
width: 100% !important;
|
||||
border: none !important;
|
||||
background-color: rgba(0, 0, 0, 0.32) !important;
|
||||
box-shadow: none !important;
|
||||
border-radius: 10px !important;
|
||||
font-size: 14px;
|
||||
color: #fff !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.offpeak-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 32px;
|
||||
}
|
||||
1
src/assets/vue.svg
Normal file
1
src/assets/vue.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 496 B |
144
src/components/Header/Header.vue
Normal file
144
src/components/Header/Header.vue
Normal file
@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<div class="header-container">
|
||||
<div class="header">
|
||||
<!-- 左侧Logo(完全不动) -->
|
||||
<div class="logo">
|
||||
<img src="@/assets/images/favicon.png" alt="logo" />
|
||||
<span class="header-title">元境</span>
|
||||
</div>
|
||||
|
||||
<!-- 右侧操作区 -->
|
||||
<div class="header-actions">
|
||||
<!-- 登录后显示:充值 + 余额 一体化模块 -->
|
||||
<template v-if="loginStore.isLoggedIn">
|
||||
<!-- 一体化:余额 + 充值按钮 -->
|
||||
<div class="balance-recharge-combo" @click="showRechargeModal = true">
|
||||
<i class="iconfont icon-yue"></i>
|
||||
<span class="balance-text">{{ balances }}</span>
|
||||
<span class="recharge-text">充值</span>
|
||||
</div>
|
||||
|
||||
<!-- 用户信息 -->
|
||||
<div class="user-profile" :class="{ 'dropdown-open': userDropdownVisible }">
|
||||
<div class="user-trigger" @click="toggleUserDropdown">
|
||||
<div class="user-name">{{ loginStore.userInfo?.user?.name || loginStore.userInfo?.user?.username }}</div>
|
||||
<svg class="dropdown-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M6 9l6 6 6-6" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<Transition name="dropdown">
|
||||
<div v-if="userDropdownVisible" class="user-dropdown">
|
||||
<div class="dropdown-item user-id">
|
||||
<span class="item-label">用户ID:</span>
|
||||
<span class="item-value">{{ loginStore.userInfo?.user?.id }}</span>
|
||||
</div>
|
||||
<div class="dropdown-divider"></div>
|
||||
<div class="dropdown-item logout" @click="handleLogout">
|
||||
<svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
|
||||
<polyline points="16 17 21 12 16 7" />
|
||||
<line x1="21" y1="12" x2="9" y2="12" />
|
||||
</svg>
|
||||
<span>退出登录</span>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 未登录:登录按钮 -->
|
||||
<div v-else class="btn" @click="$router.push('/login')">
|
||||
登录
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Recharge v-if="showRechargeModal" @close="showRechargeModal = false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang='ts'>
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
||||
import Recharge from '@/components/Recharge/Recharge.vue';
|
||||
import { useLoginStore } from '@/store/LoginStore';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { balance } from '@/apis/home';
|
||||
import type { BalanceResult } from '@/types/home';
|
||||
|
||||
const router = useRouter();
|
||||
const loginStore = useLoginStore();
|
||||
|
||||
// 控制充值弹窗
|
||||
const showRechargeModal = ref(false);
|
||||
// 控制用户下拉
|
||||
const userDropdownVisible = ref(false);
|
||||
// 余额
|
||||
const balances = ref(0);
|
||||
|
||||
// 切换下拉菜单
|
||||
const toggleUserDropdown = () => {
|
||||
userDropdownVisible.value = !userDropdownVisible.value;
|
||||
};
|
||||
|
||||
// 退出登录
|
||||
const handleLogout = () => {
|
||||
loginStore.logout();
|
||||
router.push('/login');
|
||||
userDropdownVisible.value = false;
|
||||
};
|
||||
|
||||
// ==============================================
|
||||
// 获取余额(仅登录后调用)
|
||||
// ==============================================
|
||||
const getBalance = async () => {
|
||||
try {
|
||||
const res = await balance<BalanceResult>();
|
||||
console.log('用户余额:', res);
|
||||
if (res.status === 'ok') {
|
||||
balances.value = res.data[0].balance;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取余额失败:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// ==============================================
|
||||
// 监听登录状态 → 登录后才请求余额
|
||||
// ==============================================
|
||||
watch(
|
||||
() => loginStore.isLoggedIn,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
getBalance(); // 登录成功后获取余额
|
||||
} else {
|
||||
balances.value = 0; // 退出登录清空余额
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 点击外部关闭下拉
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
if (!target.closest('.user-profile')) {
|
||||
userDropdownVisible.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 会话超时
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleClickOutside);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang='less'>
|
||||
@import url('../../assets/less/Header/header.less');
|
||||
</style>
|
||||
724
src/components/Recharge/Recharge.vue
Normal file
724
src/components/Recharge/Recharge.vue
Normal file
@ -0,0 +1,724 @@
|
||||
<template>
|
||||
<div class="recharge-modal-mask" @click.self="closeModal">
|
||||
<div class="recharge-container">
|
||||
<!-- 头部 -->
|
||||
<div class="modal-header">
|
||||
<h2>充值账户</h2>
|
||||
<button class="close-btn" @click="closeModal">✕</button>
|
||||
</div>
|
||||
|
||||
<!-- 充值渠道选择 -->
|
||||
<div class="provider-section">
|
||||
<p class="section-title">选择充值渠道</p>
|
||||
<div class="provider-grid">
|
||||
<button
|
||||
v-for="provider in providers"
|
||||
:key="provider.value"
|
||||
class="provider-btn"
|
||||
:class="{ active: selectedProvider === provider.value }"
|
||||
@click="selectProvider(provider.value)"
|
||||
>
|
||||
<span class="iconfont icon-zhifubaozhifu"></span>
|
||||
<span class="provider-name">{{ provider.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 金额选择区 -->
|
||||
<div class="amount-section">
|
||||
<p class="section-title">选择充值金额</p>
|
||||
<div class="amount-grid">
|
||||
<button
|
||||
v-for="amount in presetAmounts"
|
||||
:key="amount"
|
||||
class="amount-btn"
|
||||
:class="{ active: selectedAmount === amount }"
|
||||
@click="selectPresetAmount(amount)"
|
||||
>
|
||||
<span class="currency">¥</span>{{ amount }}
|
||||
</button>
|
||||
<button
|
||||
class="amount-btn custom-btn"
|
||||
:class="{ active: isCustomSelected }"
|
||||
@click="openCustomDialog"
|
||||
>
|
||||
<span class="currency">✧</span>
|
||||
{{ customButtonText }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 当前选中金额展示 -->
|
||||
<div class="selected-info" v-if="selectedAmount">
|
||||
<span>充值金额:</span>
|
||||
<strong class="selected-amount">¥{{ selectedAmount }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 确认充值按钮 -->
|
||||
<button class="confirm-btn" @click="handleConfirm" :disabled="loading">
|
||||
<span v-if="!loading">确认充值</span>
|
||||
<span v-else>处理中...</span>
|
||||
<svg v-if="!loading" class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M5 12h14M12 5l7 7-7 7" />
|
||||
</svg>
|
||||
<svg v-else class="btn-icon loading" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M12 6v6l4 2" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 自定义金额弹窗 (独立遮罩) -->
|
||||
<Teleport to="body">
|
||||
<Transition name="fade">
|
||||
<div v-if="customDialogVisible" class="custom-modal-mask" @click.self="closeCustomDialog">
|
||||
<div class="custom-dialog">
|
||||
<div class="dialog-header">
|
||||
<h3>自定义金额</h3>
|
||||
<button class="close-btn" @click="closeCustomDialog">✕</button>
|
||||
</div>
|
||||
<div class="dialog-body">
|
||||
<div class="input-group">
|
||||
<span class="input-currency">¥</span>
|
||||
<input
|
||||
ref="customInputRef"
|
||||
v-model.number="customAmount"
|
||||
type="number"
|
||||
placeholder="请输入金额"
|
||||
class="custom-input"
|
||||
min="0.01"
|
||||
step="0.01"
|
||||
@keyup.enter="confirmCustomAmount"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="customError" class="error-msg">{{ customError }}</div>
|
||||
</div>
|
||||
<div class="dialog-footer">
|
||||
<button class="dialog-btn cancel" @click="closeCustomDialog">取消</button>
|
||||
<button class="dialog-btn confirm" @click="confirmCustomAmount">确定</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, nextTick } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { recharge } from '@/apis/home';
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits<{
|
||||
(e: 'close'): void;
|
||||
(e: 'confirm', amount: number): void;
|
||||
}>();
|
||||
|
||||
// 充值渠道列表
|
||||
const providers = [
|
||||
{ value: 'alipay', name: '支付宝', icon: '' },
|
||||
|
||||
];
|
||||
|
||||
// 预设金额
|
||||
const presetAmounts = [50, 200, 500, 1000];
|
||||
|
||||
// 主弹窗选中金额
|
||||
const selectedAmount = ref<number | null>(50); // 默认选中50
|
||||
// 选中的充值渠道
|
||||
const selectedProvider = ref<string>('alipay'); // 默认选中支付宝
|
||||
// 加载状态
|
||||
const loading = ref(false);
|
||||
|
||||
// 自定义弹窗相关
|
||||
const customDialogVisible = ref(false);
|
||||
const customAmount = ref<number | null>(null);
|
||||
const customError = ref('');
|
||||
const customInputRef = ref<HTMLInputElement | null>(null);
|
||||
|
||||
// 判断当前是否选中自定义金额 (非预设值)
|
||||
const isCustomSelected = computed(() => {
|
||||
if (selectedAmount.value === null) return false;
|
||||
return !presetAmounts.includes(selectedAmount.value);
|
||||
});
|
||||
|
||||
// 自定义按钮显示文本
|
||||
const customButtonText = computed(() => {
|
||||
if (isCustomSelected.value && selectedAmount.value !== null) {
|
||||
return `自定义 (¥${selectedAmount.value})`;
|
||||
}
|
||||
return '自定义';
|
||||
});
|
||||
|
||||
// 选择预设金额
|
||||
const selectPresetAmount = (amount: number) => {
|
||||
selectedAmount.value = amount;
|
||||
};
|
||||
|
||||
// 选择充值渠道
|
||||
const selectProvider = (provider: string) => {
|
||||
selectedProvider.value = provider;
|
||||
};
|
||||
|
||||
// 打开自定义弹窗
|
||||
const openCustomDialog = () => {
|
||||
// 如果当前选中的是自定义金额,则回显该金额到输入框
|
||||
if (isCustomSelected.value && selectedAmount.value !== null) {
|
||||
customAmount.value = selectedAmount.value;
|
||||
} else {
|
||||
customAmount.value = null;
|
||||
}
|
||||
customError.value = '';
|
||||
customDialogVisible.value = true;
|
||||
// 等待DOM更新后自动聚焦输入框
|
||||
nextTick(() => {
|
||||
customInputRef.value?.focus();
|
||||
});
|
||||
};
|
||||
|
||||
// 关闭自定义弹窗
|
||||
const closeCustomDialog = () => {
|
||||
customDialogVisible.value = false;
|
||||
customError.value = '';
|
||||
};
|
||||
|
||||
// 确认自定义金额 (点击确定)
|
||||
const confirmCustomAmount = () => {
|
||||
// 校验金额
|
||||
let amount = customAmount.value;
|
||||
if (amount === null || isNaN(amount)) {
|
||||
customError.value = '请输入有效的金额';
|
||||
return;
|
||||
}
|
||||
if (amount <= 0) {
|
||||
customError.value = '金额必须大于0';
|
||||
return;
|
||||
}
|
||||
if (amount > 999999) {
|
||||
customError.value = '金额不能超过999999';
|
||||
return;
|
||||
}
|
||||
|
||||
// 保留两位小数,转为数字
|
||||
const finalAmount = Number(amount.toFixed(2));
|
||||
|
||||
// 更新主弹窗选中金额
|
||||
selectedAmount.value = finalAmount;
|
||||
|
||||
// 打印参数 (自定义弹窗确定)
|
||||
console.log('[自定义充值] amount:', finalAmount);
|
||||
|
||||
// 关闭自定义弹窗
|
||||
closeCustomDialog();
|
||||
};
|
||||
|
||||
// 主弹窗确认充值
|
||||
const handleConfirm = async () => {
|
||||
if (selectedAmount.value === null) {
|
||||
console.warn('未选择金额');
|
||||
ElMessage.warning('请选择充值金额');
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
const amount = selectedAmount.value;
|
||||
// 准备充值参数
|
||||
const rechargeParams = {
|
||||
provider: 'alipay',
|
||||
amount: amount,
|
||||
currency: 'CNY'
|
||||
};
|
||||
|
||||
// 打印参数 (主弹窗确认)
|
||||
console.log('[确认充值]', rechargeParams);
|
||||
|
||||
// 充值响应类型
|
||||
interface RechargeResponse {
|
||||
status: string;
|
||||
message?: string;
|
||||
data: { url: string } | null;
|
||||
}
|
||||
|
||||
// 调用充值接口
|
||||
const res = await recharge<RechargeResponse, typeof rechargeParams>(rechargeParams);
|
||||
console.log('充值接口返回:', res);
|
||||
|
||||
// 处理充值结果
|
||||
if (res.status === 'ok') {
|
||||
ElMessage.success('充值请求成功,正在跳转到支付页面');
|
||||
// 检查是否有支付链接
|
||||
if (res.data && res.data.url) {
|
||||
// 在新窗口打开支付宝支付页面
|
||||
window.open(res.data.url, '_blank', 'width=800,height=600');
|
||||
} else {
|
||||
ElMessage.error('缺少支付链接,请稍后重试');
|
||||
}
|
||||
// 通知父组件
|
||||
emit('confirm', amount);
|
||||
// 关闭主弹窗
|
||||
closeModal();
|
||||
} else {
|
||||
ElMessage.error(res.message || '充值请求失败,请稍后重试');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('充值失败:', error);
|
||||
ElMessage.error('充值失败,请稍后重试');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭主弹窗(触发 close 事件)
|
||||
const closeModal = () => {
|
||||
emit('close');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
// 科技感色调
|
||||
@primary: #00f2fe;
|
||||
@primary-dark: #4facfe;
|
||||
@bg-dark: rgba(10, 20, 40, 0.92);
|
||||
@glass-bg: rgba(20, 30, 55, 0.75);
|
||||
@border-glow: 0 0 10px rgba(0, 242, 254, 0.3);
|
||||
@text-light: #eef5ff;
|
||||
@text-secondary: #b0c4ff;
|
||||
|
||||
// 通用动画
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
// 主弹窗遮罩层
|
||||
.recharge-modal-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.65);
|
||||
backdrop-filter: blur(8px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
// 主弹窗容器
|
||||
.recharge-container {
|
||||
width: 520px;
|
||||
max-width: 90vw;
|
||||
background: @glass-bg;
|
||||
backdrop-filter: blur(12px);
|
||||
border-radius: 32px;
|
||||
padding: 28px 32px 36px;
|
||||
// box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(0, 242, 254, 0.2), 0 0 20px rgba(0, 242, 254, 0.3);
|
||||
transition: transform 0.2s ease;
|
||||
// border: 1px solid rgba(0, 242, 254, 0.4);
|
||||
}
|
||||
|
||||
// 头部
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
margin-bottom: 28px;
|
||||
h2 {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
background: linear-gradient(135deg, #fff, @primary);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
color: transparent;
|
||||
letter-spacing: 1px;
|
||||
margin: 0;
|
||||
text-shadow: 0 0 5px rgba(0, 242, 254, 0.3);
|
||||
}
|
||||
.close-btn {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: none;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
color: @text-secondary;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
color: #fff;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 充值渠道区域
|
||||
.provider-section {
|
||||
margin-bottom: 24px;
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
color: @text-secondary;
|
||||
margin-bottom: 16px;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
.provider-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.provider-btn {
|
||||
background: rgba(15, 25, 45, 0.7);
|
||||
border: 1px solid rgba(0, 242, 254, 0.3);
|
||||
border-radius: 20px;
|
||||
padding: 16px 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: @text-light;
|
||||
cursor: pointer;
|
||||
transition: all 0.25s ease;
|
||||
backdrop-filter: blur(4px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
|
||||
.provider-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.provider-name {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: @primary;
|
||||
box-shadow: 0 0 12px rgba(0, 242, 254, 0.5);
|
||||
background: rgba(0, 242, 254, 0.15);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: linear-gradient(135deg, rgba(0, 242, 254, 0.3), rgba(79, 172, 254, 0.3));
|
||||
border-color: @primary;
|
||||
box-shadow: 0 0 15px rgba(0, 242, 254, 0.6);
|
||||
color: #fff;
|
||||
// text-shadow: 0 0 4px rgba(0, 242, 254, 0.8);
|
||||
}
|
||||
.iconfont{
|
||||
color: #00f2fe;
|
||||
}
|
||||
}
|
||||
|
||||
// 金额区域
|
||||
.amount-section {
|
||||
margin-bottom: 32px;
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
color: @text-secondary;
|
||||
margin-bottom: 16px;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
.amount-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
|
||||
@media (min-width: 480px) {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.amount-btn {
|
||||
background: rgba(15, 25, 45, 0.7);
|
||||
border: 1px solid rgba(0, 242, 254, 0.3);
|
||||
border-radius: 20px;
|
||||
padding: 12px 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: @text-light;
|
||||
cursor: pointer;
|
||||
transition: all 0.25s ease;
|
||||
backdrop-filter: blur(4px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
|
||||
.currency {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: @primary;
|
||||
box-shadow: 0 0 12px rgba(0, 242, 254, 0.5);
|
||||
background: rgba(0, 242, 254, 0.15);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: linear-gradient(135deg, rgba(0, 242, 254, 0.3), rgba(79, 172, 254, 0.3));
|
||||
border-color: @primary;
|
||||
box-shadow: 0 0 15px rgba(0, 242, 254, 0.6);
|
||||
color: #fff;
|
||||
text-shadow: 0 0 4px rgba(0, 242, 254, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.custom-btn {
|
||||
.currency {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.selected-info {
|
||||
text-align: right;
|
||||
padding: 12px 8px 0;
|
||||
border-top: 1px dashed rgba(0, 242, 254, 0.3);
|
||||
font-size: 16px;
|
||||
color: @text-secondary;
|
||||
.selected-amount {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: @primary;
|
||||
margin-left: 8px;
|
||||
text-shadow: 0 0 6px rgba(0, 242, 254, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
// 确认按钮
|
||||
.confirm-btn {
|
||||
width: 100%;
|
||||
background: linear-gradient(90deg, @primary-dark, @primary);
|
||||
border: none;
|
||||
border-radius: 40px;
|
||||
padding: 14px 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #0a1428;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
transition: all 0.3s;
|
||||
box-shadow: 0 4px 12px rgba(0, 242, 254, 0.3);
|
||||
letter-spacing: 2px;
|
||||
|
||||
.btn-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
transition: transform 0.2s;
|
||||
|
||||
&.loading {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 8px 20px rgba(0, 242, 254, 0.5);
|
||||
filter: brightness(1.05);
|
||||
.btn-icon:not(.loading) {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
transform: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义弹窗遮罩层 (更高层级)
|
||||
.custom-modal-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
backdrop-filter: blur(12px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1100;
|
||||
}
|
||||
|
||||
.custom-dialog {
|
||||
width: 100%;
|
||||
max-width: 25vw;
|
||||
background: rgba(12, 22, 40, 0.95);
|
||||
backdrop-filter: blur(16px);
|
||||
border-radius: 28px;
|
||||
border: 1px solid rgba(0, 242, 254, 0.5);
|
||||
box-shadow: 0 20px 35px rgba(0, 0, 0, 0.5), 0 0 25px rgba(0, 242, 254, 0.2);
|
||||
overflow: hidden;
|
||||
animation: slideUp 0.25s ease;
|
||||
|
||||
.dialog-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 24px 12px;
|
||||
border-bottom: 1px solid rgba(0, 242, 254, 0.2);
|
||||
h3 {
|
||||
font-size: 22px;
|
||||
font-weight: 500;
|
||||
color: @text-light;
|
||||
margin: 0;
|
||||
background: linear-gradient(135deg, #fff, @primary);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
.close-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
color: @text-secondary;
|
||||
transition: all 0.2s;
|
||||
&:hover {
|
||||
color: @primary;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-body {
|
||||
padding: 28px 24px 20px;
|
||||
.input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
border-radius: 48px;
|
||||
border: 1px solid rgba(0, 242, 254, 0.4);
|
||||
transition: all 0.2s;
|
||||
&:focus-within {
|
||||
border-color: @primary;
|
||||
box-shadow: 0 0 12px rgba(0, 242, 254, 0.4);
|
||||
}
|
||||
.input-currency {
|
||||
font-size: 28px;
|
||||
font-weight: 500;
|
||||
padding: 0 16px;
|
||||
color: @primary;
|
||||
}
|
||||
.custom-input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 16px 16px 16px 0;
|
||||
font-size: 26px;
|
||||
font-weight: 500;
|
||||
color: @text-light;
|
||||
outline: none;
|
||||
&::placeholder {
|
||||
color: rgba(176, 196, 255, 0.5);
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
}
|
||||
/* 去除 number 箭头 */
|
||||
&::-webkit-outer-spin-button,
|
||||
&::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
}
|
||||
.error-msg {
|
||||
color: #ff6b6b;
|
||||
font-size: 13px;
|
||||
margin-top: 12px;
|
||||
padding-left: 16px;
|
||||
text-shadow: 0 0 2px rgba(0,0,0,0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
padding: 12px 24px 28px;
|
||||
.dialog-btn {
|
||||
flex: 1;
|
||||
padding: 12px 0;
|
||||
border-radius: 40px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: none;
|
||||
background: transparent;
|
||||
&.cancel {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
color: @text-secondary;
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
&.confirm {
|
||||
background: linear-gradient(90deg, @primary-dark, @primary);
|
||||
color: #0a1428;
|
||||
box-shadow: 0 0 10px rgba(0, 242, 254, 0.4);
|
||||
&:hover {
|
||||
transform: scale(1.02);
|
||||
filter: brightness(1.05);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
618
src/components/RightResult/RightResult.vue
Normal file
618
src/components/RightResult/RightResult.vue
Normal file
@ -0,0 +1,618 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card class="rig">
|
||||
<!-- 未登录状态 -->
|
||||
<div v-if="!isLoggedIn" class="login-require">
|
||||
<div class="login-require-content">
|
||||
<div class="message">请登录后再进行创作哦~</div>
|
||||
<el-button type="primary" class="login-btn" @click="handleLogin">
|
||||
登录
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 已登录状态 -->
|
||||
<template v-else>
|
||||
<div class="top">
|
||||
<el-checkbox v-model="showProcessing" @change="handleFilterChange" label="进行中" size="large" />
|
||||
|
||||
<el-select v-model="statusFilter" placeholder="全部状态" style="width: 240px" @change="handleFilterChange">
|
||||
<el-option label="全部状态" value="" />
|
||||
<el-option label="已创建" value="created" />
|
||||
<el-option label="处理中" value="processing" />
|
||||
<el-option label="排队中" value="queueing" />
|
||||
<el-option label="已完成" value="succeeded" />
|
||||
<el-option label="失败" value="failed" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<!-- 加载中 -->
|
||||
<div v-if="isLoading" class="loading-container">
|
||||
<el-icon class="is-loading" :size="40">
|
||||
<svg viewBox="0 0 1024 1024">
|
||||
<path fill="currentColor" d="M512 64a448 448 0 1 1 0 896A448 448 0 0 1 0 64z"></path>
|
||||
</svg>
|
||||
</el-icon>
|
||||
<div class="loading-text">加载中...</div>
|
||||
</div>
|
||||
|
||||
<!-- 无任务 -->
|
||||
<div v-else-if="!filteredTasks.length" class="empty">
|
||||
<img src="../../assets/images/preparation.png" alt="">
|
||||
<div class="title">开始创作您的第一个作品吧!</div>
|
||||
</div>
|
||||
|
||||
<!-- 任务列表 -->
|
||||
<div v-else class="task-list">
|
||||
<div class="task-item" v-for="task in filteredTasks" :key="task.taskid">
|
||||
<!-- 顶部标题栏 -->
|
||||
<div class="task-header">
|
||||
<div class="task-title">
|
||||
<svg width="1em" height="1em" fill="none" viewBox="0 0 24 24" class="title-icon">
|
||||
<path d="M20.3996 11.3997L20.3996 6.59971C20.3996 5.27422 19.3251 4.19971 17.9996 4.19971H5.99961C4.67413 4.19971 3.59961 5.27422 3.59961 6.59971V17.3997C3.59961 18.7252 4.67413 19.7997 5.99961 19.7997L12.5996 19.7997" stroke="currentColor" stroke-linecap="round" stroke-width="1.5"></path>
|
||||
<path d="M14.3996 11.9999L10.7996 14.0784V9.92144L14.3996 11.9999Z" fill="currentColor" stroke="currentColor" stroke-width="1.2"></path>
|
||||
</svg>
|
||||
<span>{{ task.action === 't2v' ? '文生视频' : task.action === 'i2v' ? '图生视频' : '视频生成' }}</span>
|
||||
</div>
|
||||
<div class="task-status">
|
||||
<span :class="['status-badge', getStatusClass(task.status)]">
|
||||
{{ getStatusText(task.status) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提示词 + 模型信息 -->
|
||||
<div class="task-prompt-wrapper">
|
||||
<div class="prompt-content">
|
||||
<div class="video-preview">
|
||||
<div class="preview-bg" :style="{ '--bg-image-url': `url(${task.cover_url})` }">
|
||||
<video
|
||||
:src="task.video_url"
|
||||
:poster="task.cover_url"
|
||||
class="preview-video"
|
||||
autoplay loop muted playsinline
|
||||
></video>
|
||||
</div>
|
||||
</div>
|
||||
<div class="prompt-text">
|
||||
{{ task.prompt }}
|
||||
</div>
|
||||
<div class="task-meta">
|
||||
<span class="task-time">{{ task.use_time }}</span>
|
||||
<span class="split">|</span>
|
||||
<span>{{ task.model || '未知模型' }}</span>
|
||||
<span class="split">|</span>
|
||||
<span>{{ task.resolution || '未知清晰度' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 视频播放区域 -->
|
||||
<div class="video-player">
|
||||
<div class="player-container">
|
||||
<!-- 视频生成中提示 -->
|
||||
<div v-if="!task.video_url && (task.status === 'created' || task.status === 'processing' || task.status === 'queueing' || task.status === 'running' || task.status === 'PENDING')" class="generating-status">
|
||||
<div class="spinner"></div>
|
||||
<div class="generating-text">{{ getStatusText(task.status) }}</div>
|
||||
</div>
|
||||
<!-- 视频播放器 -->
|
||||
<video
|
||||
v-else
|
||||
:src="task.video_url"
|
||||
:poster="task.cover_url"
|
||||
class="task-video"
|
||||
controls
|
||||
playsinline
|
||||
></video>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<div class="task-actions">
|
||||
<div class="action-left">
|
||||
<!-- <el-button class="action-btn">做同款</el-button> -->
|
||||
</div>
|
||||
<div class="action-right">
|
||||
<!-- 下载按钮 -->
|
||||
<div
|
||||
class="icon-btn"
|
||||
:class="{ disabled: !task.video_url || downloadingTaskIds.has(task.taskid) }"
|
||||
@click="task.video_url && !downloadingTaskIds.has(task.taskid) ? downloadVideo(task) : null"
|
||||
:title="task.video_url ? '下载视频' : '暂无可下载的视频'"
|
||||
>
|
||||
<template v-if="downloadingTaskIds.has(task.taskid)">
|
||||
<el-icon class="is-loading">
|
||||
<Loading />
|
||||
</el-icon>
|
||||
</template>
|
||||
<template v-else>
|
||||
<svg width="1em" height="1em" fill="none" viewBox="0 0 24 24">
|
||||
<path d="M3 14.0042V18.5C3 19.6046 3.89543 20.5 5 20.5H19C20.1046 20.5 21 19.6046 21 18.5V14" stroke="currentColor" stroke-linecap="round" stroke-width="1.5"></path>
|
||||
<path d="M17.5 9.5L12 15L6.5 9.5" stroke="currentColor" stroke-linecap="round" stroke-width="1.5"></path>
|
||||
<path d="M11.9961 3V15" stroke="currentColor" stroke-linecap="round" stroke-width="1.5"></path>
|
||||
</svg>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang='ts'>
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { ElLoading, ElMessage } from 'element-plus';
|
||||
import { Loading } from '@element-plus/icons-vue';
|
||||
import { useLoginStore } from '@/store/LoginStore';
|
||||
import { getAsyncTaskStatus, getTodayTask } from '@/apis/Model';
|
||||
import type { TaskItem, TaskResult } from '@/types/model';
|
||||
|
||||
// ==================== 实例和状态 ====================
|
||||
const router = useRouter();
|
||||
const loginStore = useLoginStore();
|
||||
|
||||
// ==================== 响应式数据 ====================
|
||||
/** 登录状态 */
|
||||
const isLoggedIn = computed(() => loginStore.isLoggedIn);
|
||||
|
||||
/** 任务列表数据 */
|
||||
const taskList = ref<TaskItem[]>([]);
|
||||
|
||||
/** 加载状态 */
|
||||
const isLoading = ref(false);
|
||||
|
||||
/** 是否只显示进行中的任务 */
|
||||
const showProcessing = ref(false);
|
||||
|
||||
/** 状态筛选器 */
|
||||
const statusFilter = ref('');
|
||||
|
||||
/** 正在下载的任务ID集合 */
|
||||
const downloadingTaskIds = ref<Set<string>>(new Set());
|
||||
|
||||
/** 轮询定时器 */
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
/** 轮询间隔(30秒) */
|
||||
const POLL_INTERVAL = 30;
|
||||
|
||||
// ==================== 计算属性 ====================
|
||||
/**
|
||||
* 过滤和排序后的任务列表
|
||||
* - 根据 showProcessing 筛选进行中的任务
|
||||
* - 根据 statusFilter 筛选特定状态的任务
|
||||
* - 按时间降序排序(最新的在前)
|
||||
*/
|
||||
const filteredTasks = computed(() => {
|
||||
let tasks = [...taskList.value];
|
||||
|
||||
// 筛选进行中的任务
|
||||
if (showProcessing.value) {
|
||||
tasks = tasks.filter(task => task.status === 'processing');
|
||||
}
|
||||
|
||||
// 筛选特定状态的任务
|
||||
if (statusFilter.value) {
|
||||
tasks = tasks.filter(task => task.status === statusFilter.value);
|
||||
}
|
||||
|
||||
// 按时间降序排序(最新的在前)
|
||||
tasks.sort((a, b) => {
|
||||
const timeA = a.use_time || '';
|
||||
const timeB = b.use_time || '';
|
||||
return timeB.localeCompare(timeA);
|
||||
});
|
||||
|
||||
return tasks;
|
||||
});
|
||||
|
||||
// ==================== 事件处理 ====================
|
||||
/**
|
||||
* 处理登录按钮点击
|
||||
*/
|
||||
const handleLogin = () => {
|
||||
router.push('/login');
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理筛选变化
|
||||
*/
|
||||
const handleFilterChange = () => {};
|
||||
|
||||
/**
|
||||
* 获取状态显示文本
|
||||
* @param status 状态值
|
||||
*/
|
||||
const getStatusText = (status: string): string => {
|
||||
const statusMap: Record<string, string> = {
|
||||
'created': '已创建',
|
||||
'processing': '处理中',
|
||||
'running': '处理中',
|
||||
'queueing': '排队中',
|
||||
'succeeded': '已完成',
|
||||
'completed': '已完成',
|
||||
'failed': '失败',
|
||||
'PENDING': '处理中'
|
||||
};
|
||||
return statusMap[status] || status;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取状态样式类名
|
||||
* @param status 状态值
|
||||
*/
|
||||
const getStatusClass = (status: string): string => {
|
||||
const classMap: Record<string, string> = {
|
||||
'created': 'status-created',
|
||||
'processing': 'status-processing',
|
||||
'running': 'status-processing',
|
||||
'queueing': 'status-queueing',
|
||||
'succeeded': 'status-succeeded',
|
||||
'completed': 'status-completed',
|
||||
'failed': 'status-failed',
|
||||
'PENDING': 'status-processing'
|
||||
};
|
||||
return classMap[status] || '';
|
||||
};
|
||||
|
||||
/**
|
||||
* 下载视频
|
||||
* @param task 任务对象
|
||||
*/
|
||||
const downloadVideo = async (task: TaskItem) => {
|
||||
// 检查登录状态
|
||||
if (!isLoggedIn.value) {
|
||||
ElMessage.warning('请先登录');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查视频地址是否存在
|
||||
if (!task.video_url) {
|
||||
ElMessage.error('视频地址不存在,无法下载');
|
||||
return;
|
||||
}
|
||||
|
||||
const taskId = task.taskid;
|
||||
|
||||
// 防止重复下载
|
||||
if (downloadingTaskIds.value.has(taskId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
downloadingTaskIds.value.add(taskId);
|
||||
|
||||
const loadingInstance = ElLoading.service({
|
||||
text: '正在下载...',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
});
|
||||
|
||||
try {
|
||||
// 方法1:尝试使用 fetch 下载(不携带凭证,避免 CORS 问题)
|
||||
const response = await fetch(task.video_url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
|
||||
// 生成有意义的文件名:提示词_任务ID_时间戳.mp4
|
||||
const timestamp = new Date().getTime();
|
||||
const prompt = task.prompt?.substring(0, 15) || 'video';
|
||||
const fileName = `${prompt}_${task.taskid}_${timestamp}.mp4`;
|
||||
link.download = fileName;
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
// 清理对象 URL
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
// ElMessage.success('下载成功');
|
||||
} catch (error) {
|
||||
console.error('下载失败:', error);
|
||||
|
||||
// 方法2:降级方案 - 直接创建下载链接
|
||||
try {
|
||||
const link = document.createElement('a');
|
||||
link.href = task.video_url;
|
||||
link.download = `video_${task.taskid}_${new Date().getTime()}.mp4`;
|
||||
link.target = '_blank';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
ElMessage.success('已开始下载');
|
||||
} catch (fallbackError) {
|
||||
console.error('降级下载也失败:', fallbackError);
|
||||
ElMessage.error('下载失败,请右键点击视频选择"另存为"');
|
||||
}
|
||||
} finally {
|
||||
loadingInstance.close();
|
||||
downloadingTaskIds.value.delete(taskId);
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 数据加载 ====================
|
||||
/**
|
||||
* 查询单个任务的状态
|
||||
* @param taskId 任务ID
|
||||
*/
|
||||
const updateTaskStatus = async (taskId: string) => {
|
||||
try {
|
||||
interface Params {
|
||||
taskid: string;
|
||||
}
|
||||
interface Response {
|
||||
status: string;
|
||||
video?: string;
|
||||
image?: string;
|
||||
}
|
||||
interface Data {
|
||||
resposne: Response;
|
||||
}
|
||||
interface Result {
|
||||
status: string;
|
||||
data: Data;
|
||||
}
|
||||
|
||||
const res = await getAsyncTaskStatus<Result, Params>({ taskid: taskId });
|
||||
if (res.status === 'ok' && res.data?.resposne) {
|
||||
// 更新任务列表中对应任务的状态
|
||||
const taskIndex = taskList.value.findIndex(t => t.taskid === taskId);
|
||||
if (taskIndex !== -1) {
|
||||
// 更新状态(从 res.data.resposne.status 获取)
|
||||
const newStatus = res.data.resposne.status?.toLowerCase() || taskList.value[taskIndex].status;
|
||||
taskList.value[taskIndex].status = newStatus;
|
||||
|
||||
// 如果返回了新的视频地址,更新视频地址(从 res.data.resposne 获取)
|
||||
if (res.data.resposne.video) {
|
||||
taskList.value[taskIndex].video_url = res.data.resposne.video;
|
||||
}
|
||||
if (res.data.resposne.image) {
|
||||
taskList.value[taskIndex].cover_url = res.data.resposne.image;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`查询任务状态失败 (${taskId}):`, error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 轮询所有进行中的任务状态
|
||||
*/
|
||||
const pollTasksStatus = () => {
|
||||
// 获取所有进行中的任务
|
||||
const processingTasks = taskList.value.filter(task =>
|
||||
task.status === 'created' ||
|
||||
task.status === 'processing' ||
|
||||
task.status === 'queueing' ||
|
||||
task.status === 'running' ||
|
||||
task.status === 'PENDING'
|
||||
);
|
||||
|
||||
if (processingTasks.length === 0) {
|
||||
// 如果没有进行中的任务,停止轮询
|
||||
stopPolling();
|
||||
return;
|
||||
}
|
||||
|
||||
// 并发查询所有进行中任务的状态
|
||||
processingTasks.forEach(task => {
|
||||
updateTaskStatus(task.taskid);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 启动轮询
|
||||
*/
|
||||
const startPolling = () => {
|
||||
stopPolling();
|
||||
// 立即执行一次
|
||||
pollTasksStatus();
|
||||
// 定时轮询(30秒间隔)
|
||||
pollTimer = setInterval(() => {
|
||||
pollTasksStatus();
|
||||
}, POLL_INTERVAL * 1000);
|
||||
};
|
||||
|
||||
/**
|
||||
* 停止轮询
|
||||
*/
|
||||
const stopPolling = () => {
|
||||
if (pollTimer) {
|
||||
clearInterval(pollTimer);
|
||||
pollTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 加载任务列表
|
||||
* 从后端获取今日任务列表并转换为前端格式
|
||||
*/
|
||||
const loadTaskList = async () => {
|
||||
console.log('=== 开始加载任务列表 ===');
|
||||
|
||||
if (!isLoggedIn.value) {
|
||||
console.log('未登录,跳过加载');
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
try {
|
||||
console.log('正在调用 getTodayTask 接口...');
|
||||
// 调用任务列表接口
|
||||
const res = await getTodayTask<TaskResult>();
|
||||
|
||||
console.log('getTodayTask 响应:', res);
|
||||
|
||||
if (res.status === 'ok' && res.data?.tasks && Array.isArray(res.data.tasks)) {
|
||||
console.log('任务列表数据:', res.data.tasks);
|
||||
// 转换后端数据格式为前端期望的格式
|
||||
taskList.value = res.data.tasks.map((task) => {
|
||||
const output = task.ioinfo?.output || [];
|
||||
const finalOutput = output.find((o) => o.image && o.video) || output[output.length - 1] || {};
|
||||
const input = task.ioinfo?.input || {};
|
||||
|
||||
return {
|
||||
taskid: task.taskid,
|
||||
status: task.status?.toLowerCase() || finalOutput.status?.toLowerCase() || 'created',
|
||||
prompt: input.prompt,
|
||||
cover_url: finalOutput.image || undefined,
|
||||
video_url: finalOutput.video || undefined,
|
||||
duration: input.duration,
|
||||
resolution: input.resolution,
|
||||
llmid: input.llmid,
|
||||
model: input.model,
|
||||
use_time: task.use_time,
|
||||
action: finalOutput.usage?.action,
|
||||
} as TaskItem;
|
||||
});
|
||||
|
||||
console.log('转换后的任务列表:', taskList.value);
|
||||
|
||||
// 加载完成后,启动轮询(如果有进行中的任务)
|
||||
const hasProcessingTasks = taskList.value.some(task =>
|
||||
task.status === 'created' ||
|
||||
task.status === 'processing' ||
|
||||
task.status === 'queueing' ||
|
||||
task.status === 'running' ||
|
||||
task.status === 'PENDING'
|
||||
);
|
||||
console.log('是否有进行中的任务:', hasProcessingTasks);
|
||||
|
||||
if (hasProcessingTasks) {
|
||||
console.log('启动轮询...');
|
||||
startPolling();
|
||||
}
|
||||
} else {
|
||||
console.log('任务列表为空或格式错误');
|
||||
taskList.value = [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取任务列表失败:', error);
|
||||
taskList.value = [];
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
console.log('=== 任务列表加载完成 ===');
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 生命周期 ====================
|
||||
/**
|
||||
* 组件挂载时加载任务列表
|
||||
*/
|
||||
onMounted(() => {
|
||||
if (isLoggedIn.value) {
|
||||
loadTaskList();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 组件卸载时停止轮询
|
||||
*/
|
||||
onUnmounted(() => {
|
||||
stopPolling();
|
||||
});
|
||||
|
||||
/**
|
||||
* 监听登录状态变化
|
||||
* 登录时加载任务列表,登出时清空任务列表并停止轮询
|
||||
*/
|
||||
watch(() => loginStore.isLoggedIn, (newValue) => {
|
||||
if (newValue) {
|
||||
loadTaskList();
|
||||
} else {
|
||||
taskList.value = [];
|
||||
stopPolling();
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== 组件暴露 ====================
|
||||
/**
|
||||
* 向父组件暴露的方法
|
||||
*/
|
||||
defineExpose({
|
||||
refreshTaskList: loadTaskList // 刷新任务列表
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import url('../../assets/less/RightResult/RightResult.less');
|
||||
|
||||
/* 下载按钮禁用样式 */
|
||||
.icon-btn.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 下载图标悬停效果 */
|
||||
.icon-btn:not(.disabled):hover {
|
||||
opacity: 0.8;
|
||||
transform: scale(1.05);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
/* loading 图标动画 */
|
||||
.is-loading {
|
||||
animation: rotate 1.5s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* 生成中状态样式 */
|
||||
.generating-status {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #0F0F19;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.2);
|
||||
border-top: 3px solid #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.generating-text {
|
||||
font-size: 16px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
108
src/components/img/ImgLeft.vue
Normal file
108
src/components/img/ImgLeft.vue
Normal file
@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="left">
|
||||
<div class="tab">
|
||||
<ImgTap :activeTab="activeTab" @tabChange="handleTabChange" />
|
||||
</div>
|
||||
<div class="content">
|
||||
<ReferenceImg v-if="activeTab === 'reference'" />
|
||||
<TextImg v-else-if="activeTab === 'text'" />
|
||||
</div>
|
||||
<div className="create-button-container">
|
||||
<div
|
||||
type="primary"
|
||||
className="create-button"
|
||||
>
|
||||
创作
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang='ts'>
|
||||
import { ref, onMounted, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import ImgTap from './ImgTap.vue';
|
||||
import ReferenceImg from './ReferenceImg.vue';
|
||||
import TextImg from './TextImg.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
// 当前激活的tab
|
||||
const activeTab = ref('reference');
|
||||
|
||||
// 处理tab切换
|
||||
const handleTabChange = (tab: string) => {
|
||||
activeTab.value = tab;
|
||||
// 更新路由
|
||||
router.push(`/ImgGenerate/${tab}`);
|
||||
};
|
||||
|
||||
// 根据路由设置activeTab
|
||||
const updateActiveTabFromRoute = () => {
|
||||
const path = route.path;
|
||||
if (path.includes('/ImgGenerate/reference')) {
|
||||
activeTab.value = 'reference';
|
||||
} else if (path.includes('/ImgGenerate/text')) {
|
||||
activeTab.value = 'text';
|
||||
} else {
|
||||
// 默认显示reference
|
||||
activeTab.value = 'reference';
|
||||
}
|
||||
};
|
||||
|
||||
// 监听路由变化
|
||||
watch(() => route.path, () => {
|
||||
updateActiveTabFromRoute();
|
||||
});
|
||||
|
||||
// 组件挂载时初始化
|
||||
onMounted(() => {
|
||||
updateActiveTabFromRoute();
|
||||
});
|
||||
</script>
|
||||
<style scoped lang='less'>
|
||||
.left{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
// overflow-y: auto;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.create-button-container {
|
||||
z-index: 999;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
background: linear-gradient(180deg, rgba(2,11,19,0) 0%, #0F0F19 76.04%);
|
||||
|
||||
.create-button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
border-radius: 12px !important;
|
||||
background-image: linear-gradient(270deg, #94d8ff 11.11%, #e0ddff);
|
||||
border: none !important;
|
||||
color: #000 !important;
|
||||
font-size: 18px !important;
|
||||
font-weight: 600 !important;
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.02) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
84
src/components/img/ImgTap.vue
Normal file
84
src/components/img/ImgTap.vue
Normal file
@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="tabs-container ">
|
||||
<div
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'reference' }"
|
||||
@click="switchTab('reference')"
|
||||
>
|
||||
参考生图
|
||||
</div>
|
||||
<div
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'text' }"
|
||||
@click="switchTab('text')"
|
||||
>
|
||||
文生图
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang='ts'>
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
// 接收父组件传递的activeTab
|
||||
const props = defineProps<{
|
||||
activeTab: string;
|
||||
}>();
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits<{
|
||||
(e: 'tabChange', tab: string): void;
|
||||
}>();
|
||||
|
||||
// 切换tab
|
||||
const switchTab = (tab: string) => {
|
||||
emit('tabChange', tab);
|
||||
};
|
||||
</script>
|
||||
<style scoped lang='less'>
|
||||
.tabs-container {
|
||||
width: 100%;
|
||||
height: 44x;
|
||||
display: flex;
|
||||
background-color: #1a1c20;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
width: 50%;
|
||||
flex: 1;
|
||||
padding: 12px 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tab-item:hover {
|
||||
color: #fff;
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
color: #fff;
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.tab-item.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 20%;
|
||||
width: 60%;
|
||||
height: 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
</style>
|
||||
170
src/components/img/ReferenceImg.vue
Normal file
170
src/components/img/ReferenceImg.vue
Normal file
@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<el-card class="Topcard">
|
||||
<div class="card-top">
|
||||
<div class="top-left">
|
||||
<h2>参考生图</h2>
|
||||
<div class="desc">参考多图元素,生成全新图片</div>
|
||||
<div class="add">
|
||||
<div class="add-item">
|
||||
<i class="iconfont icon-jia"></i>
|
||||
<span>添加图片</span>
|
||||
</div>
|
||||
<div class="add-item">
|
||||
<i class="iconfont icon-jia"></i>
|
||||
<span>添加主体</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="top-right">
|
||||
<img src="https://image01.vidu.zone/vidu/media-asset/20250808-140539-3230cb2d.webp" alt="">
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-bottom">
|
||||
<el-input v-model="textarea2" class="input" :autosize="{ minRows: 10, maxRows: 10 }" type="textarea"
|
||||
placeholder="结合图片,描述想要生成的内容" />
|
||||
</div>
|
||||
</el-card>
|
||||
<el-card class="Bottomcard">
|
||||
<div class="select">
|
||||
<div class="select-item">
|
||||
<div class="select-item">
|
||||
<div class="title">
|
||||
模型
|
||||
</div>
|
||||
<div class="content">
|
||||
<el-select v-model="value" placeholder="Select" popper-class="custom-select-popup">
|
||||
<el-option v-for="item in options" :key="item.value" :label="item.label"
|
||||
:value="item.value" />
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border"></div>
|
||||
<div class="select">
|
||||
<div class="select-item">
|
||||
<div class="select-item">
|
||||
<div class="title">
|
||||
清晰度
|
||||
</div>
|
||||
<div class="content">
|
||||
<el-select v-model="value" placeholder="Select" popper-class="custom-select-popup">
|
||||
<el-option v-for="item in options" :key="item.value" :label="item.label"
|
||||
:value="item.value" />
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border"></div>
|
||||
|
||||
<div class="select">
|
||||
<div class="select-item">
|
||||
<div class="select-item">
|
||||
<div class="title">
|
||||
宽高比
|
||||
</div>
|
||||
<div class="content">
|
||||
<el-select v-model="value" placeholder="Select" popper-class="custom-select-popup">
|
||||
<el-option v-for="item in options" :key="item.value" :label="item.label"
|
||||
:value="item.value" />
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border"></div>
|
||||
|
||||
<div class="select">
|
||||
<div class="select-item">
|
||||
<div class="select-item">
|
||||
<div class="title">
|
||||
数量
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="quantity-buttons">
|
||||
<button
|
||||
v-for="num in [1, 2, 3, 4]"
|
||||
:key="num"
|
||||
class="quantity-btn"
|
||||
:class="{ active: selectedQuantity === num }"
|
||||
@click="selectedQuantity = num"
|
||||
>
|
||||
{{ num }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang='ts'>
|
||||
import { ref } from 'vue';
|
||||
const textarea2 = ref('')
|
||||
const value = ref('')
|
||||
const selectedQuantity = ref(1)
|
||||
const options = [
|
||||
{
|
||||
value: 'Option1',
|
||||
label: 'Option1',
|
||||
},
|
||||
{
|
||||
value: 'Option2',
|
||||
label: 'Option2',
|
||||
},
|
||||
{
|
||||
value: 'Option3',
|
||||
label: 'Option3',
|
||||
},
|
||||
{
|
||||
value: 'Option4',
|
||||
label: 'Option4',
|
||||
},
|
||||
{
|
||||
value: 'Option5',
|
||||
label: 'Option5',
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang='less'>
|
||||
@import url('../../assets/less/Img/ImgBox.less');
|
||||
</style>
|
||||
<style>
|
||||
.custom-select-popup {
|
||||
background-color: #2A2D34 !important;
|
||||
border-radius: 12px !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4) !important;
|
||||
border: none !important;
|
||||
|
||||
|
||||
.el-select-dropdown__item {
|
||||
color: #ffffff !important;
|
||||
background-color: transparent !important;
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
.el-select-dropdown__item.selected,
|
||||
.el-select-dropdown__item:hover {
|
||||
background-color: #3a3d46 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-select {
|
||||
.el-input__inner {
|
||||
background-color: #35383F !important;
|
||||
border: none !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.el-input__suffix-inner {
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
156
src/components/img/TextImg.vue
Normal file
156
src/components/img/TextImg.vue
Normal file
@ -0,0 +1,156 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<el-card class="Topcard">
|
||||
|
||||
<div class="card-bottom">
|
||||
<el-input v-model="textarea2" class="input" :autosize="{ minRows: 10, maxRows: 10 }" type="textarea"
|
||||
placeholder="结合图片,描述想要生成的内容" />
|
||||
</div>
|
||||
</el-card>
|
||||
<el-card class="Bottomcard">
|
||||
<div class="select">
|
||||
<div class="select-item">
|
||||
<div class="select-item">
|
||||
<div class="title">
|
||||
模型
|
||||
</div>
|
||||
<div class="content">
|
||||
<el-select v-model="value" placeholder="Select" popper-class="custom-select-popup">
|
||||
<el-option v-for="item in options" :key="item.value" :label="item.label"
|
||||
:value="item.value" />
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border"></div>
|
||||
<div class="select">
|
||||
<div class="select-item">
|
||||
<div class="select-item">
|
||||
<div class="title">
|
||||
清晰度
|
||||
</div>
|
||||
<div class="content">
|
||||
<el-select v-model="value" placeholder="Select" popper-class="custom-select-popup">
|
||||
<el-option v-for="item in options" :key="item.value" :label="item.label"
|
||||
:value="item.value" />
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border"></div>
|
||||
|
||||
<div class="select">
|
||||
<div class="select-item">
|
||||
<div class="select-item">
|
||||
<div class="title">
|
||||
宽高比
|
||||
</div>
|
||||
<div class="content">
|
||||
<el-select v-model="value" placeholder="Select" popper-class="custom-select-popup">
|
||||
<el-option v-for="item in options" :key="item.value" :label="item.label"
|
||||
:value="item.value" />
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border"></div>
|
||||
|
||||
<div class="select">
|
||||
<div class="select-item">
|
||||
<div class="select-item">
|
||||
<div class="title">
|
||||
数量
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="quantity-buttons">
|
||||
<button
|
||||
v-for="num in [1, 2, 3, 4]"
|
||||
:key="num"
|
||||
class="quantity-btn"
|
||||
:class="{ active: selectedQuantity === num }"
|
||||
@click="selectedQuantity = num"
|
||||
>
|
||||
{{ num }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang='ts'>
|
||||
import { ref } from 'vue';
|
||||
const textarea2 = ref('')
|
||||
const value = ref('')
|
||||
const selectedQuantity = ref(1)
|
||||
|
||||
const options = [
|
||||
{
|
||||
value: 'Option1',
|
||||
label: 'Option1',
|
||||
},
|
||||
{
|
||||
value: 'Option2',
|
||||
label: 'Option2',
|
||||
},
|
||||
{
|
||||
value: 'Option3',
|
||||
label: 'Option3',
|
||||
},
|
||||
{
|
||||
value: 'Option4',
|
||||
label: 'Option4',
|
||||
},
|
||||
{
|
||||
value: 'Option5',
|
||||
label: 'Option5',
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang='less'>
|
||||
@import url('../../assets/less/Img/ImgBox.less');
|
||||
.card-bottom{
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
.custom-select-popup {
|
||||
background-color: #2A2D34 !important;
|
||||
border-radius: 12px !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4) !important;
|
||||
border: none !important;
|
||||
|
||||
|
||||
.el-select-dropdown__item {
|
||||
color: #ffffff !important;
|
||||
background-color: transparent !important;
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
.el-select-dropdown__item.selected,
|
||||
.el-select-dropdown__item:hover {
|
||||
background-color: #3a3d46 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-select {
|
||||
.el-input__inner {
|
||||
background-color: #35383F !important;
|
||||
border: none !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.el-input__suffix-inner {
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
147
src/components/index/AIMode.vue
Normal file
147
src/components/index/AIMode.vue
Normal file
@ -0,0 +1,147 @@
|
||||
<template>
|
||||
<!-- AI视频功能选择栏 -->
|
||||
<div class="ai-video-function-bar">
|
||||
<!-- 功能按钮列表 - 横向滚动容器 -->
|
||||
<div class="function-btn-scroll-container">
|
||||
<!-- 功能按钮项 -->
|
||||
<button
|
||||
v-for="(item, index) in functionList"
|
||||
:key="index"
|
||||
class="function-btn-item"
|
||||
type="button"
|
||||
>
|
||||
<!-- 按钮文本区域 -->
|
||||
<div class="function-btn-text-wrapper">
|
||||
<div class="function-btn-text">{{ item.name }}</div>
|
||||
</div>
|
||||
<!-- 按钮图片区域 -->
|
||||
<img
|
||||
:src="item.imageUrl"
|
||||
:alt="item.name"
|
||||
class="function-btn-image"
|
||||
loading="lazy"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive } from 'vue';
|
||||
|
||||
// 定义功能列表数据类型
|
||||
interface FunctionItem {
|
||||
name: string;
|
||||
imageUrl: string;
|
||||
}
|
||||
|
||||
// 功能列表数据(可根据实际需求扩展)
|
||||
const functionList = reactive<FunctionItem[]>([
|
||||
{
|
||||
name: '参考生视频',
|
||||
imageUrl: 'https://image01.vidu.zone/vidu/media-asset/c2v-68811cfd.webp',
|
||||
},
|
||||
{
|
||||
name: '图生视频',
|
||||
imageUrl: 'https://image01.vidu.zone/vidu/media-asset/i2v-6e715ae0.webp',
|
||||
},
|
||||
{
|
||||
name: 'AI模板',
|
||||
imageUrl: 'https://image01.vidu.zone/vidu/media-asset/templates-bab5f63b.webp',
|
||||
},
|
||||
{
|
||||
name: '视频换脸',
|
||||
imageUrl: 'https://image01.vidu.zone/vidu/media-asset/face-7de6838a.webp',
|
||||
},
|
||||
{
|
||||
name: '视频复刻',
|
||||
imageUrl: 'https://image01.vidu.zone/vidu/media-asset/remake-09a26546.webp',
|
||||
},
|
||||
{
|
||||
name: '一键成片',
|
||||
imageUrl: 'https://image01.vidu.zone/vidu/media-asset/oneclick-beafcd9f.webp',
|
||||
},
|
||||
{
|
||||
name: '视频编辑',
|
||||
imageUrl: 'https://image01.vidu.zone/vidu/media-asset/videoedit-de4fb509.webp',
|
||||
},
|
||||
{
|
||||
name: '视频增强',
|
||||
imageUrl: 'https://image01.vidu.zone/vidu/media-asset/videoupscale-190d7537.webp',
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
// AI视频功能栏外层容器
|
||||
.ai-video-function-bar {
|
||||
padding-bottom: 0.25rem; // 对应原pb-1
|
||||
margin-top: 1.5rem;
|
||||
// 确保父容器宽度不受限制(默认就是块级元素宽度100%)
|
||||
}
|
||||
|
||||
// 横向滚动容器
|
||||
.function-btn-scroll-container {
|
||||
display: flex;
|
||||
gap: 1rem; // 对应原gap-4
|
||||
overflow-x: auto; // 允许水平滚动,并显示滚动条
|
||||
padding-bottom: 0.25rem; // 为滚动条预留空间,避免内容被遮挡
|
||||
|
||||
// 如果希望滚动条更美观,可自定义(但保留滚动功能)
|
||||
&::-webkit-scrollbar {
|
||||
height: 6px; // 滚动条高度
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(255, 255, 255, 0.3); // 滚动条颜色
|
||||
border-radius: 10px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
// 功能按钮项
|
||||
.function-btn-item {
|
||||
display: flex;
|
||||
min-width: 13.75rem; // 对应原min-w-[220px]
|
||||
flex: 1 0 13.75rem; // 对应原flex-[1_0_220px]
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem; // 对应原gap-3
|
||||
overflow: hidden;
|
||||
border-radius: 1rem; // 对应原rounded-16(假设16px=1rem)
|
||||
border: 1px solid rgba(255, 255, 255, 0.12); // 对应原border-system-white12
|
||||
background-color: rgba(255, 255, 255, 0.08); // 对应原bg-system-white08
|
||||
padding-left: 1rem; // 对应原pl-4
|
||||
text-align: left;
|
||||
transition: background-color 0.3s ease;
|
||||
cursor: pointer;
|
||||
|
||||
// hover状态
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.12); // 对应原hover:bg-system-white12
|
||||
}
|
||||
}
|
||||
|
||||
// 按钮文本容器
|
||||
.function-btn-text-wrapper {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
// 按钮文本样式
|
||||
.function-btn-text {
|
||||
font-weight: 600; // 对应原font-semibold
|
||||
font-size: 1rem; // 对应原text-base
|
||||
color: #ffffff; // 对应原text-white
|
||||
line-height: 1.4; // 对应原leading-[1.4]
|
||||
}
|
||||
|
||||
// 按钮图片样式
|
||||
.function-btn-image {
|
||||
height: 5.625rem; // 对应原h-[90px]
|
||||
width: 6.8125rem; // 对应原w-[109px]
|
||||
flex-shrink: 0;
|
||||
object-fit: cover;
|
||||
}
|
||||
</style>
|
||||
691
src/components/index/Stencil.vue
Normal file
691
src/components/index/Stencil.vue
Normal file
@ -0,0 +1,691 @@
|
||||
<template>
|
||||
<div class="content">
|
||||
<!-- 只替换这里的 Tab 结构,其他全部不动 -->
|
||||
<div class="stencil-container">
|
||||
<div class="stencil-top">
|
||||
<div class="stencil-item" :class="{ active: activeTab === 'mixed' }" @click="switchTab('mixed')">
|
||||
<div class="item-title">推荐</div>
|
||||
</div>
|
||||
<div class="stencil-item" :class="{ active: activeTab === 'share_elements' }"
|
||||
@click="switchTab('share_elements')">
|
||||
<div class="item-title reference-title">
|
||||
<img src="https://sfzas1.vidu.cn/2.17.51-cn/_next/static/media/refhub_zh.4622df68.svg" alt="reference"
|
||||
class="reference-icon" />
|
||||
<img src="https://sfzas1.vidu.cn/2.17.51-cn/_next/static/media/reference_2.0.bbfb4cad.svg"
|
||||
alt="reference_2.0" class="reference-sub-icon" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="stencil-item" :class="{ active: activeTab === 'short_film' }" @click="switchTab('short_film')">
|
||||
<div class="item-title">短片</div>
|
||||
</div>
|
||||
<div class="stencil-item" :class="{ active: activeTab === 'tutorial' }" @click="switchTab('tutorial')">
|
||||
<div class="item-title">教程</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ref="container" class="waterfall-container" :style="{
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
overflow: 'hidden auto',
|
||||
position: 'relative',
|
||||
willChange: 'transform',
|
||||
}">
|
||||
<!-- 加载遮罩层 -->
|
||||
<div v-if="loading && items.length > 0" class="loading-overlay">
|
||||
<div class="loading-overlay-spinner"></div>
|
||||
<span class="loading-overlay-text">加载中...</span>
|
||||
</div>
|
||||
|
||||
<div ref="innerScrollContainer" class="waterfall-inner" :style="{
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
height: `${containerHeight}px`,
|
||||
}">
|
||||
<div v-for="(item, index) in itemsWithPosition" :key="item.id" :data-index="index" class="waterfall-item"
|
||||
:style="{
|
||||
width: `${cardWidth}px`,
|
||||
position: 'absolute',
|
||||
top: `${item.top}px`,
|
||||
left: `${item.left}px`,
|
||||
}">
|
||||
<div class="card-wrapper">
|
||||
<div class="video-container" :style="{ aspectRatio: item.aspectRatio }"
|
||||
@mouseenter="handleMouseEnter(index)" @mouseleave="handleMouseLeave(index)">
|
||||
<video :ref="(el) => setVideoRef(index, el)" :src="item.src" :poster="item.poster" preload="none"
|
||||
class="video-element" :class="{ 'playing': isVideoPlaying[index] }" loop muted playsinline />
|
||||
<!-- 底部信息栏 -->
|
||||
<div class="card-info-overlay">
|
||||
<!-- 左侧:用户信息 -->
|
||||
<div class="user-info">
|
||||
<img v-if="item.avatar" :src="item.avatar" class="user-avatar" alt="avatar" />
|
||||
<span v-if="item.userName" class="user-name">{{ item.userName }}</span>
|
||||
</div>
|
||||
<!-- 右侧:点赞和收藏 -->
|
||||
<div class="interaction-info">
|
||||
<div v-if="item.likes > 0" class="interaction-item">
|
||||
<span class="icon-heart">♡</span>
|
||||
<span class="likes-count">{{ item.likes }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部加载指示器 -->
|
||||
<div ref="loadMoreTrigger" class="load-more-trigger">
|
||||
<span v-if="loading" class="loading-spinner"></span>
|
||||
<span v-else-if="hasMore">加载更多...</span>
|
||||
<span v-else>已经到底啦~</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, nextTick, watch } from 'vue'
|
||||
import { getVideos } from '@/apis/home'
|
||||
import { transformFeedItem, transformShareElementItem } from '@/utlis/transform'
|
||||
import type { FeedResponse, ShareElementsResponse } from '@/types/home'
|
||||
|
||||
// 定义卡片数据格式
|
||||
export interface StencilItem {
|
||||
id: string
|
||||
type: 'video'
|
||||
src: string
|
||||
poster: string
|
||||
aspectRatio: string // e.g., "1.77778/1"
|
||||
avatar: string
|
||||
userName: string
|
||||
likes: number
|
||||
averageColor?: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
gap?: number // 卡片间距,默认 16px
|
||||
columnCount?: number // 列数,默认根据屏幕宽度自动计算
|
||||
items?: StencilItem[] // 外部传入的卡片数据
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'like', item: StencilItem): void
|
||||
(e: 'makeSame', item: StencilItem): void
|
||||
(e: 'loadMore'): void
|
||||
}>()
|
||||
|
||||
// ==================== Tab 切换核心(仅新增) ====================
|
||||
const activeTab = ref('mixed') // 默认推荐
|
||||
|
||||
// Tab 切换方法
|
||||
const switchTab = (type: string) => {
|
||||
if (activeTab.value === type) return
|
||||
activeTab.value = type
|
||||
// 重置数据 & 重新加载
|
||||
items.value = []
|
||||
pageToken.value = null
|
||||
hasMore.value = true
|
||||
containerHeight.value = 0
|
||||
itemsWithPosition.value = []
|
||||
loadFirstPage()
|
||||
// 切换后滚动到顶部
|
||||
if (container.value) container.value.scrollTop = 0
|
||||
}
|
||||
|
||||
// ==================== 数据状态 ====================
|
||||
const items = ref<StencilItem[]>([])
|
||||
const loading = ref(false)
|
||||
const hasMore = ref(true)
|
||||
const pageToken = ref<string | null>(null)
|
||||
|
||||
// ==================== 视频播放控制 ====================
|
||||
const videoRefs = ref<(HTMLVideoElement | null)[]>([])
|
||||
const isVideoPlaying = ref<Record<number, boolean>>({})
|
||||
|
||||
// 设置视频引用
|
||||
const setVideoRef = (index: number, el: HTMLVideoElement | null) => {
|
||||
videoRefs.value[index] = el
|
||||
}
|
||||
|
||||
// 鼠标移入:播放视频
|
||||
const handleMouseEnter = (index: number) => {
|
||||
const video = videoRefs.value[index]
|
||||
if (video) {
|
||||
video.play().catch(err => {
|
||||
console.log('播放失败:', err)
|
||||
})
|
||||
isVideoPlaying.value[index] = true
|
||||
}
|
||||
}
|
||||
|
||||
// 鼠标移出:暂停视频
|
||||
const handleMouseLeave = (index: number) => {
|
||||
const video = videoRefs.value[index]
|
||||
if (video) {
|
||||
video.pause()
|
||||
video.currentTime = 0 // 重置到开头
|
||||
isVideoPlaying.value[index] = false
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 布局相关 ====================
|
||||
const container = ref<HTMLElement>()
|
||||
const innerScrollContainer = ref<HTMLElement>()
|
||||
const loadMoreTrigger = ref<HTMLElement>()
|
||||
const cardWidth = ref(0)
|
||||
const containerHeight = ref(0)
|
||||
const itemsWithPosition = ref<(StencilItem & { top: number; left: number; height: number })[]>([])
|
||||
|
||||
let columnHeights: number[] = []
|
||||
let resizeObserver: ResizeObserver | null = null
|
||||
let isLayoutPending = false
|
||||
let intersectionObserver: IntersectionObserver | null = null
|
||||
|
||||
// ==================== 辅助函数 ====================
|
||||
// 获取当前列数(优先使用 props,否则根据屏幕宽度)
|
||||
const getColumnCount = (): number => {
|
||||
if (props.columnCount !== undefined) return props.columnCount
|
||||
const width = window.innerWidth
|
||||
if (width >= 1280) return 4 // PC
|
||||
if (width >= 768) return 3 // 平板
|
||||
return 2 // 手机
|
||||
}
|
||||
|
||||
/* 核心布局算法 */
|
||||
const layout = async () => {
|
||||
if (!container.value || !innerScrollContainer.value || items.value.length === 0) return
|
||||
if (isLayoutPending) return
|
||||
isLayoutPending = true
|
||||
|
||||
try {
|
||||
const containerWidth = container.value.clientWidth
|
||||
const columnCount = getColumnCount()
|
||||
const gap = 4
|
||||
|
||||
// 计算卡片宽度
|
||||
cardWidth.value = (containerWidth - gap * (columnCount - 1)) / columnCount
|
||||
|
||||
// 重置列高度
|
||||
columnHeights = new Array(columnCount).fill(0)
|
||||
|
||||
// 降级:根据宽高比估算高度(更可靠)
|
||||
const heights: number[] = []
|
||||
for (const item of items.value) {
|
||||
const ratio = parseFloat(item.aspectRatio.split('/')[0]) / parseFloat(item.aspectRatio.split('/')[1])
|
||||
heights.push(cardWidth.value / ratio)
|
||||
}
|
||||
|
||||
// 计算每个卡片的位置
|
||||
const newItems = items.value.map((item, idx) => {
|
||||
const height = heights[idx] || 0
|
||||
// 找到当前最短列
|
||||
const minHeight = Math.min(...columnHeights)
|
||||
const minIndex = columnHeights.indexOf(minHeight)
|
||||
const left = minIndex * (cardWidth.value + gap)
|
||||
const top = columnHeights[minIndex]
|
||||
// 更新该列高度
|
||||
columnHeights[minIndex] += height + gap
|
||||
return { ...item, top, left, height }
|
||||
})
|
||||
|
||||
itemsWithPosition.value = newItems
|
||||
containerHeight.value = Math.max(...columnHeights) - gap
|
||||
} finally {
|
||||
isLayoutPending = false
|
||||
}
|
||||
}
|
||||
|
||||
// 媒体加载完成后重新布局
|
||||
// ==================== 数据加载 ====================
|
||||
// 加载第一页
|
||||
const loadFirstPage = async () => {
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res: FeedResponse | ShareElementsResponse = await getVideos({
|
||||
channel_type: activeTab.value // 自动使用当前 Tab 类型
|
||||
})
|
||||
|
||||
const cards: StencilItem[] = []
|
||||
|
||||
// 根据不同的数据格式进行转换
|
||||
if (activeTab.value === 'share_elements') {
|
||||
// 使用新的 share_elements 数据格式
|
||||
const shareRes = res as ShareElementsResponse
|
||||
for (const item of shareRes.share_elements) {
|
||||
const card = transformShareElementItem(item)
|
||||
if (card) cards.push(card)
|
||||
}
|
||||
pageToken.value = shareRes.next_page_token ?? null
|
||||
} else {
|
||||
// 使用原有的数据格式
|
||||
const feedRes = res as FeedResponse
|
||||
for (const item of feedRes.list) {
|
||||
const card = transformFeedItem(item)
|
||||
if (card) cards.push(card)
|
||||
}
|
||||
pageToken.value = feedRes.next_page_token ?? null
|
||||
}
|
||||
|
||||
items.value = cards
|
||||
hasMore.value = !!pageToken.value
|
||||
} catch (error) {
|
||||
console.error('获取视频列表失败', error)
|
||||
hasMore.value = false
|
||||
} finally {
|
||||
loading.value = false
|
||||
nextTick(() => {
|
||||
setupObserver()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
const loadMore = async () => {
|
||||
console.log('--- loadMore 开始 ---')
|
||||
console.log('loading:', loading.value, 'hasMore:', hasMore.value, 'pageToken:', pageToken.value)
|
||||
|
||||
if (loading.value || !hasMore.value) return
|
||||
if (!pageToken.value) {
|
||||
console.log('pageToken 为空,停止加载')
|
||||
hasMore.value = false
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
// 动态构建参数:只在 pageToken 有值时才传递 page_token
|
||||
const params: {
|
||||
channel_type: string;
|
||||
page_token?: string;
|
||||
} = {
|
||||
channel_type: activeTab.value
|
||||
}
|
||||
if (pageToken.value) {
|
||||
params.page_token = pageToken.value
|
||||
}
|
||||
|
||||
console.log('请求参数:', params)
|
||||
console.log('准备调用 getVideos...')
|
||||
|
||||
const res: FeedResponse | ShareElementsResponse = await getVideos(params)
|
||||
|
||||
console.log('getVideos 调用成功!')
|
||||
console.log('接口返回数据:', res)
|
||||
|
||||
const newCards: StencilItem[] = []
|
||||
|
||||
// 根据不同的数据格式进行转换
|
||||
if (activeTab.value === 'share_elements') {
|
||||
// 使用新的 share_elements 数据格式
|
||||
const shareRes = res as ShareElementsResponse
|
||||
console.log('返回的 share_elements 长度:', shareRes.share_elements?.length)
|
||||
console.log('返回的 next_page_token:', shareRes.next_page_token)
|
||||
|
||||
for (const item of shareRes.share_elements) {
|
||||
const card = transformShareElementItem(item)
|
||||
if (card) newCards.push(card)
|
||||
}
|
||||
pageToken.value = shareRes.next_page_token ?? null
|
||||
} else {
|
||||
// 使用原有的数据格式
|
||||
const feedRes = res as FeedResponse
|
||||
console.log('返回的 list 长度:', feedRes.list?.length)
|
||||
console.log('返回的 next_page_token:', feedRes.next_page_token)
|
||||
|
||||
for (const item of feedRes.list) {
|
||||
const card = transformFeedItem(item)
|
||||
if (card) newCards.push(card)
|
||||
}
|
||||
pageToken.value = feedRes.next_page_token ?? null
|
||||
}
|
||||
|
||||
console.log('转换后的卡片数量:', newCards.length)
|
||||
|
||||
items.value = [...items.value, ...newCards]
|
||||
hasMore.value = !!pageToken.value
|
||||
|
||||
console.log('更新后 - items 长度:', items.value.length, 'pageToken:', pageToken.value, 'hasMore:', hasMore.value)
|
||||
} catch (error) {
|
||||
console.error('加载更多失败,错误详情:', error)
|
||||
console.error('错误类型:', typeof error)
|
||||
if (error instanceof Error) {
|
||||
console.error('错误消息:', error.message)
|
||||
console.error('错误堆栈:', error.stack)
|
||||
} else if (error && typeof error === 'object') {
|
||||
const errObj = error as Record<string, unknown>;
|
||||
console.error('错误响应:', errObj.response)
|
||||
console.error('错误请求:', errObj.request)
|
||||
}
|
||||
console.error('完整错误对象:', JSON.stringify(error))
|
||||
hasMore.value = false
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
console.log('--- loadMore 结束 ---')
|
||||
}
|
||||
|
||||
// ==================== 滚动监听 ====================
|
||||
// 设置 IntersectionObserver 监听底部
|
||||
const setupObserver = () => {
|
||||
if (intersectionObserver) {
|
||||
intersectionObserver.disconnect()
|
||||
}
|
||||
if (!loadMoreTrigger.value) {
|
||||
console.warn('loadMoreTrigger 元素不存在')
|
||||
return
|
||||
}
|
||||
intersectionObserver = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const entry = entries[0]
|
||||
if (entry.isIntersecting && !loading.value && hasMore.value) {
|
||||
emit('loadMore')
|
||||
loadMore()
|
||||
}
|
||||
},
|
||||
{
|
||||
root: container.value,
|
||||
threshold: 0.1,
|
||||
}
|
||||
)
|
||||
intersectionObserver.observe(loadMoreTrigger.value)
|
||||
}
|
||||
|
||||
// ==================== 响应式监听 ====================
|
||||
// 响应窗口尺寸变化
|
||||
let resizeTimer: number
|
||||
const handleResize = () => {
|
||||
if (resizeTimer) clearTimeout(resizeTimer)
|
||||
resizeTimer = window.setTimeout(() => {
|
||||
layout()
|
||||
}, 100)
|
||||
}
|
||||
|
||||
// 监听内部 items 变化,重新布局
|
||||
watch(
|
||||
() => items.value,
|
||||
() => {
|
||||
nextTick(() => layout())
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
// 监听列数变化
|
||||
watch(
|
||||
() => getColumnCount(),
|
||||
() => {
|
||||
layout()
|
||||
}
|
||||
)
|
||||
|
||||
// ==================== 生命周期 ====================
|
||||
onMounted(() => {
|
||||
loadFirstPage()
|
||||
window.addEventListener('resize', handleResize)
|
||||
|
||||
if (window.ResizeObserver && container.value) {
|
||||
resizeObserver = new ResizeObserver(() => layout())
|
||||
resizeObserver.observe(container.value)
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
setupObserver()
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
if (resizeObserver) resizeObserver.disconnect()
|
||||
if (intersectionObserver) intersectionObserver.disconnect()
|
||||
if (resizeTimer) clearTimeout(resizeTimer)
|
||||
})
|
||||
|
||||
defineExpose({ refreshLayout: layout })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.user-avatar{
|
||||
width: 20px!important;
|
||||
height: 20px!important;
|
||||
}
|
||||
/* 把原来的 .tab 相关样式删掉,换成下面这些 */
|
||||
.stencil-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.stencil-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.stencil-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.stencil-item.active .item-title {
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
font-size: 16px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.reference-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.reference-icon,
|
||||
.reference-sub-icon {
|
||||
height: 20px;
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* 你原来的其他样式原样保留 */
|
||||
.waterfall-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.waterfall-item .relative {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.waterfall-item img,
|
||||
.waterfall-item video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.video-element {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 卡片容器 */
|
||||
.card-wrapper {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 视频容器 */
|
||||
.video-container {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
/* 底部信息遮罩 */
|
||||
.card-info-overlay {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
/* background: rgba(74, 79, 90, 0.75); */
|
||||
padding: 10px 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
backdrop-filter: blur(2px);
|
||||
transition: all 0.2s ease;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* 视频容器hover效果 */
|
||||
.video-container:hover .card-info-overlay {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
/* background: rgba(80, 85, 96, 0.85); */
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.video-container:hover .user-avatar {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
/* border-color: rgba(255, 255, 255, 0.9); */
|
||||
}
|
||||
|
||||
.video-container:hover .user-name {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.video-container:hover .icon-heart {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.video-container:hover .likes-count {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* 用户信息区 */
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
font-weight: 400;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 互动信息区 */
|
||||
.interaction-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.interaction-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.icon-heart {
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.likes-count {
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.load-more-trigger {
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.2);
|
||||
border-top-color: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* 加载遮罩层 */
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
z-index: 10;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.loading-overlay-spinner {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.2);
|
||||
border-top-color: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
.loading-overlay-text {
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.rounded-4 {
|
||||
border-radius: 14px;
|
||||
}
|
||||
</style>
|
||||
485
src/components/index/top.vue
Normal file
485
src/components/index/top.vue
Normal file
@ -0,0 +1,485 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="top">
|
||||
<!-- 左侧菜单(浮层,半透明) -->
|
||||
<div class="top-left">
|
||||
<div class="left-item">
|
||||
<div class="title">参考生视频</div>
|
||||
<div class="detail">万物皆可自由组合搭配,无限创意</div>
|
||||
<div class="item-btn">
|
||||
<div class="btn" @click="$router.push('/videoGenerate/reference')">立即使用</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="left-item">
|
||||
<div class="title">图生视频</div>
|
||||
<div class="detail">20s让图片动起来,首尾帧无缝融合,过渡丝滑自然</div>
|
||||
<div class="item-btn">
|
||||
<div class="btn" @click="$router.push('/videoGenerate/image')">立即使用</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧轮播(使用 Swiper) -->
|
||||
<div class="top-right" @mouseenter="pauseAutoPlay" @mouseleave="startAutoPlay">
|
||||
<swiper :modules="modules" :slides-per-view="1" :space-between="0" :loop="true"
|
||||
:autoplay="{ delay: AUTO_PLAY_INTERVAL, disableOnInteraction: false }"
|
||||
:pagination="{ clickable: true, el: '.custom-pagination' }" @swiper="onSwiper" @slideChange="onSlideChange">
|
||||
<swiper-slide v-for="(slide, index) in slides" :key="slide.id">
|
||||
<div class="carousel-slide">
|
||||
<!-- 背景层:当前项显示视频,其他项显示封面图片 -->
|
||||
<div class="slide-background">
|
||||
<video v-if="index === currentIndex && slide.videoSrc" :src="slide.videoSrc" autoplay loop muted
|
||||
playsinline class="background-media"></video>
|
||||
<img v-else :src="slide.poster" alt="cover" class="background-media" />
|
||||
</div>
|
||||
|
||||
<!-- 右侧小图列表(带按钮) -->
|
||||
<div class="right-thumbnails">
|
||||
<template v-for="(item, idx) in slide.rightItems" :key="idx">
|
||||
<div v-if="item.type === 'image'" class="thumbnail-img" @click="goToSlide(index)">
|
||||
<img :src="item.src" alt="thumb" />
|
||||
</div>
|
||||
<div v-else-if="item.type === 'button'" class="thumbnail-btn"
|
||||
:class="{ 'play-btn': item.shape === 'play' }">
|
||||
<svg v-if="item.shape === 'plus'" width="1em" height="1em" viewBox="0 0 25 25" fill="none">
|
||||
<path d="M12.28 5.96875L12.2617 19.9688" stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M5.25 12.9688H19.25" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
</svg>
|
||||
<svg v-else-if="item.shape === 'play'" width="1em" height="1em" viewBox="0 0 24 24" fill="none">
|
||||
<path
|
||||
d="M6.25391 12V5.36603C6.25391 4.98113 6.67057 4.74056 7.00391 4.93301L12.7491 8.25L18.4942 11.567C18.8276 11.7594 18.8276 12.2406 18.4942 12.433L12.7491 15.75L7.00391 19.067C6.67057 19.2594 6.25391 19.0189 6.25391 18.634V12Z"
|
||||
fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 底部文字 -->
|
||||
<div class="bottom-text">
|
||||
<div class="subtitle">{{ slide.subtitle }}</div>
|
||||
<div class="title">{{ slide.title }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</swiper-slide>
|
||||
</swiper>
|
||||
|
||||
<!-- 全局自定义分页器容器(放在轮播容器底部,和左侧菜单对齐) -->
|
||||
<div class="custom-pagination"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onBeforeUnmount } from 'vue'
|
||||
import { Swiper, SwiperSlide } from 'swiper/vue'
|
||||
import { Autoplay, Pagination } from 'swiper/modules'
|
||||
import 'swiper/swiper-bundle.css'
|
||||
|
||||
/**
|
||||
* 右侧缩略图项类型
|
||||
*/
|
||||
interface RightItem {
|
||||
type: 'image' | 'button' // 类型:图片或按钮
|
||||
src?: string // 图片源地址(仅图片类型)
|
||||
shape?: 'plus' | 'play' // 按钮形状(仅按钮类型)
|
||||
}
|
||||
|
||||
/**
|
||||
* 轮播幻灯片数据类型
|
||||
*/
|
||||
interface SlideData {
|
||||
id: number // 幻灯片唯一标识
|
||||
poster: string // 封面图片地址
|
||||
videoSrc: string // 视频源地址
|
||||
subtitle: string // 副标题
|
||||
title: string // 主标题
|
||||
rightItems: RightItem[] // 右侧缩略图列表
|
||||
}
|
||||
|
||||
/**
|
||||
* Swiper 实例类型定义
|
||||
* 包含常用的 Swiper 方法和属性
|
||||
*/
|
||||
interface SwiperInstance {
|
||||
autoplay: {
|
||||
stop: () => void // 停止自动播放
|
||||
start: () => void // 开始自动播放
|
||||
}
|
||||
slideToLoop: (index: number, speed?: number, runCallbacks?: boolean) => void // 跳转到指定幻灯片(支持循环模式)
|
||||
}
|
||||
|
||||
// 示例数据
|
||||
const slides: SlideData[] = [
|
||||
{
|
||||
id: 1,
|
||||
poster: 'https://image01.vidu.zone/vidu/media-asset/R2V1-poster-1-517a8d5f.webp',
|
||||
videoSrc: 'https://image01.vidu.zone/vidu/media-asset/R2V1-218ca30f.mp4',
|
||||
subtitle: '参考生视频',
|
||||
title: '短片',
|
||||
rightItems: [
|
||||
{ type: 'image', src: 'https://image01.vidu.zone/vidu/media-asset/R2V1-img-1-e607d496.webp' },
|
||||
{ type: 'button', shape: 'plus' },
|
||||
{ type: 'image', src: 'https://image01.vidu.zone/vidu/media-asset/R2V1-img-2-1b75e6ed.webp' },
|
||||
{ type: 'button', shape: 'plus' },
|
||||
{ type: 'image', src: 'https://image01.vidu.zone/vidu/media-asset/R2V1-img-3-89cf0518.webp' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
poster: 'https://image01.vidu.zone/vidu/media-asset/R2V2-poster-1-8a2c64f9.webp',
|
||||
videoSrc: 'https://image01.vidu.zone/vidu/media-asset/R2V2-279a047f.mp4',
|
||||
subtitle: '参考生视频',
|
||||
title: '创意',
|
||||
rightItems: [
|
||||
{ type: 'image', src: 'https://image01.vidu.zone/vidu/media-asset/R2V2-img-1-a636a740.webp' },
|
||||
{ type: 'button', shape: 'plus' },
|
||||
{ type: 'image', src: 'https://image01.vidu.zone/vidu/media-asset/R2V2-img-2-c5dac1e5.webp' },
|
||||
{ type: 'button', shape: 'plus' },
|
||||
{ type: 'image', src: 'https://image01.vidu.zone/vidu/media-asset/R2V2-img-3-908d7ec6.webp' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
poster: 'https://image01.vidu.zone/vidu/media-asset/R2V3-poster-1-7664cd7a.webp',
|
||||
videoSrc: 'https://image01.vidu.zone/vidu/media-asset/R2V3-c79fd401.mp4',
|
||||
subtitle: '参考生视频',
|
||||
title: '广告 / 电商',
|
||||
rightItems: [
|
||||
{ type: 'image', src: 'https://image01.vidu.zone/vidu/media-asset/R2V3-img-1-27138837.webp' },
|
||||
{ type: 'button', shape: 'plus' },
|
||||
{ type: 'image', src: 'https://image01.vidu.zone/vidu/media-asset/R2V3-img-2-72f1929e.webp' },
|
||||
{ type: 'button', shape: 'plus' },
|
||||
{ type: 'image', src: 'https://image01.vidu.zone/vidu/media-asset/R2V3-img-3-71250e7f.webp' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
poster: 'https://image01.vidu.zone/vidu/media-asset/I2V3-poster-1-b7a7f198.webp',
|
||||
videoSrc: 'https://image01.vidu.zone/vidu/media-asset/I2V3-837996ba.mp4',
|
||||
subtitle: '图生视频',
|
||||
title: '故事',
|
||||
rightItems: [
|
||||
{ type: 'image', src: 'https://image01.vidu.zone/vidu/media-asset/I2V3-img-1-dfff7f10.webp' },
|
||||
{ type: 'button', shape: 'play' },
|
||||
{ type: 'image', src: 'https://image01.vidu.zone/vidu/media-asset/I2V3-img-2-507d7080.webp' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
poster: 'https://image01.vidu.zone/vidu/media-asset/I2V2-poster-1-1e73ffc8.webp',
|
||||
videoSrc: 'https://image01.vidu.zone/vidu/media-asset/I2V2-74201655.mp4',
|
||||
subtitle: '图生视频',
|
||||
title: '广告 / 电商',
|
||||
rightItems: [
|
||||
{ type: 'image', src: 'https://image01.vidu.zone/vidu/media-asset/I2V2-img-1-3a3ff1a7.webp' }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
// Swiper 模块配置
|
||||
const modules = [Autoplay, Pagination]
|
||||
|
||||
// 自动播放间隔时间(毫秒)
|
||||
const AUTO_PLAY_INTERVAL = 5000
|
||||
|
||||
// 当前显示的幻灯片索引
|
||||
const currentIndex = ref(0)
|
||||
|
||||
// Swiper 实例引用
|
||||
const swiperInstance = ref<SwiperInstance | null>(null)
|
||||
|
||||
/**
|
||||
* 保存 Swiper 实例
|
||||
* @param swiper - Swiper 实例对象
|
||||
*/
|
||||
const onSwiper = (swiper: SwiperInstance) => {
|
||||
swiperInstance.value = swiper
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听幻灯片切换事件
|
||||
* 更新当前索引(使用 realIndex 以支持循环模式)
|
||||
* @param swiper - Swiper 实例对象,包含 realIndex 属性
|
||||
*/
|
||||
const onSlideChange = (swiper: SwiperInstance & { realIndex: number }) => {
|
||||
currentIndex.value = swiper.realIndex
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转到指定幻灯片
|
||||
* @param index - 目标幻灯片索引
|
||||
*/
|
||||
const goToSlide = (index: number) => {
|
||||
// 如果已经是当前幻灯片,不执行跳转
|
||||
if (index === currentIndex.value) return
|
||||
// 使用 slideToLoop 方法支持循环模式
|
||||
swiperInstance.value?.slideToLoop(index)
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停自动播放
|
||||
*/
|
||||
const pauseAutoPlay = () => {
|
||||
swiperInstance.value?.autoplay.stop()
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动自动播放
|
||||
*/
|
||||
const startAutoPlay = () => {
|
||||
swiperInstance.value?.autoplay.start()
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件卸载前清理
|
||||
* 停止自动播放,避免内存泄漏
|
||||
*/
|
||||
onBeforeUnmount(() => {
|
||||
pauseAutoPlay()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.top {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 260px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.top-left {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 248px;
|
||||
padding: 8px;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(25px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
z-index: 2;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.left-item {
|
||||
height: 118px;
|
||||
box-sizing: border-box;
|
||||
padding: 16px 24px;
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
transition: background-color 0.2s;
|
||||
cursor: pointer;
|
||||
background-color: transparent;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.detail {
|
||||
font-size: 12px;
|
||||
opacity: 0.4;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.item-btn {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
.btn {
|
||||
font-size: 12px;
|
||||
padding: 2px 16px;
|
||||
background-color: rgba(255, 255, 255, 0.16);
|
||||
border-radius: 16px;
|
||||
line-height: 24px;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.top-right {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background-color: transparent;
|
||||
z-index: 1;
|
||||
|
||||
// Swiper 容器样式
|
||||
.swiper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.swiper-wrapper {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.carousel-slide {
|
||||
flex: 0 0 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.slide-background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
|
||||
.background-media {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.right-thumbnails {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 8px;
|
||||
bottom: 0;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
pointer-events: none;
|
||||
|
||||
.thumbnail-img {
|
||||
pointer-events: auto;
|
||||
width: 80px;
|
||||
height: 48px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.thumbnail-btn {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(255, 255, 255, 0.16);
|
||||
backdrop-filter: blur(10px);
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
|
||||
&.play-btn {
|
||||
background-color: rgba(255, 255, 255, 0.24);
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-text {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 120px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 4px;
|
||||
background: linear-gradient(0deg, rgba(0, 0, 0, 0.4) 0%, rgba(0, 0, 0, 0) 100%);
|
||||
padding-bottom: 24px;
|
||||
padding-left: 248px;
|
||||
color: white;
|
||||
z-index: 5;
|
||||
|
||||
.subtitle {
|
||||
font-size: 16px;
|
||||
line-height: 16px;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.64) 0.33%, rgba(255, 255, 255, 0.64) 99.67%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
line-height: 22px;
|
||||
background: linear-gradient(180deg, #fff 0.33%, #fff 99.67%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
// 全局自定义分页器容器样式
|
||||
.custom-pagination {
|
||||
position: absolute;
|
||||
left: 54%;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
/* 全局样式修改分页器外观 */
|
||||
.swiper-pagination-bullet {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
opacity: 1;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.swiper-pagination-bullet-active {
|
||||
width: 24px;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
}
|
||||
</style>
|
||||
297
src/components/left/Menu.vue
Normal file
297
src/components/left/Menu.vue
Normal file
@ -0,0 +1,297 @@
|
||||
<template>
|
||||
<div class="left">
|
||||
<!-- 可滚动菜单区域 -->
|
||||
<div class="sidebar-scroll">
|
||||
<!-- 主导航 -->
|
||||
<div class="main-navigation mb-6">
|
||||
<el-menu
|
||||
:default-active="activeIndex"
|
||||
class="custom-menu"
|
||||
background-color="transparent"
|
||||
text-color="#fff"
|
||||
active-text-color="#00FFCC"
|
||||
@select="handleSelect"
|
||||
>
|
||||
<el-menu-item index="index">
|
||||
<div class="menu-item-content">
|
||||
<i class="iconfont icon-shouye"></i>
|
||||
<span class="menu-text">首页</span>
|
||||
</div>
|
||||
</el-menu-item>
|
||||
|
||||
</el-menu>
|
||||
</div>
|
||||
|
||||
<!-- AI 创作分组 -->
|
||||
<div class="ai-creation-menu mb-8">
|
||||
<div class="menu-title">AI 创作</div>
|
||||
<el-menu
|
||||
:default-active="activeIndex"
|
||||
class="custom-menu"
|
||||
background-color="transparent"
|
||||
text-color="#fff"
|
||||
active-text-color="#00FFCC"
|
||||
@select="handleSelect"
|
||||
>
|
||||
|
||||
<el-menu-item index="/videoGenerate">
|
||||
<div class="menu-item-content">
|
||||
<i class="iconfont icon-img2video"></i>
|
||||
<span class="menu-text">视频生成</span>
|
||||
</div>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/ImgGenerate">
|
||||
<div class="menu-item-content">
|
||||
<i class="iconfont icon-tupianshengcheng"></i>
|
||||
<span class="menu-text">图片生成</span>
|
||||
</div>
|
||||
</el-menu-item>
|
||||
|
||||
</el-menu>
|
||||
</div>
|
||||
|
||||
<!-- 创作工具分组 -->
|
||||
<div class="tool-navigation mb-8">
|
||||
<div class="menu-title">创作工具</div>
|
||||
<el-menu
|
||||
:default-active="activeIndex"
|
||||
class="custom-menu"
|
||||
background-color="transparent"
|
||||
text-color="#fff"
|
||||
active-text-color="#00FFCC"
|
||||
@select="handleSelect"
|
||||
>
|
||||
<el-menu-item index="material-square">
|
||||
<div class="menu-item-content">
|
||||
<i class="iconfont icon-image-edit"></i>
|
||||
<span class="menu-text">素材广场</span>
|
||||
</div>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</div>
|
||||
|
||||
<!-- 底部占位空白 -->
|
||||
<div class="pb-20"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
|
||||
// 路由实例
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
// 根据当前路由计算激活的菜单项索引
|
||||
const activeIndex = ref('index')
|
||||
|
||||
// 菜单项点击事件
|
||||
const handleSelect = (index: string) => {
|
||||
activeIndex.value = index
|
||||
|
||||
// 处理路由跳转
|
||||
switch (index) {
|
||||
case 'index':
|
||||
router.push('/index');
|
||||
break;
|
||||
case 'discover':
|
||||
router.push('/discover');
|
||||
break;
|
||||
case 'profile':
|
||||
router.push('/profile');
|
||||
break;
|
||||
case 'infinite-canvas':
|
||||
router.push('/infinite-canvas');
|
||||
break;
|
||||
case '/videoGenerate':
|
||||
router.push('/videoGenerate');
|
||||
break;
|
||||
case '/ImgGenerate':
|
||||
router.push('/ImgGenerate');
|
||||
break;
|
||||
case 'digital-human':
|
||||
router.push('/digital-human');
|
||||
break;
|
||||
case 'material-square':
|
||||
router.push('/material-square');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 监听路由变化,更新激活状态
|
||||
const updateActiveIndex = () => {
|
||||
const path = route.path
|
||||
if (path.includes('/index')) {
|
||||
activeIndex.value = 'index';
|
||||
} else if (path.includes('/discover')) {
|
||||
activeIndex.value = 'discover';
|
||||
} else if (path.includes('/profile')) {
|
||||
activeIndex.value = 'profile';
|
||||
} else if (path.includes('/infinite-canvas')) {
|
||||
activeIndex.value = 'infinite-canvas';
|
||||
} else if (path.includes('/videoGenerate')) {
|
||||
activeIndex.value = '/videoGenerate';
|
||||
} else if (path.includes('/ImgGenerate')) {
|
||||
activeIndex.value = '/ImgGenerate';
|
||||
} else if (path.includes('/digital-human')) {
|
||||
activeIndex.value = 'digital-human';
|
||||
} else if (path.includes('/material-square')) {
|
||||
activeIndex.value = 'material-square';
|
||||
} else {
|
||||
activeIndex.value = 'index';
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时更新激活状态
|
||||
onMounted(() => {
|
||||
updateActiveIndex()
|
||||
|
||||
// 监听路由变化
|
||||
router.afterEach(() => {
|
||||
updateActiveIndex()
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.left {
|
||||
height: calc(100vh - 80px);
|
||||
padding: 0 24px;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.sidebar-scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// 菜单标题
|
||||
.menu-title {
|
||||
margin-bottom: 16px;
|
||||
padding-left: 16px;
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
// 自定义菜单样式覆盖
|
||||
:deep(.custom-menu) {
|
||||
border-right: none;
|
||||
background-color: transparent;
|
||||
|
||||
.el-menu-item {
|
||||
height: auto;
|
||||
line-height: normal;
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 4px;
|
||||
border-radius: 8px;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background: linear-gradient(90deg, #2cfff84d, #2c68ff4d);
|
||||
|
||||
.menu-text {
|
||||
color: #00FFCC;
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
color: #00FFCC;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.menu-text {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 菜单项内容布局
|
||||
.menu-item-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
.iconfont {
|
||||
font-size: 20px;
|
||||
margin-right: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.menu-text {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
// 标签样式
|
||||
.menu-tag {
|
||||
margin-left: 8px;
|
||||
padding: 2px 8px;
|
||||
font-size: 10px;
|
||||
border-radius: 12px;
|
||||
background-color: #2563eb;
|
||||
color: #fff;
|
||||
white-space: nowrap;
|
||||
|
||||
&.new {
|
||||
background-color: #2563eb;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
// 底部空白
|
||||
.pb-20 {
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
// 辅助类
|
||||
.mb-6 {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.mb-8 {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
333
src/components/video/ImgVideo.vue
Normal file
333
src/components/video/ImgVideo.vue
Normal file
@ -0,0 +1,333 @@
|
||||
<template>
|
||||
<div class="reference-to-video">
|
||||
<!-- 顶部卡片 -->
|
||||
<el-card class="card">
|
||||
<div class="reference-container">
|
||||
<!-- 上传模块 -->
|
||||
|
||||
|
||||
<!-- 文本输入框 -->
|
||||
<div class="input">
|
||||
<el-input type="textarea" :rows="10" maxlength="400"
|
||||
placeholder="支持上传图片、视频或主体作为参考。可参考视频中的动作、表情、运镜、特效、风格与构图等,并支持对元素进行增删和修改" />
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 底部配置卡片 -->
|
||||
<el-card class="btmcard">
|
||||
<div class="select">
|
||||
<!-- 模型 -->
|
||||
<div class="select-item">
|
||||
<div class="select-title">模型</div>
|
||||
<div class="select-content">
|
||||
<el-select v-model="selectModel" style="width: 100%" @change="handleChange"
|
||||
popper-class="custom-select-popup">
|
||||
<el-option label="Jack" value="jack" />
|
||||
<el-option label="Lucy" value="lucy" />
|
||||
<el-option label="yiminghe" value="Yiminghe" />
|
||||
<el-option label="Disabled" value="disabled" disabled />
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- 清晰度 & 编码 -->
|
||||
<div class="select-item">
|
||||
<div class="select-title">
|
||||
清晰度<span class="and">&</span>编码格式
|
||||
</div>
|
||||
<div class="select-content">
|
||||
<el-select v-model="selectQuality" style="width: 50%" @change="handleChange"
|
||||
popper-class="custom-select-popup">
|
||||
<el-option label="1080P" value="1080P" />
|
||||
</el-select>
|
||||
<el-select v-model="selectCodec" style="width: 50%" @change="handleChange"
|
||||
popper-class="custom-select-popup">
|
||||
<el-option label="H265" value="H265" />
|
||||
<el-option label="H264" value="H264" />
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 生成模式 -->
|
||||
<div class="select-item">
|
||||
<div class="select-title">生成模式</div>
|
||||
<div class="select-content">
|
||||
<el-select v-model="selectModelviedo" style="width: 100%" @change="handleChange"
|
||||
popper-class="custom-select-popup">
|
||||
<el-option label="电影大片" value="电影大片" />
|
||||
</el-select>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 数量 -->
|
||||
<div class="select-item">
|
||||
<div class="select-title">数量</div>
|
||||
<div class="radio-group">
|
||||
<button v-for="num in [1, 2, 3, 4]" :key="num" class="radio-item"
|
||||
:class="{ selected: selectedCount === num }" @click="selectedCount = num" type="button">
|
||||
{{ num }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, defineEmits } from 'vue'
|
||||
|
||||
// 定义 emit 事件
|
||||
const emit = defineEmits<{
|
||||
(e: 'price-update', price: number): void;
|
||||
(e: 'prompt-change', isEmpty: boolean): void;
|
||||
}>();
|
||||
|
||||
// 下拉选中项
|
||||
const selectModel = ref('lucy')
|
||||
const selectQuality = ref('1080P')
|
||||
const selectCodec = ref('H265')
|
||||
const selectModelviedo = ref('电影大片')
|
||||
|
||||
// 单选按钮
|
||||
const selectedDuration = ref(5)
|
||||
const selectedCount = ref(1)
|
||||
|
||||
// 价格计算
|
||||
const price = computed(() => {
|
||||
const basePrice = selectedDuration.value > 5 ? 2 : 1
|
||||
return basePrice * selectedCount.value
|
||||
})
|
||||
|
||||
// 监听价格变化
|
||||
watch(price, (newPrice) => {
|
||||
emit('price-update', newPrice)
|
||||
})
|
||||
|
||||
// 统一 change 事件
|
||||
const handleChange = (value: string) => {
|
||||
console.log('selected', value)
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
:deep(.el-select__wrapper) {
|
||||
|
||||
width: 100% !important;
|
||||
border: none !important;
|
||||
background-color: rgba(0, 0, 0, 0.32) !important;
|
||||
box-shadow: none !important;
|
||||
border-radius: 0 !important;
|
||||
font-size: 14px;
|
||||
color: #fff !important;
|
||||
resize: vertical;
|
||||
outline: none !important;
|
||||
border-radius: 10px !important;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
:deep(.el-select__selected-item) {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
:deep(.el-textarea) {
|
||||
|
||||
.el-textarea__inner {
|
||||
|
||||
border: none !important;
|
||||
background-color: rgba(0, 0, 0, 0.32) !important;
|
||||
box-shadow: none !important;
|
||||
border-radius: 0 !important;
|
||||
padding: 12px 0 !important;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: #fff !important;
|
||||
resize: vertical;
|
||||
outline: none !important;
|
||||
border-radius: 10px !important;
|
||||
color: #fff;
|
||||
backdrop-filter: blur(30px);
|
||||
padding: 14px 16px !important;
|
||||
|
||||
&:focus {
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: #ffffff7a !important;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
// 确保没有任何附加的边框或装饰出现
|
||||
.el-input__count {
|
||||
background: transparent;
|
||||
color: #ffffff7a;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #1a1c20;
|
||||
border: none;
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 12px 16px;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.reference-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
height: 100%;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
background-color: rgba(0, 0, 0, 0.32);
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
backdrop-filter: blur(30px);
|
||||
padding: 14px 16px;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
|
||||
&::placeholder {
|
||||
color: rgba(255, 255, 255, 0.36);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btmcard {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #1a1c20;
|
||||
border: none;
|
||||
margin-top: 16px;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.select {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.select-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.select-title {
|
||||
width: 50%;
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.select-content {
|
||||
width: 50%;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
width: 50%;
|
||||
display: flex;
|
||||
background-color: rgba(255, 255, 255, 0.04);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.and {
|
||||
color: #ffffff66;
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.radio-item {
|
||||
flex: 1;
|
||||
height: 36px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.radio-item.selected {
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.radio-item.premium {
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
color: transparent;
|
||||
background-image: linear-gradient(to bottom, #7280FF, #DADFFF);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.custom-select-popup {
|
||||
background-color: #2A2D34 !important;
|
||||
border-radius: 12px !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4) !important;
|
||||
border: none !important;
|
||||
|
||||
|
||||
.el-select-dropdown__item {
|
||||
color: #ffffff !important;
|
||||
background-color: transparent !important;
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
.el-select-dropdown__item.selected,
|
||||
.el-select-dropdown__item:hover {
|
||||
background-color: #3a3d46 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-select {
|
||||
.el-input__inner {
|
||||
background-color: #35383F !important;
|
||||
border: none !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.el-input__suffix-inner {
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
468
src/components/video/ReferenceVideo.vue
Normal file
468
src/components/video/ReferenceVideo.vue
Normal file
@ -0,0 +1,468 @@
|
||||
<template>
|
||||
<div class="reference-to-video">
|
||||
<!-- 顶部卡片 -->
|
||||
<el-card class="card">
|
||||
<div class="reference-container">
|
||||
<!-- 上传模块 -->
|
||||
<div class="upload-section">
|
||||
<!-- 上传视频 -->
|
||||
<div class="upload-item">
|
||||
<div class="upload-icon">
|
||||
<svg width="1em" height="1em" fill="none" viewBox="0 0 25 25" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.28 5.96875L12.2617 19.9688" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2"></path>
|
||||
<path d="M5.25 12.9688H19.25" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="upload-text">
|
||||
<span class="upload-title">上传视频 / 主体</span>
|
||||
<span class="upload-desc">最多可上传1个5-8s或2个5s内视频 / 主体</span>
|
||||
</div>
|
||||
<img src="https://image01.vidu.zone/vidu/media-asset/video-a09ab611.webp" alt="视频示例"
|
||||
class="upload-preview" />
|
||||
</div>
|
||||
|
||||
<!-- 分割线 -->
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- 上传图片 -->
|
||||
<div class="upload-item">
|
||||
<div class="upload-icon">
|
||||
<svg width="1em" height="1em" fill="none" viewBox="0 0 25 25" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.28 5.96875L12.2617 19.9688" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2"></path>
|
||||
<path d="M5.25 12.9688H19.25" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="upload-text">
|
||||
<span class="upload-title">上传图片 / 主体</span>
|
||||
<span class="upload-desc">最多可上传1-4张图片</span>
|
||||
</div>
|
||||
<img src="https://image01.vidu.zone/vidu/media-asset/img-052445cb.webp" alt="图片示例" class="upload-preview" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文本输入框 -->
|
||||
<div class="input">
|
||||
<el-input
|
||||
v-model="promptText"
|
||||
type="textarea"
|
||||
:rows="10"
|
||||
maxlength="400"
|
||||
@input="handlePromptInput"
|
||||
placeholder="支持上传图片、视频或主体作为参考。可参考视频中的动作、表情、运镜、特效、风格与构图等,并支持对元素进行增删和修改"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 底部配置卡片 -->
|
||||
<el-card class="btmcard">
|
||||
<div class="select">
|
||||
<!-- 模型 -->
|
||||
<div class="select-item">
|
||||
<div class="select-title">模型</div>
|
||||
<div class="select-content">
|
||||
<el-select v-model="selectModel" style="width: 100%" @change="handleChange"
|
||||
popper-class="custom-select-popup">
|
||||
<el-option label="Jack" value="jack" />
|
||||
<el-option label="Lucy" value="lucy" />
|
||||
<el-option label="yiminghe" value="Yiminghe" />
|
||||
<el-option label="Disabled" value="disabled" disabled />
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 时长 -->
|
||||
<div class="select-item">
|
||||
<div class="select-title">时长</div>
|
||||
<div class="radio-group">
|
||||
<button v-for="duration in durations" :key="duration" :class="[
|
||||
'radio-item',
|
||||
selectedDuration === duration ? 'selected' : '',
|
||||
duration > 5 ? 'premium' : ''
|
||||
]" @click="selectedDuration = duration" type="button">
|
||||
{{ duration }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 配音 -->
|
||||
<div class="select-item">
|
||||
<div class="select-title">配音</div>
|
||||
<div class="select-content">
|
||||
<el-select v-model="selectVoice" style="width: 100%" @change="handleChange"
|
||||
popper-class="custom-select-popup">
|
||||
<el-option label="无配音" value="无配音" />
|
||||
<el-option label="仅音效" value="仅音效" />
|
||||
<el-option label="音效+人声" value="音效+人声" />
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 清晰度 & 编码 -->
|
||||
<div class="select-item">
|
||||
<div class="select-title">
|
||||
清晰度<span class="and">&</span>编码格式
|
||||
</div>
|
||||
<div class="select-content">
|
||||
<el-select v-model="selectQuality" style="width: 50%" @change="handleChange"
|
||||
popper-class="custom-select-popup">
|
||||
<el-option label="1080P" value="1080P" />
|
||||
</el-select>
|
||||
<el-select v-model="selectCodec" style="width: 50%" @change="handleChange"
|
||||
popper-class="custom-select-popup">
|
||||
<el-option label="H265" value="H265" />
|
||||
<el-option label="H264" value="H264" />
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 宽高比 -->
|
||||
<div class="select-item">
|
||||
<div class="select-title">宽高比</div>
|
||||
<div class="select-content">
|
||||
<el-select v-model="selectRatio" style="width: 100%" @change="handleChange"
|
||||
popper-class="custom-select-popup">
|
||||
<el-option label="16:9" value="16:9" />
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 数量 -->
|
||||
<div class="select-item">
|
||||
<div class="select-title">数量</div>
|
||||
<div class="radio-group">
|
||||
<button v-for="num in [1, 2, 3, 4]" :key="num" class="radio-item"
|
||||
:class="{ selected: selectedCount === num }" @click="selectedCount = num" type="button">
|
||||
{{ num }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, defineEmits } from 'vue'
|
||||
|
||||
// 定义 emit 事件
|
||||
const emit = defineEmits(['price-update', 'prompt-change'])
|
||||
|
||||
// 下拉选中项
|
||||
const selectModel = ref('lucy')
|
||||
const selectVoice = ref('无配音')
|
||||
const selectQuality = ref('1080P')
|
||||
const selectCodec = ref('H265')
|
||||
const selectRatio = ref('16:9')
|
||||
|
||||
// 单选按钮
|
||||
const selectedDuration = ref(5)
|
||||
const selectedCount = ref(1)
|
||||
const durations = [2, 3, 4, 5, 6, 7, 8]
|
||||
|
||||
// 提示词文本
|
||||
const promptText = ref('')
|
||||
|
||||
// 价格计算
|
||||
const price = computed(() => {
|
||||
// 基础价格:5秒及以下每个 1 单位,超过5秒每个 2 单位
|
||||
let basePrice = selectedDuration.value > 5 ? 2 : 1
|
||||
|
||||
// 数量乘数
|
||||
const quantityMultiplier = selectedCount.value
|
||||
|
||||
// 总价格
|
||||
return basePrice * quantityMultiplier
|
||||
})
|
||||
|
||||
// 监听价格变化,触发 price-update 事件
|
||||
watch(price, (newPrice) => {
|
||||
emit('price-update', newPrice)
|
||||
})
|
||||
|
||||
// 监听提示词变化,触发 prompt-change 事件
|
||||
watch(promptText, (newText) => {
|
||||
emit('prompt-change', !newText || newText.trim().length === 0)
|
||||
})
|
||||
|
||||
// 统一 change 事件
|
||||
const handleChange = (value: string) => {
|
||||
console.log('selected', value)
|
||||
}
|
||||
|
||||
// 处理文本输入变化
|
||||
const handlePromptInput = (value: string) => {
|
||||
promptText.value = value
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
:deep(.el-select__wrapper) {
|
||||
|
||||
width: 100% !important;
|
||||
border: none !important;
|
||||
background-color: rgba(0, 0, 0, 0.32) !important;
|
||||
box-shadow: none !important;
|
||||
border-radius: 0 !important;
|
||||
font-size: 14px;
|
||||
color: #fff !important;
|
||||
resize: vertical;
|
||||
outline: none !important;
|
||||
border-radius: 10px !important;
|
||||
color: #fff;
|
||||
}
|
||||
:deep(.el-select__selected-item){
|
||||
color: #fff!important;
|
||||
}
|
||||
:deep(.el-textarea) {
|
||||
|
||||
.el-textarea__inner {
|
||||
|
||||
border: none !important;
|
||||
background-color: rgba(0, 0, 0, 0.32) !important;
|
||||
box-shadow: none !important;
|
||||
border-radius: 0 !important;
|
||||
padding: 12px 0 !important;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: #fff !important;
|
||||
resize: vertical;
|
||||
outline: none !important;
|
||||
border-radius: 10px !important;
|
||||
color: #fff;
|
||||
backdrop-filter: blur(30px);
|
||||
padding: 14px 16px !important;
|
||||
|
||||
&:focus {
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: #ffffff7a !important;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
// 确保没有任何附加的边框或装饰出现
|
||||
.el-input__count {
|
||||
background: transparent;
|
||||
color: #ffffff7a;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #1a1c20;
|
||||
border: none;
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 12px 16px;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.reference-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
height: 100%;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.upload-section {
|
||||
border: 1px dashed #35383F;
|
||||
border-radius: 12px;
|
||||
background-color: rgba(0, 0, 0, 0.36);
|
||||
|
||||
.upload-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 16px 0 16px 24px;
|
||||
height: 88px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-radius: 12px 12px 0 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-radius: 0 0 12px 12px;
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
font-size: 20px;
|
||||
color: #fff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 10;
|
||||
|
||||
.upload-title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
line-height: 1.25;
|
||||
color: #fff;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.upload-desc {
|
||||
font-size: 12px;
|
||||
line-height: 1.25;
|
||||
color: rgba(255, 255, 255, 0.48);
|
||||
}
|
||||
}
|
||||
|
||||
.upload-preview {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 148px;
|
||||
aspect-ratio: 185/75;
|
||||
object-fit: cover;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
width: 185px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
margin: 0 24px;
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
background-color: rgba(0, 0, 0, 0.32);
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
backdrop-filter: blur(30px);
|
||||
padding: 14px 16px;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
|
||||
&::placeholder {
|
||||
color: rgba(255, 255, 255, 0.36);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btmcard {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #1a1c20;
|
||||
border: none;
|
||||
margin-top: 16px;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.select {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.select-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.select-title {
|
||||
width: 50%;
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.select-content {
|
||||
width: 50%;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
width: 50%;
|
||||
display: flex;
|
||||
background-color: rgba(255, 255, 255, 0.04);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.and {
|
||||
color: #ffffff66;
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.radio-item {
|
||||
flex: 1;
|
||||
height: 36px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.radio-item.selected {
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.radio-item.premium {
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
color: transparent;
|
||||
background-image: linear-gradient(to bottom, #7280FF, #DADFFF);
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
471
src/components/video/TextVideo.vue
Normal file
471
src/components/video/TextVideo.vue
Normal file
@ -0,0 +1,471 @@
|
||||
<template>
|
||||
<div class="reference-to-video">
|
||||
<!-- 顶部卡片:提示词输入区 -->
|
||||
<el-card class="card">
|
||||
<div class="reference-container">
|
||||
<div class="input">
|
||||
<el-input
|
||||
type="textarea"
|
||||
:rows="8"
|
||||
maxlength="400"
|
||||
:placeholder="promptPlaceholder"
|
||||
v-model="prompt"
|
||||
show-word-limit
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 底部卡片:模型及配置项 -->
|
||||
<el-card class="btmcard">
|
||||
<div class="select">
|
||||
<!-- 模型选择 -->
|
||||
<div class="select-item">
|
||||
<div class="select-title">模型</div>
|
||||
<div class="select-content">
|
||||
<el-select
|
||||
v-model="currentModelId"
|
||||
style="width: 100%"
|
||||
popper-class="custom-select-popup"
|
||||
@change="handleModelChange"
|
||||
:disabled="generating"
|
||||
>
|
||||
<el-option
|
||||
v-for="model in models"
|
||||
:key="model.id"
|
||||
:label="model.name"
|
||||
:value="model.id"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 动态渲染除 prompt 以外的其他配置字段 -->
|
||||
<template v-for="field in visibleFields" :key="field.name">
|
||||
<div class="select-item">
|
||||
<div class="select-title">
|
||||
{{ field.label }}
|
||||
<span v-if="field.required" class="required-star">*</span>
|
||||
</div>
|
||||
<div class="select-content">
|
||||
<!-- 下拉选择 -->
|
||||
<el-select
|
||||
v-if="field.uitype === 'code'"
|
||||
v-model="configData[field.name]"
|
||||
style="width: 100%"
|
||||
popper-class="custom-select-popup"
|
||||
:placeholder="`请选择${field.label}`"
|
||||
:disabled="generating"
|
||||
>
|
||||
<el-option
|
||||
v-for="option in field.data"
|
||||
:key="option.value"
|
||||
:label="option.text || option.value"
|
||||
:value="option.value"
|
||||
/>
|
||||
</el-select>
|
||||
|
||||
<!-- 文本输入 -->
|
||||
<el-input
|
||||
v-else-if="field.uitype === 'text'"
|
||||
v-model="configData[field.name]"
|
||||
:placeholder="`请输入${field.label}`"
|
||||
:type="field.name === 'negative_prompt' ? 'textarea' : 'text'"
|
||||
:rows="field.name === 'negative_prompt' ? 3 : 1"
|
||||
clearable
|
||||
:disabled="generating"
|
||||
/>
|
||||
|
||||
<!-- 整数输入 -->
|
||||
<el-input-number
|
||||
v-else-if="field.uitype === 'int'"
|
||||
v-model="configData[field.name]"
|
||||
:min="1"
|
||||
:max="field.max || 15"
|
||||
:step="1"
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
:disabled="generating"
|
||||
/>
|
||||
|
||||
<!-- 音频上传 -->
|
||||
<div v-else-if="field.uitype === 'audio'" class="audio-wrapper">
|
||||
<el-input
|
||||
v-model="configData[field.name]"
|
||||
placeholder="请输入音频文件URL或点击上传"
|
||||
clearable
|
||||
:disabled="generating"
|
||||
/>
|
||||
<el-button size="small" @click="simulateUpload" :disabled="generating" class="upload-btn">
|
||||
上传
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 其他未知类型 -->
|
||||
<el-input
|
||||
v-else
|
||||
v-model="configData[field.name]"
|
||||
:placeholder="`请输入${field.label}`"
|
||||
clearable
|
||||
:disabled="generating"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { textvideo, AllModel, queryModelPrice } from '../../apis/Model';
|
||||
import type {
|
||||
Model,
|
||||
AllModelResponse,
|
||||
PriceQueryParams,
|
||||
PriceQueryResponse,
|
||||
TextVideoParams,
|
||||
TextVideoResponse,
|
||||
} from '../../types/model';
|
||||
|
||||
// ==================== 事件定义 ====================
|
||||
const emit = defineEmits<{
|
||||
(e: 'price-update', price: number): void;
|
||||
(e: 'prompt-change', isEmpty: boolean): void;
|
||||
(e: 'task-created', taskid: string): void;
|
||||
}>();
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
/** 配置字段的值类型(不包括 prompt) */
|
||||
type ConfigValue = string | number | boolean;
|
||||
|
||||
// ==================== 数据定义 ====================
|
||||
const models = ref<Model[]>([]);
|
||||
const currentModelId = ref('');
|
||||
const currentModel = ref<Model | null>(null);
|
||||
|
||||
/** 单独的提示词字段,确保是 string 类型 */
|
||||
const prompt = ref('');
|
||||
|
||||
/** 其他配置字段的集合 */
|
||||
const configData = ref<Record<string, ConfigValue>>({});
|
||||
|
||||
const generating = ref(false);
|
||||
const price = ref(0);
|
||||
|
||||
// ==================== 计算属性 ====================
|
||||
const visibleFields = computed(() => {
|
||||
if (!currentModel.value) return [];
|
||||
return currentModel.value.input_fields.filter(field => field.name !== 'prompt');
|
||||
});
|
||||
|
||||
const promptPlaceholder = computed(() => {
|
||||
if (!currentModel.value) return '请输入提示词';
|
||||
const promptField = currentModel.value.input_fields.find(f => f.name === 'prompt');
|
||||
if (promptField?.required) return '请输入提示词(必填)';
|
||||
return '请输入提示词';
|
||||
});
|
||||
|
||||
// ==================== 表单初始化 ====================
|
||||
const initFormData = () => {
|
||||
if (!currentModel.value) return;
|
||||
const newConfigData: Record<string, ConfigValue> = {};
|
||||
|
||||
currentModel.value.input_fields.forEach(field => {
|
||||
if (field.name === 'prompt') {
|
||||
// prompt 单独处理,不清空已有内容(保持用户输入)
|
||||
return;
|
||||
}
|
||||
|
||||
let defaultValue: ConfigValue = field.defaultvalue ?? '';
|
||||
|
||||
if (defaultValue === undefined || defaultValue === null) {
|
||||
if (field.uitype === 'code' && field.data && field.data.length > 0) {
|
||||
defaultValue = field.data[0].value;
|
||||
} else if (field.uitype === 'int') {
|
||||
defaultValue = 1;
|
||||
} else {
|
||||
defaultValue = '';
|
||||
}
|
||||
}
|
||||
newConfigData[field.name] = defaultValue;
|
||||
});
|
||||
|
||||
configData.value = newConfigData;
|
||||
};
|
||||
|
||||
// ==================== 价格查询 ====================
|
||||
const buildPriceParams = (): PriceQueryParams | null => {
|
||||
if (!currentModel.value) return null;
|
||||
|
||||
const configDataForPrice: PriceQueryParams['config_data'] = { action: '' };
|
||||
|
||||
currentModel.value.input_fields.forEach(field => {
|
||||
if (field.name === 'prompt') return;
|
||||
const value = configData.value[field.name];
|
||||
if (value !== undefined) {
|
||||
if (field.uitype === 'audio') {
|
||||
configDataForPrice.audio = !!value;
|
||||
} else {
|
||||
configDataForPrice[field.name] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
configDataForPrice.action = currentModel.value.apiname;
|
||||
|
||||
if (!configDataForPrice.num && !configDataForPrice.count) {
|
||||
configDataForPrice.num = 1;
|
||||
}
|
||||
|
||||
return {
|
||||
product_type: 'llm',
|
||||
product_id: currentModel.value.id,
|
||||
config_data: configDataForPrice,
|
||||
};
|
||||
};
|
||||
|
||||
let priceTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const fetchPrice = () => {
|
||||
if (priceTimer) clearTimeout(priceTimer);
|
||||
|
||||
priceTimer = setTimeout(async () => {
|
||||
const trimmedPrompt = prompt.value?.trim();
|
||||
if (!trimmedPrompt) {
|
||||
price.value = 0;
|
||||
emit('price-update', 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentModel.value) return;
|
||||
|
||||
const priceParams = buildPriceParams();
|
||||
if (!priceParams) return;
|
||||
|
||||
try {
|
||||
const res = (await queryModelPrice(priceParams)) as PriceQueryResponse;
|
||||
if (res.status === 'ok' && res.data) {
|
||||
price.value = res.data.amount;
|
||||
emit('price-update', price.value);
|
||||
} else {
|
||||
price.value = 0;
|
||||
emit('price-update', 0);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('价格查询失败:', error);
|
||||
price.value = 0;
|
||||
emit('price-update', 0);
|
||||
}
|
||||
}, 500);
|
||||
};
|
||||
|
||||
// ==================== 模型加载 ====================
|
||||
const loadModels = async () => {
|
||||
try {
|
||||
const res = (await AllModel({})) as AllModelResponse;
|
||||
if (res.status === 'ok' && Array.isArray(res.data)) {
|
||||
models.value = res.data;
|
||||
if (models.value.length > 0) {
|
||||
currentModelId.value = models.value[0].id;
|
||||
currentModel.value = models.value[0];
|
||||
initFormData();
|
||||
fetchPrice();
|
||||
}
|
||||
} else {
|
||||
ElMessage.error('获取模型列表失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载模型失败:', error);
|
||||
ElMessage.error('加载模型列表失败,请刷新重试');
|
||||
}
|
||||
};
|
||||
|
||||
const handleModelChange = (modelId: string) => {
|
||||
const selected = models.value.find(m => m.id === modelId);
|
||||
if (selected) {
|
||||
currentModel.value = selected;
|
||||
initFormData();
|
||||
fetchPrice();
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 监听器 ====================
|
||||
watch(
|
||||
() => [configData.value, currentModelId.value],
|
||||
() => {
|
||||
if (currentModel.value) fetchPrice();
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => prompt.value,
|
||||
(newVal) => {
|
||||
const isEmpty = !newVal?.trim();
|
||||
emit('prompt-change', isEmpty);
|
||||
if (isEmpty) {
|
||||
price.value = 0;
|
||||
emit('price-update', 0);
|
||||
} else {
|
||||
fetchPrice();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// ==================== 辅助方法 ====================
|
||||
const simulateUpload = () => {
|
||||
ElMessage.info('此处可对接实际音频上传服务');
|
||||
configData.value.audio_file = 'https://example.com/demo_audio.mp3';
|
||||
};
|
||||
|
||||
// ==================== 生成视频 ====================
|
||||
const buildSubmitParams = (): TextVideoParams => {
|
||||
const baseParams: TextVideoParams = {
|
||||
llmid: currentModel.value!.id,
|
||||
prompt: prompt.value, // 确保是 string
|
||||
};
|
||||
|
||||
if (configData.value.duration !== undefined) {
|
||||
baseParams.duration = Number(configData.value.duration);
|
||||
}
|
||||
if (configData.value.ratio !== undefined) {
|
||||
baseParams.ratio = String(configData.value.ratio);
|
||||
}
|
||||
if (configData.value.resolution !== undefined) {
|
||||
baseParams.resolution = String(configData.value.resolution);
|
||||
}
|
||||
if (configData.value.size !== undefined && !baseParams.resolution) {
|
||||
baseParams.resolution = String(configData.value.size);
|
||||
}
|
||||
baseParams.audio = !!configData.value.audio_file;
|
||||
|
||||
return baseParams;
|
||||
};
|
||||
|
||||
const generate = async (): Promise<boolean> => {
|
||||
if (!currentModel.value) {
|
||||
ElMessage.error('请选择模型');
|
||||
return false;
|
||||
}
|
||||
|
||||
const missingFields: string[] = [];
|
||||
currentModel.value.input_fields.forEach(field => {
|
||||
if (field.required) {
|
||||
if (field.name === 'prompt') {
|
||||
if (!prompt.value?.trim()) missingFields.push(field.label);
|
||||
} else {
|
||||
const val = configData.value[field.name];
|
||||
if (!val) missingFields.push(field.label);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
ElMessage.error(`请填写必填项:${missingFields.join('、')}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
generating.value = true;
|
||||
|
||||
try {
|
||||
const params = buildSubmitParams();
|
||||
const res = (await textvideo(params)) as TextVideoResponse;
|
||||
|
||||
console.log('=== 文生视频接口响应 ===');
|
||||
console.log('响应状态:', res.status);
|
||||
console.log('响应数据:', res.data);
|
||||
console.log('完整响应:', res);
|
||||
|
||||
// 尝试从多个位置获取 taskid
|
||||
let taskid = res.data?.taskid || res.taskid;
|
||||
console.log('提取的 taskid:', taskid);
|
||||
|
||||
if (res.status === 'created' || res.status === 'processing' || res.status === 'queueing') {
|
||||
const statusMsg = res.status === 'created' ? '视频生成任务已提交' : (res.status === 'processing' ? '进行中' : '排队中');
|
||||
ElMessage.success(`${statusMsg},请稍后在任务列表中查看`);
|
||||
|
||||
if (taskid) {
|
||||
console.log('触发 task-created 事件, taskid:', taskid);
|
||||
emit('task-created', taskid);
|
||||
console.log('task-created 事件已发送');
|
||||
} else {
|
||||
console.warn('响应中没有 taskid,尝试使用其他方式获取');
|
||||
// 如果响应中没有 taskid,可能需要从其他字段获取
|
||||
// 这里先记录日志,不做其他处理
|
||||
}
|
||||
resetForm();
|
||||
return true;
|
||||
} else {
|
||||
ElMessage.error(res.data?.message || '生成失败,请重试');
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('生成失败:', error);
|
||||
ElMessage.error(error instanceof Error ? error.message : '生成失败,请重试');
|
||||
return false;
|
||||
} finally {
|
||||
generating.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
prompt.value = '';
|
||||
initFormData();
|
||||
price.value = 0;
|
||||
emit('price-update', 0);
|
||||
emit('prompt-change', true);
|
||||
};
|
||||
|
||||
// ==================== 暴露给父组件 ====================
|
||||
defineExpose({
|
||||
generate,
|
||||
resetForm,
|
||||
});
|
||||
|
||||
// ==================== 生命周期 ====================
|
||||
onMounted(() => {
|
||||
loadModels();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
@import url('../../assets/less/Video/text.less');
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.custom-select-popup {
|
||||
background-color: #2A2D34 !important;
|
||||
border-radius: 12px !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4) !important;
|
||||
border: none !important;
|
||||
}
|
||||
.custom-select-popup .el-select-dropdown__item {
|
||||
color: #ffffff !important;
|
||||
background-color: transparent !important;
|
||||
font-size: 14px !important;
|
||||
}
|
||||
.custom-select-popup .el-select-dropdown__item.selected,
|
||||
.custom-select-popup .el-select-dropdown__item:hover {
|
||||
background-color: #3a3d46 !important;
|
||||
}
|
||||
.el-select .el-input__wrapper {
|
||||
background-color: rgba(0, 0, 0, 0.32) !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
border-radius: 10px !important;
|
||||
}
|
||||
.el-select .el-input__inner {
|
||||
color: #fff !important;
|
||||
}
|
||||
.el-select .el-input__suffix-inner {
|
||||
color: #fff !important;
|
||||
}
|
||||
.el-textarea .el-input__count {
|
||||
background: transparent;
|
||||
color: #ffffff7a;
|
||||
}
|
||||
</style>
|
||||
269
src/components/video/VideoLeft.vue
Normal file
269
src/components/video/VideoLeft.vue
Normal file
@ -0,0 +1,269 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="left">
|
||||
<div class="tab">
|
||||
<VideoTab :activeTab="activeTab" @tabChange="handleTabChange" />
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<Referencevideo
|
||||
ref="referenceVideoRef"
|
||||
v-if="activeTab === 'reference'"
|
||||
@price-update="handleReferencePriceUpdate"
|
||||
@prompt-change="handlePromptChange"
|
||||
/>
|
||||
<ImgVideo
|
||||
ref="imgVideoRef"
|
||||
v-else-if="activeTab === 'image'"
|
||||
@price-update="handleImagePriceUpdate"
|
||||
@prompt-change="handleImagePromptChange"
|
||||
/>
|
||||
<TextVideo
|
||||
ref="textVideoRef"
|
||||
v-else-if="activeTab === 'text'"
|
||||
@price-update="handleTextPriceUpdate"
|
||||
@prompt-change="handleTextPromptChange"
|
||||
@task-created="handleTaskCreated"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="create-button-container">
|
||||
<div
|
||||
class="create-button"
|
||||
:class="{ disabled: isCreateButtonDisabled }"
|
||||
@click="handleCreate"
|
||||
>
|
||||
创作
|
||||
<span v-if="currentPrice > 0" class="price-info">
|
||||
$ {{ currentPrice }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import VideoTab from './VideoTab.vue';
|
||||
import Referencevideo from './ReferenceVideo.vue';
|
||||
import TextVideo from './TextVideo.vue';
|
||||
import ImgVideo from './ImgVideo.vue';
|
||||
|
||||
// ==================== 子组件暴露的方法类型定义 ====================
|
||||
/** 文生视频组件暴露的方法 */
|
||||
interface TextVideoExposed {
|
||||
generate: () => Promise<boolean>;
|
||||
resetForm: () => void;
|
||||
}
|
||||
|
||||
/** 图生视频组件暴露的方法(示例,根据实际情况补充) */
|
||||
interface ImgVideoExposed {
|
||||
resetForm: () => void;
|
||||
}
|
||||
|
||||
/** 参考生视频组件暴露的方法(示例) */
|
||||
interface ReferenceVideoExposed {
|
||||
resetForm: () => void;
|
||||
}
|
||||
|
||||
// ==================== 子组件引用(使用精确类型) ====================
|
||||
const referenceVideoRef = ref<ReferenceVideoExposed | null>(null);
|
||||
const imgVideoRef = ref<ImgVideoExposed | null>(null);
|
||||
const textVideoRef = ref<TextVideoExposed | null>(null);
|
||||
|
||||
// ==================== 其他状态和逻辑(保持不变) ====================
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const activeTab = ref<'reference' | 'image' | 'text'>('reference');
|
||||
const referencePrice = ref(0);
|
||||
const textPrice = ref(0);
|
||||
const imagePrice = ref(0);
|
||||
const isPromptEmpty = ref(true);
|
||||
const isTextPromptEmpty = ref(true);
|
||||
const isImagePromptEmpty = ref(true);
|
||||
|
||||
// 价格更新处理
|
||||
const handleReferencePriceUpdate = (price: number) => { referencePrice.value = price; };
|
||||
const handleTextPriceUpdate = (price: number) => { textPrice.value = price; };
|
||||
const handleImagePriceUpdate = (price: number) => { imagePrice.value = price; };
|
||||
|
||||
// 提示词变化处理
|
||||
const handlePromptChange = (isEmpty: boolean) => { isPromptEmpty.value = isEmpty; };
|
||||
const handleTextPromptChange = (isEmpty: boolean) => { isTextPromptEmpty.value = isEmpty; };
|
||||
const handleImagePromptChange = (isEmpty: boolean) => { isImagePromptEmpty.value = isEmpty; };
|
||||
|
||||
// 计算属性
|
||||
const currentPrice = computed(() => {
|
||||
switch (activeTab.value) {
|
||||
case 'reference': return referencePrice.value;
|
||||
case 'text': return textPrice.value;
|
||||
case 'image': return imagePrice.value;
|
||||
default: return 0;
|
||||
}
|
||||
});
|
||||
|
||||
const isCreateButtonDisabled = computed(() => {
|
||||
switch (activeTab.value) {
|
||||
case 'reference': return isPromptEmpty.value;
|
||||
case 'text': return isTextPromptEmpty.value;
|
||||
case 'image': return isImagePromptEmpty.value;
|
||||
default: return true;
|
||||
}
|
||||
});
|
||||
|
||||
// 切换标签页(类型已明确)
|
||||
const handleTabChange = (tab: 'reference' | 'image' | 'text') => {
|
||||
activeTab.value = tab;
|
||||
router.push(`/videoGenerate/${tab}`);
|
||||
};
|
||||
|
||||
// 创作按钮点击
|
||||
const handleCreate = async () => {
|
||||
if (isCreateButtonDisabled.value) return;
|
||||
let result = false;
|
||||
switch (activeTab.value) {
|
||||
case 'text':
|
||||
if (textVideoRef.value?.generate) {
|
||||
result = await textVideoRef.value.generate();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
ElMessage.info('该功能暂未开放');
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const emit = defineEmits<{ (e: 'task-created', taskid: string): void }>();
|
||||
|
||||
const handleTaskCreated = (taskid: string) => {
|
||||
console.log('=== VideoLeft: 接收到 task-created 事件 ===');
|
||||
console.log('taskid:', taskid);
|
||||
console.log('向上传递给父组件...');
|
||||
|
||||
emit('task-created', taskid);
|
||||
|
||||
console.log('=== VideoLeft: 重置表单 ===');
|
||||
switch (activeTab.value) {
|
||||
case 'text':
|
||||
console.log('重置文生视频表单');
|
||||
textVideoRef.value?.resetForm?.();
|
||||
break;
|
||||
case 'image':
|
||||
console.log('重置图生视频表单');
|
||||
imgVideoRef.value?.resetForm?.();
|
||||
break;
|
||||
case 'reference':
|
||||
console.log('重置参考生视频表单');
|
||||
referenceVideoRef.value?.resetForm?.();
|
||||
break;
|
||||
}
|
||||
|
||||
console.log('=== VideoLeft: handleTaskCreated 完成 ===');
|
||||
};
|
||||
|
||||
// 路由同步
|
||||
const updateActiveTabFromRoute = () => {
|
||||
const path = route.path;
|
||||
if (path.includes('/videoGenerate/reference')) activeTab.value = 'reference';
|
||||
else if (path.includes('/videoGenerate/image')) activeTab.value = 'image';
|
||||
else if (path.includes('/videoGenerate/text')) activeTab.value = 'text';
|
||||
else activeTab.value = 'reference';
|
||||
};
|
||||
|
||||
watch(() => route.path, () => updateActiveTabFromRoute());
|
||||
onMounted(() => updateActiveTabFromRoute());
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped lang="less">
|
||||
.left {
|
||||
width: 100%;
|
||||
height: calc(100vh - 80px);
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.create-button-container {
|
||||
z-index: 999;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
background: linear-gradient(180deg, rgba(2, 11, 19, 0) 0%, #0f0f19 100%);
|
||||
// position: relative;
|
||||
}
|
||||
.create-button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
border-radius: 12px !important;
|
||||
background-image: linear-gradient(270deg, #94d8ff 11.11%, #e0ddff);
|
||||
border: none !important;
|
||||
color: #000 !important;
|
||||
font-size: 18px !important;
|
||||
font-weight: 600 !important;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.create-button .price-info {
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
}
|
||||
.create-button:hover {
|
||||
opacity: 0.9;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.create-button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
.create-button.disabled {
|
||||
background-image: linear-gradient(270deg, #cccccc 11.11%, #aaaaaa);
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.create-button.disabled:hover {
|
||||
transform: none;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
// 滚动条宽度(垂直滚动条)/ 高度(水平滚动条)
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
// 轨道背景色
|
||||
background: #0F0F19;
|
||||
// 轨道圆角
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
// ========== 滚动条滑块样式 ==========
|
||||
&::-webkit-scrollbar-thumb {
|
||||
// 滑块背景色
|
||||
background: #0F0F19;
|
||||
// 滑块圆角
|
||||
border-radius: 3px;
|
||||
// 鼠标悬浮时的样式
|
||||
&:hover {
|
||||
background: #0F0F19;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
94
src/components/video/VideoTab.vue
Normal file
94
src/components/video/VideoTab.vue
Normal file
@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="tabs-container ">
|
||||
<div
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'reference' }"
|
||||
@click="switchTab('reference')"
|
||||
>
|
||||
参考生视频
|
||||
</div>
|
||||
<div
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'image' }"
|
||||
@click="switchTab('image')"
|
||||
>
|
||||
图生视频
|
||||
</div>
|
||||
<div
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'text' }"
|
||||
@click="switchTab('text')"
|
||||
>
|
||||
文生视频
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang='ts'>
|
||||
// 定义 Tab 类型,与父组件保持一致
|
||||
type TabType = 'reference' | 'image' | 'text';
|
||||
|
||||
// 接收父组件传递的 activeTab,使用联合类型
|
||||
const props = defineProps<{
|
||||
activeTab: TabType;
|
||||
}>();
|
||||
|
||||
// 定义事件,tab 参数同样使用联合类型
|
||||
const emit = defineEmits<{
|
||||
(e: 'tabChange', tab: TabType): void;
|
||||
}>();
|
||||
|
||||
// 切换 tab
|
||||
const switchTab = (tab: TabType) => {
|
||||
emit('tabChange', tab);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang='less'>
|
||||
.tabs-container {
|
||||
width: 100%;
|
||||
height: 44x;
|
||||
display: flex;
|
||||
background-color: #1a1c20;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
width: 50%;
|
||||
flex: 1;
|
||||
padding: 12px 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tab-item:hover {
|
||||
color: #fff;
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
color: #fff;
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.tab-item.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 20%;
|
||||
width: 60%;
|
||||
height: 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
</style>
|
||||
25
src/main.ts
Normal file
25
src/main.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
|
||||
// 路由
|
||||
import router from './router'
|
||||
// element-ui
|
||||
import ElementPlus from 'element-plus'
|
||||
// element-ui css
|
||||
import 'element-plus/dist/index.css'
|
||||
import 'element-plus/theme-chalk/index.css'
|
||||
// pinia持久化
|
||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
||||
// pinia
|
||||
import { createPinia } from 'pinia'
|
||||
// icon
|
||||
import './assets/css/iconfont/iconfont.css'
|
||||
|
||||
|
||||
const pinia = createPinia()
|
||||
pinia.use(piniaPluginPersistedstate)
|
||||
const app = createApp(App)
|
||||
app.use(router)
|
||||
app.use(ElementPlus)
|
||||
app.use(pinia)
|
||||
app.mount('#app')
|
||||
89
src/router/index.ts
Normal file
89
src/router/index.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/home'
|
||||
},
|
||||
{
|
||||
path: '/home',
|
||||
name: 'home',
|
||||
component: () => import('@/views/Home.vue')
|
||||
},
|
||||
// 登录
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
component: () => import('@/views/login/Login.vue')
|
||||
},
|
||||
// 首页
|
||||
{
|
||||
path: '/Homeindex',
|
||||
name: 'homeindex',
|
||||
component: () => import('@/views/Homeindex.vue'),
|
||||
children: [
|
||||
{
|
||||
path: '/index',
|
||||
name: 'index',
|
||||
component: () => import('@/views/index/index.vue')
|
||||
},
|
||||
// ImgGenerate
|
||||
{
|
||||
path: '/ImgGenerate',
|
||||
name: 'ImgGenerate',
|
||||
component: () => import('@/views/ImgGenerate/ImgGenerate.vue'),
|
||||
children: [
|
||||
{
|
||||
path: 'reference',
|
||||
name: 'ImgGenerateReference',
|
||||
component: () => import('@/views/ImgGenerate/ImgGenerate.vue')
|
||||
},
|
||||
{
|
||||
path: 'text',
|
||||
name: 'ImgGenerateText',
|
||||
component: () => import('@/views/ImgGenerate/ImgGenerate.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
// videoGenerate
|
||||
{
|
||||
path: '/videoGenerate',
|
||||
name: 'videoGenerate',
|
||||
component: () => import('@/views/videoGenerate/videoGenerate.vue'),
|
||||
children: [
|
||||
{
|
||||
path: 'reference',
|
||||
name: 'VideoGenerateReference',
|
||||
component: () => import('@/views/videoGenerate/videoGenerate.vue')
|
||||
},
|
||||
{
|
||||
path: 'image',
|
||||
name: 'VideoGenerateImage',
|
||||
component: () => import('@/views/videoGenerate/videoGenerate.vue')
|
||||
},
|
||||
{
|
||||
path: 'text',
|
||||
name: 'VideoGenerateText',
|
||||
component: () => import('@/views/videoGenerate/videoGenerate.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
// 素材广场
|
||||
{
|
||||
path: '/material-square',
|
||||
name: 'material-square',
|
||||
component: () => import('@/views/videoGenerate/videoGenerate.vue')
|
||||
}
|
||||
|
||||
]
|
||||
},
|
||||
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
export default router
|
||||
166
src/store/LoginStore.ts
Normal file
166
src/store/LoginStore.ts
Normal file
@ -0,0 +1,166 @@
|
||||
import { ref } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { defineStore } from 'pinia';
|
||||
import router from '@/router';
|
||||
import type { LoginResult, LoginParams, LoginUser } from '@/types/login';
|
||||
import { LoginApi } from '@/apis/login';
|
||||
import { openAccount } from '@/apis/home';
|
||||
|
||||
export const useLoginStore = defineStore(
|
||||
'loginStore',
|
||||
() => {
|
||||
// 登录状态
|
||||
const isLoggedIn = ref(false);
|
||||
// 当前用户信息
|
||||
const userInfo = ref<{ user: LoginUser } | null>(null);
|
||||
// 多账号列表
|
||||
const accountList = ref<LoginUser[]>([]);
|
||||
// 是否显示账号选择弹窗
|
||||
const showAccountSelect = ref(false);
|
||||
// 登录时间戳(用于精确计算剩余时间,可选)
|
||||
const loginTimestamp = ref<number | null>(null);
|
||||
// 过期前的当前页面路径
|
||||
const redirectPath = ref<string | null>(null);
|
||||
|
||||
// 定时器 ID,用于管理自动登出
|
||||
let autoLogoutTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
// 开户
|
||||
const openAccounts = async () => {
|
||||
const res = await openAccount();
|
||||
console.log('开户', res);
|
||||
};
|
||||
|
||||
// 清除自动登出计时器
|
||||
const clearAutoLogoutTimer = () => {
|
||||
if (autoLogoutTimer) {
|
||||
clearTimeout(autoLogoutTimer);
|
||||
autoLogoutTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 启动自动登出计时器(基于剩余时间)
|
||||
const startAutoLogoutTimer = () => {
|
||||
clearAutoLogoutTimer();
|
||||
if (!loginTimestamp.value) {
|
||||
console.log('没有登录时间戳,不启动计时器');
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const elapsed = now - loginTimestamp.value;
|
||||
const remaining = Math.max(0, 20 * 60 * 1000 - elapsed);
|
||||
|
||||
console.log('启动自动登出计时器,剩余时间(毫秒):', remaining);
|
||||
|
||||
autoLogoutTimer = setTimeout(() => {
|
||||
console.log('计时器触发,执行自动登出');
|
||||
logoutS();
|
||||
}, remaining);
|
||||
};
|
||||
|
||||
// 核心登录方法(统一处理)
|
||||
const doLogin = async (params: LoginParams) => {
|
||||
const res = await LoginApi<LoginResult, LoginParams>(params);
|
||||
|
||||
if (res.status === 'ok') {
|
||||
openAccounts();
|
||||
// 登录成功
|
||||
isLoggedIn.value = true;
|
||||
userInfo.value = res.data as { user: LoginUser };
|
||||
// 记录登录时间戳并启动计时器
|
||||
loginTimestamp.value = Date.now();
|
||||
startAutoLogoutTimer();
|
||||
ElMessage.success('登录成功');
|
||||
// 跳转到之前保存的路径或默认路径
|
||||
const path = redirectPath.value || '/';
|
||||
redirectPath.value = null;
|
||||
router.push(path);
|
||||
return res;
|
||||
}
|
||||
|
||||
if (res.status === 'choose') {
|
||||
// 需要选择账号
|
||||
let users: LoginUser[] = [];
|
||||
const data = res.data;
|
||||
if (Array.isArray(data)) {
|
||||
users = data;
|
||||
} else if (data && 'users' in data) {
|
||||
users = (data as { users: LoginUser[] }).users;
|
||||
}
|
||||
accountList.value = users;
|
||||
showAccountSelect.value = true;
|
||||
return res;
|
||||
}
|
||||
|
||||
// 其他失败情况
|
||||
ElMessage.error(res.message || '登录失败');
|
||||
return res;
|
||||
};
|
||||
|
||||
// 登录(首次)
|
||||
const Login = (params: LoginParams) => doLogin(params);
|
||||
|
||||
// 选择账号后登录(携带 selected_id)
|
||||
const loginWithSelectedAccount = async (
|
||||
selectedId: string,
|
||||
params: Omit<LoginParams, 'selected_id'>
|
||||
) => {
|
||||
showAccountSelect.value = false;
|
||||
return doLogin({ ...params, selected_id: selectedId });
|
||||
};
|
||||
const logoutS = () => {
|
||||
console.log('自动登出触发');
|
||||
// 保存当前路径
|
||||
redirectPath.value = router.currentRoute.value.path;
|
||||
clearAutoLogoutTimer();
|
||||
isLoggedIn.value = false;
|
||||
userInfo.value = null;
|
||||
accountList.value = [];
|
||||
showAccountSelect.value = false;
|
||||
loginTimestamp.value = null;
|
||||
router.push('/login');
|
||||
ElMessage.error('身份验证过期请重新登录');
|
||||
};
|
||||
// 登出
|
||||
const logout = () => {
|
||||
clearAutoLogoutTimer();
|
||||
isLoggedIn.value = false;
|
||||
userInfo.value = null;
|
||||
accountList.value = [];
|
||||
showAccountSelect.value = false;
|
||||
loginTimestamp.value = null;
|
||||
router.push('/login');
|
||||
};
|
||||
|
||||
// 初始化自动登出(页面刷新后调用,恢复计时器)
|
||||
const initAutoLogout = () => {
|
||||
console.log('初始化自动登出,当前登录状态:', isLoggedIn.value);
|
||||
console.log('登录时间戳:', loginTimestamp.value);
|
||||
if (isLoggedIn.value) {
|
||||
// 如果没有时间戳(可能因为持久化缺少),用当前时间作为基准
|
||||
if (!loginTimestamp.value) {
|
||||
console.log('没有登录时间戳,使用当前时间作为基准');
|
||||
loginTimestamp.value = Date.now();
|
||||
}
|
||||
startAutoLogoutTimer();
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
isLoggedIn,
|
||||
userInfo,
|
||||
accountList,
|
||||
showAccountSelect,
|
||||
redirectPath,
|
||||
Login,
|
||||
loginWithSelectedAccount,
|
||||
logout,
|
||||
logoutS,
|
||||
initAutoLogout,
|
||||
};
|
||||
},
|
||||
{
|
||||
persist: true, // 持久化登录状态、userInfo、loginTimestamp 等
|
||||
}
|
||||
);
|
||||
79
src/style.css
Normal file
79
src/style.css
Normal file
@ -0,0 +1,79 @@
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
288
src/types/home.ts
Normal file
288
src/types/home.ts
Normal file
@ -0,0 +1,288 @@
|
||||
// type/home.ts
|
||||
|
||||
// 创作者信息
|
||||
export interface Creator {
|
||||
id: string;
|
||||
nick_name: string;
|
||||
avatar: string;
|
||||
user_medal?: string;
|
||||
is_following: boolean;
|
||||
}
|
||||
|
||||
// 尺寸信息
|
||||
export interface Size {
|
||||
width: number;
|
||||
height: number;
|
||||
ratio: string;
|
||||
}
|
||||
|
||||
// 视频创作信息
|
||||
export interface Creation {
|
||||
uri: string;
|
||||
cover_uri: string;
|
||||
size: Size;
|
||||
duration: number;
|
||||
uris: Record<string, string>;
|
||||
cover_uris: Record<string, string>;
|
||||
origin_task_type: string;
|
||||
}
|
||||
|
||||
// 互动信息
|
||||
export interface ReactInfo {
|
||||
voted: boolean;
|
||||
vote_count: number;
|
||||
play_count: string;
|
||||
collected: boolean;
|
||||
collect_count: number;
|
||||
}
|
||||
|
||||
// 短剧类型
|
||||
export interface ShortFilm {
|
||||
id: string;
|
||||
creation: Creation;
|
||||
title: string;
|
||||
detail: string;
|
||||
creator: Creator;
|
||||
created_at: string;
|
||||
react_info: ReactInfo;
|
||||
type: string;
|
||||
episode_num: number;
|
||||
phase: string;
|
||||
latest_episode_num: number;
|
||||
source: string;
|
||||
award: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
// 教程类型
|
||||
export interface Tutorial {
|
||||
id: string;
|
||||
creation: Creation;
|
||||
title: string;
|
||||
detail: string;
|
||||
creator: Creator;
|
||||
created_at: string;
|
||||
react_info: ReactInfo;
|
||||
type: string;
|
||||
episode_num: number;
|
||||
phase: string;
|
||||
latest_episode_num: number;
|
||||
source: string;
|
||||
award: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
// 媒体资源类型(图生视频、文生视频等)
|
||||
export interface MediaAsset {
|
||||
id: string;
|
||||
tag: string;
|
||||
creation: Creation;
|
||||
brief: string;
|
||||
detail: string;
|
||||
creator: Creator;
|
||||
created_at: string;
|
||||
react_info: ReactInfo;
|
||||
creation_id: string;
|
||||
type: string;
|
||||
recreate_count: number;
|
||||
award: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
// 联合类型:列表中的每一项可能是短剧、教程或媒体资源
|
||||
export interface FeedItem {
|
||||
type: 'short_film' | 'tutorial' | 'media_asset';
|
||||
short_film: ShortFilm | null;
|
||||
tutorial: Tutorial | null;
|
||||
media_asset: MediaAsset | null;
|
||||
image_asset: Record<string, unknown> | null; // 根据实际情况细化
|
||||
mini_video: Record<string, unknown> | null; // 根据实际情况细化
|
||||
}
|
||||
|
||||
// 接口返回的完整数据结构
|
||||
export interface FeedResponse {
|
||||
list: FeedItem[];
|
||||
next_page_token: string;
|
||||
activities: Record<string, unknown>[] | null;
|
||||
}
|
||||
|
||||
// ==================== Share Elements 数据格式 ====================
|
||||
|
||||
// Share Elements 创建者信息
|
||||
export interface ShareElementCreator {
|
||||
id: string;
|
||||
nick_name: string;
|
||||
avatar: string;
|
||||
is_following: boolean;
|
||||
}
|
||||
|
||||
// Share Elements 视频信息
|
||||
export interface ShareElementVideo {
|
||||
duration: number;
|
||||
fps: number;
|
||||
resolution: {
|
||||
width: number;
|
||||
height: number;
|
||||
resolution: string;
|
||||
};
|
||||
ratio: string;
|
||||
}
|
||||
|
||||
// Share Elements 创作信息
|
||||
export interface ShareElementCreation {
|
||||
id: string;
|
||||
task_id: string;
|
||||
type: string;
|
||||
grade: string;
|
||||
uri: string;
|
||||
download_uri: string;
|
||||
cover_uri: string;
|
||||
resolution: {
|
||||
width: number;
|
||||
height: number;
|
||||
resolution: string;
|
||||
};
|
||||
vote: string;
|
||||
is_favor: boolean;
|
||||
src_video_duration: number;
|
||||
creator_id: string;
|
||||
video: ShareElementVideo;
|
||||
is_deleted: boolean;
|
||||
err_code: string;
|
||||
created_at: string;
|
||||
origin_task_type: string;
|
||||
style: string;
|
||||
duration: number;
|
||||
has_audio: boolean;
|
||||
is_asset: boolean;
|
||||
is_work: boolean;
|
||||
asset_selected: boolean;
|
||||
nomark_uri: string;
|
||||
source_creation: Record<string, unknown> | null;
|
||||
origin_creation: Record<string, unknown> | null;
|
||||
err_info: {
|
||||
err_code: string;
|
||||
err_msg: string;
|
||||
};
|
||||
file_name: string;
|
||||
is_posted: boolean;
|
||||
has_copyright: boolean;
|
||||
}
|
||||
|
||||
// Share Elements 互动信息
|
||||
export interface ShareElementReactInfo {
|
||||
voted: boolean;
|
||||
vote_count: number;
|
||||
play_count: string;
|
||||
collected: boolean;
|
||||
collect_count: number;
|
||||
}
|
||||
|
||||
// Share Elements 分享信息
|
||||
export interface ShareElementShare {
|
||||
id: string;
|
||||
element_id: string;
|
||||
status: string;
|
||||
category: string[];
|
||||
series: string;
|
||||
copy_count: number;
|
||||
use_count: number;
|
||||
creator: ShareElementCreator;
|
||||
introduce: string;
|
||||
creations: ShareElementCreation[];
|
||||
allow_copy: boolean;
|
||||
created_at: string;
|
||||
react_info: ShareElementReactInfo;
|
||||
cid_to_task: Record<string, unknown>;
|
||||
operate_tag: string;
|
||||
category_display: string[];
|
||||
creator_rank: number;
|
||||
element_labels: string[];
|
||||
}
|
||||
|
||||
// Share Elements 元素信息
|
||||
export interface ShareElement {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
components: Record<string, unknown>[];
|
||||
version: string;
|
||||
labels: Record<string, unknown>[];
|
||||
creator_id: string;
|
||||
recaption: {
|
||||
style: string;
|
||||
description: string;
|
||||
};
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
share_meta: {
|
||||
id: string;
|
||||
status: string;
|
||||
creator: ShareElementCreator;
|
||||
};
|
||||
voice: {
|
||||
id: string;
|
||||
url?: string;
|
||||
name?: string;
|
||||
};
|
||||
pay_info: {
|
||||
pay_required: boolean;
|
||||
};
|
||||
modality: string;
|
||||
creator: ShareElementCreator;
|
||||
}
|
||||
|
||||
// Share Elements 列表项
|
||||
export interface ShareElementItem {
|
||||
element: ShareElement;
|
||||
share: ShareElementShare;
|
||||
}
|
||||
|
||||
// Share Elements 响应数据
|
||||
export interface ShareElementsResponse {
|
||||
share_elements: ShareElementItem[];
|
||||
next_page_token: string;
|
||||
categories: {
|
||||
code: string;
|
||||
display_name: string;
|
||||
display_tag: string;
|
||||
}[];
|
||||
has_liked: boolean;
|
||||
topics: {
|
||||
code: string;
|
||||
display_name: string;
|
||||
display_tag: string;
|
||||
}[];
|
||||
has_collected: boolean;
|
||||
tags: {
|
||||
code: string;
|
||||
type: string;
|
||||
display_name: string;
|
||||
display_tag: string;
|
||||
extra: {
|
||||
cover_uri: string;
|
||||
flag: string;
|
||||
};
|
||||
}[];
|
||||
}
|
||||
|
||||
// 转换后 Stencil 组件需要的卡片格式
|
||||
export interface StencilItem {
|
||||
id: string;
|
||||
type: 'video';
|
||||
src: string;
|
||||
poster: string;
|
||||
aspectRatio: string;
|
||||
avatar: string;
|
||||
userName: string;
|
||||
likes: number;
|
||||
averageColor?: string;
|
||||
}
|
||||
|
||||
// 余额
|
||||
interface BalanceData {
|
||||
account: string;
|
||||
balance: number;
|
||||
};
|
||||
|
||||
export interface BalanceResult {
|
||||
status: string;
|
||||
data: Array< BalanceData >;
|
||||
};
|
||||
54
src/types/login.ts
Normal file
54
src/types/login.ts
Normal file
@ -0,0 +1,54 @@
|
||||
// 验证码返回数据
|
||||
export interface CodeData {
|
||||
message: string;
|
||||
key: string;
|
||||
}
|
||||
|
||||
export interface CodeResult {
|
||||
status: string;
|
||||
data: CodeData;
|
||||
}
|
||||
|
||||
// 用户信息
|
||||
export interface LoginUser {
|
||||
id: string;
|
||||
username: string;
|
||||
password: string;
|
||||
email: string;
|
||||
orgid: string;
|
||||
nick_name: string | null;
|
||||
address: string | null;
|
||||
mobile: string;
|
||||
user_status: string;
|
||||
// 可选字段,用于显示
|
||||
name?: string;
|
||||
cellphone?: string;
|
||||
}
|
||||
|
||||
// 登录成功时的数据
|
||||
export interface LoginData {
|
||||
user: LoginUser;
|
||||
}
|
||||
|
||||
// 登录返回结果
|
||||
// 注意:多账号选择时,后端可能直接返回 { status: 'choose', data: LoginUser[] }
|
||||
// 或返回 { status: 'choose', data: { users: LoginUser[] } }
|
||||
// 这里我们使用联合类型,并在 store 中根据实际情况处理
|
||||
export interface LoginResult {
|
||||
status: string;
|
||||
data: LoginData | LoginUser[] | { users: LoginUser[] }; // 兼容多种结构
|
||||
message?: string;
|
||||
}
|
||||
|
||||
// 登录参数
|
||||
export interface LoginParams {
|
||||
cellphone: string;
|
||||
key: string;
|
||||
sms_code: string;
|
||||
selected_id?: string; // 选择账号时传入
|
||||
}
|
||||
|
||||
// 验证码参数
|
||||
export interface CaptchaParams {
|
||||
cellphone: string;
|
||||
}
|
||||
230
src/types/model.ts
Normal file
230
src/types/model.ts
Normal file
@ -0,0 +1,230 @@
|
||||
interface Data {
|
||||
value: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
interface Input_field {
|
||||
name: string;
|
||||
required?: boolean;
|
||||
uitype: string;
|
||||
label?: string;
|
||||
defaultvalue?: number;
|
||||
data?: Array< Data >;
|
||||
lable?: string;
|
||||
};
|
||||
|
||||
interface ModelData {
|
||||
id: string;
|
||||
name: string;
|
||||
model: string;
|
||||
description: string;
|
||||
llmcatelogid: string;
|
||||
iconid: string;
|
||||
upappid: string;
|
||||
apiname: string;
|
||||
providerid: string;
|
||||
ownerid: string;
|
||||
enabled_date: string;
|
||||
expired_date: string;
|
||||
query_apiname: string;
|
||||
query_period: number;
|
||||
ppid: string;
|
||||
input_fields: Array< Input_field >;
|
||||
};
|
||||
|
||||
export interface ModelResult{
|
||||
status: string;
|
||||
data: Array< ModelData >;
|
||||
};
|
||||
|
||||
export interface Model {
|
||||
type: string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 模型配置字段
|
||||
*/
|
||||
export interface InputField {
|
||||
name: string; // 字段名
|
||||
label: string; // 显示标签
|
||||
uitype: string; // 控件类型: code, text, int, audio 等
|
||||
required?: boolean; // 是否必填
|
||||
defaultvalue?: string | number | boolean | null; // 默认值
|
||||
data?: Array<{ value: string; text?: string }>; // 下拉选项
|
||||
max?: number; // 最大值(int 类型)
|
||||
textField?: string; // 下拉选项显示字段
|
||||
}
|
||||
|
||||
/**
|
||||
* 模型对象
|
||||
*/
|
||||
export interface Model {
|
||||
id: string;
|
||||
name: string;
|
||||
model: string;
|
||||
description: string;
|
||||
llmcatelogid: string;
|
||||
iconid: string;
|
||||
upappid: string;
|
||||
apiname: string;
|
||||
providerid: string;
|
||||
ownerid: string;
|
||||
enabled_date: string;
|
||||
expired_date: string;
|
||||
query_apiname: string;
|
||||
query_period: number;
|
||||
ppid: string;
|
||||
input_fields: InputField[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模型列表响应
|
||||
*/
|
||||
export interface AllModelResponse {
|
||||
status: string
|
||||
data: Model[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 价格查询请求参数
|
||||
*/
|
||||
export interface PriceQueryParams {
|
||||
product_type: 'llm';
|
||||
product_id: string;
|
||||
config_data: {
|
||||
action: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 价格查询响应
|
||||
*/
|
||||
export interface PriceQueryResponse {
|
||||
status: string
|
||||
data: {
|
||||
amount: number; // 价格
|
||||
original_amount?: number | null;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 文生视频请求参数
|
||||
*/
|
||||
export interface TextVideoParams {
|
||||
llmid: string;
|
||||
prompt: string;
|
||||
duration?: number;
|
||||
ratio?: string;
|
||||
resolution?: string;
|
||||
audio?: boolean;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* 文生视频响应
|
||||
*/
|
||||
export interface TextVideoResponse {
|
||||
taskid: string | undefined;
|
||||
status: string;
|
||||
message?: string;
|
||||
data?: { taskid: string } | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务列表响应 - 后端实际返回类型
|
||||
*/
|
||||
export interface TaskResult {
|
||||
status: string;
|
||||
data: ItemData;
|
||||
}
|
||||
|
||||
export interface ItemData {
|
||||
tasks: Array<Tasks>;
|
||||
}
|
||||
|
||||
export interface Tasks {
|
||||
id: string;
|
||||
llmid: string;
|
||||
use_date: string;
|
||||
use_time: string;
|
||||
userid: string;
|
||||
usages: null;
|
||||
ioinfo: Ioinfo;
|
||||
transno: string;
|
||||
responsed_seconds: number;
|
||||
finish_seconds: number;
|
||||
status: string;
|
||||
taskid: string;
|
||||
amount: number;
|
||||
cost: number;
|
||||
userorgid: string;
|
||||
ownerid: null;
|
||||
accounting_status: string;
|
||||
}
|
||||
|
||||
export interface Ioinfo {
|
||||
input: Input;
|
||||
output: Array<Output>;
|
||||
}
|
||||
|
||||
export interface Input {
|
||||
llmid: string;
|
||||
prompt: string;
|
||||
duration: number;
|
||||
ratio: string;
|
||||
resolution: string;
|
||||
audio: boolean;
|
||||
transno: string;
|
||||
model: string;
|
||||
callbackurl: string;
|
||||
}
|
||||
|
||||
export interface Usage {
|
||||
action: string;
|
||||
credits: number;
|
||||
type: string;
|
||||
model: string;
|
||||
resolution: string;
|
||||
off_peak: boolean;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
export interface Output {
|
||||
taskid?: string;
|
||||
status: string;
|
||||
credits?: number;
|
||||
usage?: Usage;
|
||||
type?: string;
|
||||
image?: string;
|
||||
video?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务列表响应 - 前端使用的简化类型(保持向后兼容)
|
||||
*/
|
||||
export interface TaskListResponse {
|
||||
status: string;
|
||||
data: TaskItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务项 - 前端显示用的简化格式
|
||||
*/
|
||||
export interface TaskItem {
|
||||
taskid: string;
|
||||
status: string;
|
||||
llmid?: string;
|
||||
prompt?: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
video_url?: string;
|
||||
cover_url?: string;
|
||||
duration?: number;
|
||||
resolution?: string;
|
||||
error_msg?: string;
|
||||
model?: string;
|
||||
use_time?: string;
|
||||
action?: string;
|
||||
}
|
||||
125
src/utlis/request.ts
Normal file
125
src/utlis/request.ts
Normal file
@ -0,0 +1,125 @@
|
||||
|
||||
import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
// import { ElMessage } from 'element-plus'
|
||||
import { useLoginStore } from '@/store/LoginStore'
|
||||
// 请求拦截器
|
||||
// 添加请求拦截器
|
||||
axios.interceptors.request.use(
|
||||
function (config) {
|
||||
// console.log('=== 请求拦截器 ===')
|
||||
// console.log('请求 URL:', config.url)
|
||||
// console.log('请求方法:', config.method)
|
||||
// console.log('请求参数 (params):', config.params)
|
||||
// console.log('请求数据 (data):', config.data)
|
||||
// console.log('请求头:', config.headers)
|
||||
|
||||
// config.baseURL = import.meta.env.VITE_API_BASE_URL
|
||||
|
||||
|
||||
const Store = useLoginStore()
|
||||
if (Store.userInfo && Store.userInfo.user && Store.userInfo.user.id) {
|
||||
config.headers.Authorization = Store.userInfo.user.id
|
||||
}
|
||||
return config
|
||||
},
|
||||
function (error) {
|
||||
// 对请求错误做些什么
|
||||
return Promise.reject(error)
|
||||
},
|
||||
)
|
||||
|
||||
// 添加响应拦截器
|
||||
|
||||
axios.interceptors.response.use(
|
||||
function (response) {
|
||||
// 放行的状态列表
|
||||
const allowedStatuses = ['ok', 'PENDING', 'created', 'processing', 'queueing', 'succeeded', 'completed', 'failed'];
|
||||
|
||||
// 检查响应数据中的状态
|
||||
const responseData = response.data;
|
||||
const status = responseData?.status || responseData?.data?.resposne?.status;
|
||||
|
||||
// 如果状态在放行列表中,直接返回
|
||||
if (status && allowedStatuses.includes(status)) {
|
||||
return response;
|
||||
}
|
||||
|
||||
// 如果状态不在放行列表中,也返回(兼容其他状态)
|
||||
return response;
|
||||
}, function (error) {
|
||||
// 只有在确实有错误消息时才显示 ElMessage
|
||||
if (error?.response?.data?.message) {
|
||||
ElMessage({
|
||||
message: error.response.data.message,
|
||||
type: 'error',
|
||||
duration: 2 * 1000
|
||||
})
|
||||
}
|
||||
return Promise.reject(error);
|
||||
});
|
||||
const http = <T, D, P>(url: string, data?: D, params?: P, method: 'get' | 'PUT' |'Delete'|'post' = 'get'): Promise<T> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios({
|
||||
method,
|
||||
url,
|
||||
data,
|
||||
params,
|
||||
})
|
||||
.then((response) => {
|
||||
resolve(response.data as T)
|
||||
})
|
||||
|
||||
.catch(function (error) {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export const get = <T, P>(url: string, params?: P): Promise<T> => {
|
||||
return http<T, undefined, P>(url, undefined, params, 'get')
|
||||
}
|
||||
export const post = <T, D>(url: string, data?: D): Promise<T> => {
|
||||
return http<T, D, undefined>(url, data, undefined, 'post')
|
||||
}
|
||||
|
||||
export const Delete = <T, D>(url: string, data?: D): Promise<T> => {
|
||||
return http<T, D, undefined>(url, data, undefined, 'Delete')
|
||||
}
|
||||
export const PUT = <T, D>(url: string, data?: D): Promise<T> => {
|
||||
return http<T, D, undefined>(url, data, undefined, 'PUT')
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const uploadFile=async(file:File, uploadUrl:string, additionalData: Record<string, string> = {})=> {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file); // 'file'是后端接收文件的字段名
|
||||
|
||||
// 如果需要附加其他数据
|
||||
Object.keys(additionalData).forEach(key => {
|
||||
formData.append(key, additionalData[key]);
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await axios.post(uploadUrl, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
|
||||
console.log("000",response.data);
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('文件上传失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
98
src/utlis/transform.ts
Normal file
98
src/utlis/transform.ts
Normal file
@ -0,0 +1,98 @@
|
||||
// utils/transform.ts
|
||||
import type { FeedItem, StencilItem, ShareElementItem, ShortFilm, Tutorial, MediaAsset } from '../types/home';
|
||||
|
||||
type FeedItemData = ShortFilm | Tutorial | MediaAsset;
|
||||
|
||||
export function transformFeedItem(item: FeedItem): StencilItem | null {
|
||||
// 根据 type 确定实际数据来源
|
||||
let data: FeedItemData | null = null;
|
||||
if (item.type === 'short_film' && item.short_film) {
|
||||
data = item.short_film;
|
||||
} else if (item.type === 'tutorial' && item.tutorial) {
|
||||
data = item.tutorial;
|
||||
} else if (item.type === 'media_asset' && item.media_asset) {
|
||||
data = item.media_asset;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!data) return null;
|
||||
|
||||
const creation = data.creation;
|
||||
if (!creation) return null;
|
||||
|
||||
// 构建宽高比字符串(CSS 支持 "宽/高" 格式)
|
||||
const { width, height } = creation.size;
|
||||
const aspectRatio = `${width}/${height}`;
|
||||
|
||||
return {
|
||||
id: data.id,
|
||||
type: 'video', // 当前所有项都是视频,可根据实际情况调整
|
||||
src: creation.uri,
|
||||
poster: creation.cover_uri,
|
||||
aspectRatio,
|
||||
avatar: data.creator?.avatar ?? '',
|
||||
userName: data.creator?.nick_name ?? '',
|
||||
likes: data.react_info?.vote_count ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换 Share Elements 数据格式为 StencilItem 格式
|
||||
*/
|
||||
export function transformShareElementItem(item: ShareElementItem): StencilItem | null {
|
||||
const element = item.element;
|
||||
const share = item.share;
|
||||
|
||||
if (!element || !share) return null;
|
||||
|
||||
// 尝试从 creations 中获取第一个视频/图片
|
||||
const firstCreation = share.creations?.[0];
|
||||
let src = '';
|
||||
let poster = '';
|
||||
let aspectRatio = '1/1';
|
||||
|
||||
if (firstCreation) {
|
||||
// 从 creation 获取
|
||||
src = firstCreation.uri;
|
||||
poster = firstCreation.cover_uri;
|
||||
|
||||
// 计算宽高比
|
||||
const width = firstCreation.resolution?.width || 1920;
|
||||
const height = firstCreation.resolution?.height || 1080;
|
||||
aspectRatio = `${width}/${height}`;
|
||||
} else if (element.components?.[0]) {
|
||||
// 从 components 获取(降级方案)
|
||||
const component = element.components[0];
|
||||
src = component.content;
|
||||
poster = component.cover_uri || component.src_img;
|
||||
|
||||
// 尝试从 selected_region 获取宽高
|
||||
if (component.selected_region) {
|
||||
const { x: x1, y: y1 } = component.selected_region.top_left;
|
||||
const { x: x2, y: y2 } = component.selected_region.bottom_right;
|
||||
const width = x2 - x1;
|
||||
const height = y2 - y1;
|
||||
if (width > 0 && height > 0) {
|
||||
aspectRatio = `${width}/${height}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果仍然没有数据,返回 null
|
||||
if (!src) return null;
|
||||
|
||||
// 如果没有 poster,使用 src 作为 poster
|
||||
if (!poster) poster = src;
|
||||
|
||||
return {
|
||||
id: element.id,
|
||||
type: 'video',
|
||||
src,
|
||||
poster,
|
||||
aspectRatio,
|
||||
avatar: element.creator?.avatar ?? '',
|
||||
userName: element.creator?.nick_name ?? '',
|
||||
likes: share.react_info?.vote_count ?? 0,
|
||||
};
|
||||
}
|
||||
309
src/views/Home.vue
Normal file
309
src/views/Home.vue
Normal file
@ -0,0 +1,309 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="home">
|
||||
|
||||
<div class="background-video-stack">
|
||||
<video v-for="(video, index) in videoList" :key="index" class="background-video"
|
||||
:class="{ 'is-active': activeIndex === index }" autoplay muted loop :poster="video.poster">
|
||||
<source :src="video.src" type="video/mp4" />
|
||||
</video>
|
||||
</div>
|
||||
|
||||
<header>
|
||||
<div class="left">
|
||||
<img src="../assets/images/favicon.png" alt="logo" />
|
||||
<span class="header-title">元境</span>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="btn" @click="$router.push('/index')">立即体验</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="container">
|
||||
<h2 class="slogan">元启万象,境由心生</h2>
|
||||
<!-- 输入框容器 -->
|
||||
<div class="input-container">
|
||||
<!-- 头像区域:使用 transition 实现淡入淡出 -->
|
||||
<div class="avatar">
|
||||
<!-- <transition name="fade" mode="out-in"> -->
|
||||
<img :key="activeIndex" :src="videoList[activeIndex].poster" alt="avatar" />
|
||||
<!-- </transition> -->
|
||||
</div>
|
||||
<!-- 输入框:添加动态闪烁类,提示占位符变化 -->
|
||||
<input ref="inputRef" type="text" class="prompt-input" :placeholder="currentPlaceholder"
|
||||
:class="{ 'placeholder-flash': isFlashing }" />
|
||||
<div class="create-btn">创作</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, computed, watch } from 'vue'
|
||||
|
||||
// 视频数据:包含视频地址、封面图、对应的占位文本
|
||||
const videoList = [
|
||||
{
|
||||
src: 'https://console-download.s3.cn-north-1.jdcloud-oss.com/-lingjing-only/pre_creation/first_page/01-%E5%A5%B3%E4%BA%BA%E4%B8%8E%E8%8A%B1.mp4',
|
||||
poster: 'https://console-download.s3.cn-north-1.jdcloud-oss.com/-lingjing-only/pre_creation/first_page/%E5%A5%B3%E4%BA%BA%E4%B8%8E%E8%8A%B11.webp',
|
||||
placeholder: '一位优雅的女性与花丛,午后阳光洒在花瓣上...'
|
||||
},
|
||||
{
|
||||
src: 'https://console-download.s3.cn-north-1.jdcloud-oss.com/-lingjing-only/pre_creation/first_page/04-%E5%B0%8F%E5%A5%B3%E5%AD%A9%E4%B8%8E%E7%8C%AB.mp4',
|
||||
poster: 'https://console-download.s3.cn-north-1.jdcloud-oss.com/-lingjing-only/pre_creation/first_page/%E5%B0%8F%E5%A5%B3%E5%AD%A9%E4%B8%8E%E7%8C%AB1.webp',
|
||||
placeholder: '小女孩抱着猫,眼神温柔,毛茸茸的尾巴轻轻摆动...'
|
||||
},
|
||||
{
|
||||
src: 'https://console-download.s3.cn-north-1.jdcloud-oss.com/-lingjing-only/pre_creation/first_page/03-%E8%A1%97%E5%A4%B4%E6%BB%91%E6%9D%BF.mp4',
|
||||
poster: 'https://console-download.s3.cn-north-1.jdcloud-oss.com/-lingjing-only/pre_creation/first_page/%E8%A1%97%E5%A4%B4%E6%BB%91%E6%9D%BF1.webp',
|
||||
placeholder: '街头滑板少年,腾空跃起,动作流畅充满活力...'
|
||||
},
|
||||
{
|
||||
src: 'https://console-download.s3.cn-north-1.jdcloud-oss.com/-lingjing-only/pre_creation/first_page/05-%E4%B9%90%E9%AB%98%E9%A3%8E%E6%A0%BC%E6%9C%BA%E8%BD%A6.mp4',
|
||||
poster: 'https://console-download.s3.cn-north-1.jdcloud-oss.com/-lingjing-only/pre_creation/first_page/%E4%B9%90%E9%AB%98%E9%A3%8E%E6%A0%BC%E6%9C%BA%E8%BD%A61.webp',
|
||||
placeholder: '乐高风格的机车,色彩鲜艳,齿轮转动充满机械感...'
|
||||
}
|
||||
]
|
||||
|
||||
const activeIndex = ref(0) // 当前显示的视频索引
|
||||
let timer: number | null = null // 轮播定时器
|
||||
|
||||
// 根据当前激活的视频返回对应的占位文本
|
||||
const currentPlaceholder = computed(() => videoList[activeIndex.value].placeholder)
|
||||
|
||||
// 用于触发输入框闪烁动画
|
||||
const inputRef = ref<HTMLInputElement | null>(null)
|
||||
const isFlashing = ref(false)
|
||||
|
||||
// 监听 activeIndex 变化,触发占位符闪烁动画
|
||||
watch(activeIndex, () => {
|
||||
isFlashing.value = true
|
||||
setTimeout(() => {
|
||||
isFlashing.value = false
|
||||
}, 300) // 与动画时长保持一致
|
||||
})
|
||||
|
||||
// 开始轮播:每5秒切换到下一个视频
|
||||
const startCarousel = () => {
|
||||
timer = setInterval(() => {
|
||||
activeIndex.value = (activeIndex.value + 1) % videoList.length
|
||||
}, 5000) // 5秒切换一次
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
startCarousel()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) clearInterval(timer)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.home {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.background-video-stack {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
// z-index: 0; // 可根据需要取消注释
|
||||
}
|
||||
|
||||
.background-video {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
opacity: 0; // 默认隐藏所有视频
|
||||
pointer-events: none;
|
||||
transition: opacity .4s ease-in-out;
|
||||
}
|
||||
|
||||
.background-video.is-active {
|
||||
opacity: 1; // 当前激活的视频显示
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
position: relative;
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
img {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-family: "Microsoft Yahei", "PingFang SC", sans-serif;
|
||||
font-size: 30px;
|
||||
font-weight: 550;
|
||||
color: #fff;
|
||||
transform: skew(-8deg);
|
||||
-webkit-transform: skew(-12deg);
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
.btn {
|
||||
background-color: #000;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 40px;
|
||||
padding: 10px 28px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s, transform 0.1s;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05);
|
||||
letter-spacing: 0.3px;
|
||||
|
||||
&:hover {
|
||||
background-color: #2a2a2a;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 80px 0;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0%;
|
||||
|
||||
.container {
|
||||
// z-index: 999;
|
||||
|
||||
.slogan {
|
||||
font-size: 64px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 90%;
|
||||
max-width: 1000px;
|
||||
height: 56px;
|
||||
margin: 0 auto;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 28px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||
|
||||
.avatar {
|
||||
width: 40px;
|
||||
height: 24px;
|
||||
margin: 0 12px;
|
||||
// border-radius: 50%;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.prompt-input {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
padding: 0 8px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
caret-color: #000;
|
||||
transition: border-color 0.2s, background-color 0.2s;
|
||||
|
||||
&::placeholder {
|
||||
color: #fff;
|
||||
font-size: 15px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
|
||||
.placeholder-flash {
|
||||
border-color: rgba(255, 255, 255, 0.8) !important;
|
||||
background-color: rgba(255, 255, 255, 0.2) !important;
|
||||
animation: pulse 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
70% {
|
||||
box-shadow: 0 0 0 6px rgba(255, 255, 255, 0);
|
||||
}
|
||||
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.create-btn {
|
||||
height: 44px;
|
||||
min-width: 100px;
|
||||
margin: 0 6px;
|
||||
background-color: #000;
|
||||
color: #fff;
|
||||
border-radius: 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
background-color: #333;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 头像淡入淡出动画 */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
52
src/views/Homeindex.vue
Normal file
52
src/views/Homeindex.vue
Normal file
@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="common-layout">
|
||||
<el-container>
|
||||
<el-header>
|
||||
<Header></Header>
|
||||
</el-header>
|
||||
<el-container>
|
||||
<el-aside >
|
||||
<Menu></Menu>
|
||||
</el-aside>
|
||||
<el-main>
|
||||
<RouterView></RouterView>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang='ts'>
|
||||
import { RouterView } from 'vue-router';
|
||||
import Header from '../components/Header/Header.vue';
|
||||
import Menu from '@/components/left/Menu.vue';
|
||||
|
||||
// import { ref, reactive } from 'vue';
|
||||
</script>
|
||||
|
||||
<style scoped lang='less'>
|
||||
.el-aside {
|
||||
width: 224px;
|
||||
min-height: calc(100vh - 80px);
|
||||
background-color: #0F0F19;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.el-header {
|
||||
min-width: 100%;
|
||||
height: 80px;
|
||||
background-color: #0F0F19;
|
||||
}
|
||||
|
||||
.el-main {
|
||||
width: 100%;
|
||||
padding: 0 32px;
|
||||
height: calc(100vh - 80px);
|
||||
background-color: #0F0F19;
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
72
src/views/ImgGenerate/ImgGenerate.vue
Normal file
72
src/views/ImgGenerate/ImgGenerate.vue
Normal file
@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<div class="app">
|
||||
<div class="left-panel">
|
||||
<ImgLeft />
|
||||
</div>
|
||||
<div class="right-panel">
|
||||
<RightResult />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang='ts'>
|
||||
import ImgLeft from '@/components/img/ImgLeft.vue';
|
||||
import RightResult from '@/components/RightResult/RightResult.vue';
|
||||
|
||||
//import { ref, reactive } from 'vue';
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
:deep(.el-card__body){
|
||||
padding: 0 !important;
|
||||
}
|
||||
/* 禁止页面滚动,并占满全屏 */
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.left-panel {
|
||||
width: 35%; /* 与 leftBox 宽度一致 */
|
||||
height: 100%;
|
||||
overflow-y: auto; /* 让左侧区域独立滚动 */
|
||||
&::-webkit-scrollbar {
|
||||
// 滚动条宽度(垂直滚动条)/ 高度(水平滚动条)
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
// 轨道背景色
|
||||
background: #0F0F19;
|
||||
// 轨道圆角
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
// ========== 滚动条滑块样式 ==========
|
||||
&::-webkit-scrollbar-thumb {
|
||||
// 滑块背景色
|
||||
background: #0F0F19;
|
||||
// 滑块圆角
|
||||
border-radius: 3px;
|
||||
// 鼠标悬浮时的样式
|
||||
&:hover {
|
||||
background: #0F0F19;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right-panel {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
/* 如果右边不需要滚动,就不加 overflow */
|
||||
}
|
||||
</style>
|
||||
|
||||
28
src/views/index/index.vue
Normal file
28
src/views/index/index.vue
Normal file
@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="top"><Top /></div>
|
||||
<div class="AI"><AI /></div>
|
||||
<div class="stencil">
|
||||
<Stencil
|
||||
:gap="16"
|
||||
@like="handleLike"
|
||||
@make-same="handleMakeSame"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Top from '@/components/index/top.vue';
|
||||
import AI from '@/components/index/AIMode.vue';
|
||||
import Stencil from '@/components/index/Stencil.vue';
|
||||
import type { StencilItem } from '../../types/home';
|
||||
|
||||
const handleLike = (item: StencilItem) => {
|
||||
console.log('点赞', item);
|
||||
};
|
||||
|
||||
const handleMakeSame = (item: StencilItem) => {
|
||||
console.log('做同款', item);
|
||||
};
|
||||
</script>
|
||||
249
src/views/login/Login.vue
Normal file
249
src/views/login/Login.vue
Normal file
@ -0,0 +1,249 @@
|
||||
<template>
|
||||
<div class="login">
|
||||
<!-- 背景视频 -->
|
||||
<div class="background-video-stack">
|
||||
<video
|
||||
class="background-video"
|
||||
autoplay
|
||||
muted
|
||||
loop
|
||||
playsinline
|
||||
crossorigin="anonymous"
|
||||
src="../../assets/images/login.mp4"
|
||||
></video>
|
||||
</div>
|
||||
|
||||
<!-- 玻璃卡片表单 -->
|
||||
<div class="form-card">
|
||||
<div class="form-top">
|
||||
<h2 class="form-title">欢迎来到元境</h2>
|
||||
<span class="form-title"> 元启万象,境由心生 </span>
|
||||
</div>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
style="max-width: 400px"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="0px"
|
||||
class="login-form"
|
||||
>
|
||||
<!-- 手机号 -->
|
||||
<el-form-item prop="cellphone">
|
||||
<el-input
|
||||
v-model="form.cellphone"
|
||||
placeholder="请输入手机号"
|
||||
:prefix-icon="Iphone"
|
||||
size="large"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 验证码 + 获取按钮 -->
|
||||
<el-form-item prop="captcha">
|
||||
<div class="captcha-wrapper">
|
||||
<el-input
|
||||
v-model="form.captcha"
|
||||
placeholder="请输入验证码"
|
||||
:prefix-icon="Key"
|
||||
size="large"
|
||||
class="captcha-input"
|
||||
/>
|
||||
<el-button
|
||||
:disabled="countingDown || !validPhone"
|
||||
class="captcha-btn glass-btn"
|
||||
@click="handleGetCaptcha"
|
||||
>
|
||||
{{ countingDown ? `${countdown}秒后重试` : '获取验证码' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
class="submit-btn glass-btn"
|
||||
@click="submitForm"
|
||||
:loading="loading"
|
||||
>
|
||||
登 录 / 注 册
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="form-bottom">
|
||||
登录即表示您同意遵守《用户协议》和《隐私政策》
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 账号选择弹窗 -->
|
||||
<el-dialog
|
||||
v-model="loginStore.showAccountSelect"
|
||||
title="选择账号"
|
||||
width="400px"
|
||||
:close-on-click-modal="false"
|
||||
@close="handleDialogClose"
|
||||
>
|
||||
<div class="account-list">
|
||||
<div
|
||||
v-for="account in loginStore.accountList"
|
||||
:key="account.id"
|
||||
class="account-item"
|
||||
@click="selectAccount(account.id)"
|
||||
>
|
||||
<div class="account-info">
|
||||
<div class="account-name">{{ account.username }}</div>
|
||||
<!-- 显示手机号,优先使用 mobile 字段,其次 orgid -->
|
||||
<div class="account-phone">{{ account.mobile || account.orgid }}</div>
|
||||
</div>
|
||||
<el-button type="primary" size="small">选择</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, computed, onUnmounted } from 'vue';
|
||||
import type { FormInstance, FormRules } from 'element-plus';
|
||||
import { Iphone, Key } from '@element-plus/icons-vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { Code, LoginApi } from '@/apis/login';
|
||||
import type { CodeResult, LoginParams, CaptchaParams } from '@/types/login';
|
||||
import { useLoginStore } from '@/store/LoginStore';
|
||||
|
||||
// 登录Store
|
||||
const loginStore = useLoginStore();
|
||||
|
||||
// 表单数据类型
|
||||
interface LoginForm {
|
||||
cellphone: string;
|
||||
captcha: string;
|
||||
}
|
||||
|
||||
// 表单数据
|
||||
const form = reactive<LoginForm>({
|
||||
cellphone: '',
|
||||
captcha: '',
|
||||
});
|
||||
|
||||
// 表单实例引用
|
||||
const formRef = ref<FormInstance>();
|
||||
// 加载状态
|
||||
const loading = ref(false);
|
||||
// 登录参数(用于保存key)
|
||||
const loginParams = ref<LoginParams>({
|
||||
cellphone: '',
|
||||
key: '',
|
||||
sms_code: '',
|
||||
});
|
||||
|
||||
// 验证规则
|
||||
const rules = reactive<FormRules<LoginForm>>({
|
||||
cellphone: [
|
||||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' },
|
||||
],
|
||||
captcha: [
|
||||
{ required: true, message: '请输入验证码', trigger: 'blur' },
|
||||
{ len: 6, message: '验证码应为6位数字', trigger: 'blur' },
|
||||
],
|
||||
});
|
||||
|
||||
// 验证码倒计时
|
||||
const countdown = ref(60);
|
||||
const countingDown = ref(false);
|
||||
let timer: number | null = null;
|
||||
|
||||
// 手机号是否有效
|
||||
const validPhone = computed(() => {
|
||||
return /^1[3-9]\d{9}$/.test(form.cellphone);
|
||||
});
|
||||
|
||||
// 获取验证码
|
||||
const handleGetCaptcha = async () => {
|
||||
if (countingDown.value) return;
|
||||
|
||||
try {
|
||||
const res = await Code<CodeResult, CaptchaParams>({ cellphone: form.cellphone });
|
||||
if (res.status === 'ok') {
|
||||
ElMessage.success('发送成功');
|
||||
// 保存key
|
||||
loginParams.value.key = res.data?.key || '';
|
||||
} else {
|
||||
ElMessage.error('发送失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('发送验证码失败:', error);
|
||||
ElMessage.error('发送失败,请稍后重试');
|
||||
}
|
||||
|
||||
// 开始倒计时
|
||||
countingDown.value = true;
|
||||
countdown.value = 60;
|
||||
timer = setInterval(() => {
|
||||
countdown.value--;
|
||||
if (countdown.value <= 0) {
|
||||
clearInterval(timer!);
|
||||
timer = null;
|
||||
countingDown.value = false;
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
// 提交表单(登录)
|
||||
const submitForm = async () => {
|
||||
if (!formRef.value) return;
|
||||
|
||||
await formRef.value.validate(async (valid, fields) => {
|
||||
if (valid) {
|
||||
loading.value = true;
|
||||
try {
|
||||
// 准备登录参数
|
||||
loginParams.value.cellphone = form.cellphone;
|
||||
loginParams.value.sms_code = form.captcha;
|
||||
|
||||
// 调用登录接口(通过store)
|
||||
await loginStore.Login(loginParams.value);
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error);
|
||||
ElMessage.error('登录失败,请稍后重试');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
} else {
|
||||
console.log('验证失败:', fields);
|
||||
ElMessage.error('请正确填写表单');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 选择账号
|
||||
const selectAccount = async (selectedId: string) => {
|
||||
// 避免重复点击时多次请求
|
||||
if (loading.value) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
await loginStore.loginWithSelectedAccount(selectedId, loginParams.value);
|
||||
// 登录成功后会跳转,无需额外关闭弹窗(store 里已经关闭)
|
||||
} catch (error) {
|
||||
console.error('选择账号登录失败:', error);
|
||||
ElMessage.error('登录失败,请稍后重试');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 弹窗关闭时的回调(如果用户直接关闭,清空账号列表,避免下次残留)
|
||||
const handleDialogClose = () => {
|
||||
loginStore.accountList = [];
|
||||
};
|
||||
|
||||
// 组件卸载时清除定时器
|
||||
onUnmounted(() => {
|
||||
if (timer) clearInterval(timer);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
@import url('../../assets/less/Login/Login.less');
|
||||
</style>
|
||||
90
src/views/videoGenerate/videoGenerate.vue
Normal file
90
src/views/videoGenerate/videoGenerate.vue
Normal file
@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<div class="app">
|
||||
<div class="left-panel">
|
||||
<VideoLeft ref="videoLeftRef" @task-created="handleTaskCreated" />
|
||||
</div>
|
||||
<div class="right-panel">
|
||||
<RightResult ref="rightResultRef" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang='ts'>
|
||||
import { ref } from 'vue';
|
||||
import VideoLeft from '@/components/video/VideoLeft.vue';
|
||||
import RightResult from '@/components/RightResult/RightResult.vue';
|
||||
|
||||
const videoLeftRef = ref<InstanceType<typeof VideoLeft> | null>(null);
|
||||
const rightResultRef = ref<InstanceType<typeof RightResult> | null>(null);
|
||||
|
||||
// 处理任务创建事件,刷新任务列表
|
||||
const handleTaskCreated = (taskid: string) => {
|
||||
console.log('=== 任务创建事件被触发 ===');
|
||||
console.log('taskid:', taskid);
|
||||
console.log('rightResultRef.value:', rightResultRef.value);
|
||||
console.log('refreshTaskList 方法:', rightResultRef.value?.refreshTaskList);
|
||||
|
||||
if (rightResultRef.value && typeof rightResultRef.value.refreshTaskList === 'function') {
|
||||
console.log('开始刷新任务列表...');
|
||||
rightResultRef.value.refreshTaskList();
|
||||
console.log('任务列表刷新完成');
|
||||
} else {
|
||||
console.error('refreshTaskList 方法不存在或不可用');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
:deep(.el-card__body){
|
||||
padding: 0 !important;
|
||||
}
|
||||
/* 禁止页面滚动,并占满全屏 */
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.left-panel {
|
||||
width: 35%; /* 与 leftBox 宽度一致 */
|
||||
height: 100%;
|
||||
overflow-y: auto; /* 让左侧区域独立滚动 */
|
||||
&::-webkit-scrollbar {
|
||||
// 滚动条宽度(垂直滚动条)/ 高度(水平滚动条)
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
// 轨道背景色
|
||||
background: #0F0F19;
|
||||
// 轨道圆角
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
// ========== 滚动条滑块样式 ==========
|
||||
&::-webkit-scrollbar-thumb {
|
||||
// 滑块背景色
|
||||
background: #0F0F19;
|
||||
// 滑块圆角
|
||||
border-radius: 3px;
|
||||
// 鼠标悬浮时的样式
|
||||
&:hover {
|
||||
background: #0F0F19;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right-panel {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
/* 如果右边不需要滚动,就不加 overflow */
|
||||
}
|
||||
</style>
|
||||
|
||||
7
src/vite-env.d.ts
vendored
Normal file
7
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
24
tsconfig.app.json
Normal file
24
tsconfig.app.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
7
tsconfig.json
Normal file
7
tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
25
tsconfig.node.json
Normal file
25
tsconfig.node.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
30
vite.config.ts
Normal file
30
vite.config.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue(),],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
},
|
||||
server: {
|
||||
host: 'localhost',
|
||||
port: 5173,
|
||||
// 配置代理解决跨域问题
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'https://opencomputing.ai',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||
}
|
||||
}
|
||||
},
|
||||
// optimizeDeps: {
|
||||
// exclude: ['@techui/scifi']
|
||||
// },
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user