first commit

This commit is contained in:
hrx 2026-04-01 11:05:03 +08:00
parent e2b0aff5f3
commit a3c0fba87a
69 changed files with 14713 additions and 0 deletions

1
.env.development Normal file
View File

@ -0,0 +1 @@
VITE_API_BASE_URL = 'https://opencomputing.ai'

24
.gitignore vendored Normal file
View 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
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

13
index.html Normal file
View 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

File diff suppressed because it is too large Load Diff

35
package.json Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

27
src/App.vue Normal file
View 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
View 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
View 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
View 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)

View 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;
}

View 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">&#xe615;</span>
<div class="name">余额</div>
<div class="code-name">&amp;#xe615;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe634;</span>
<div class="name">支付宝支付</div>
<div class="code-name">&amp;#xe634;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe84f;</span>
<div class="name">警告</div>
<div class="code-name">&amp;#xe84f;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe716;</span>
<div class="name"></div>
<div class="code-name">&amp;#xe716;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe623;</span>
<div class="name">验证码</div>
<div class="code-name">&amp;#xe623;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe61c;</span>
<div class="name">003_手机号-53</div>
<div class="code-name">&amp;#xe61c;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe603;</span>
<div class="name">数字人</div>
<div class="code-name">&amp;#xe603;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe604;</span>
<div class="name">音频生成</div>
<div class="code-name">&amp;#xe604;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe622;</span>
<div class="name"></div>
<div class="code-name">&amp;#xe622;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe60f;</span>
<div class="name"></div>
<div class="code-name">&amp;#xe60f;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe600;</span>
<div class="name">img2video</div>
<div class="code-name">&amp;#xe600;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe601;</span>
<div class="name">image-edit</div>
<div class="code-name">&amp;#xe601;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe602;</span>
<div class="name">无限画布</div>
<div class="code-name">&amp;#xe602;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe636;</span>
<div class="name">图片生成</div>
<div class="code-name">&amp;#xe636;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe62e;</span>
<div class="name">首页</div>
<div class="code-name">&amp;#xe62e;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe63c;</span>
<div class="name">个人中心</div>
<div class="code-name">&amp;#xe63c;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe605;</span>
<div class="name">发现</div>
<div class="code-name">&amp;#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"
>&lt;span class="iconfont"&gt;&amp;#x33;&lt;/span&gt;
</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">&lt;link rel="stylesheet" href="./iconfont.css"&gt;
</code></pre>
<h3 id="-">第二步:挑选相应图标并获取类名,应用于页面:</h3>
<pre><code class="language-html">&lt;span class="iconfont icon-xxx"&gt;&lt;/span&gt;
</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">&lt;script src="./iconfont.js"&gt;&lt;/script&gt;
</code></pre>
<h3 id="-css-">第二步:加入通用 CSS 代码(引入一次就行):</h3>
<pre><code class="language-html">&lt;style&gt;
.icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
&lt;/style&gt;
</code></pre>
<h3 id="-">第三步:挑选相应图标并获取类名,应用于页面:</h3>
<pre><code class="language-html">&lt;svg class="icon" aria-hidden="true"&gt;
&lt;use xlink:href="#icon-xxx"&gt;&lt;/use&gt;
&lt;/svg&gt;
</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>

View 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";
}

File diff suppressed because one or more lines are too long

View 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
}
]
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
src/assets/images/login.mp4 Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View 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);
}

View 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);
}

View 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;
}

View 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;
}
}
}

View 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;
}

View 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;
}
}
}

View 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;
}

View 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;
}
}

View 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;
}

View 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
View 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

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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-1616px=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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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>

View 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
View 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
View 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>

View 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
View 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
View 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
View File

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

25
tsconfig.node.json Normal file
View 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
View 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']
// },
})