Compare commits
No commits in common. "master" and "main" have entirely different histories.
@ -1 +0,0 @@
|
|||||||
VITE_API_BASE_URL=https://token.opencomputing.cn
|
|
||||||
@ -1 +0,0 @@
|
|||||||
VITE_API_BASE_URL=https://token.opencomputing.cn
|
|
||||||
24
.gitignore
vendored
24
.gitignore
vendored
@ -1,24 +0,0 @@
|
|||||||
# 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
3
.vscode/extensions.json
vendored
@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"recommendations": ["Vue.volar"]
|
|
||||||
}
|
|
||||||
46
index.html
46
index.html
@ -1,46 +0,0 @@
|
|||||||
<!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>
|
|
||||||
;(function () {
|
|
||||||
var syncPath = '#/getCookie'
|
|
||||||
var reloadPrefix = 'yuanjing-get-cookie-reload:'
|
|
||||||
|
|
||||||
function isGetCookieRoute() {
|
|
||||||
return window.location.hash.indexOf(syncPath) === 0
|
|
||||||
}
|
|
||||||
|
|
||||||
function reloadOnce(reason) {
|
|
||||||
if (!isGetCookieRoute()) return
|
|
||||||
|
|
||||||
var key = reloadPrefix + reason + ':' + window.location.hash
|
|
||||||
if (window.sessionStorage.getItem(key)) return
|
|
||||||
|
|
||||||
window.sessionStorage.setItem(key, '1')
|
|
||||||
var url = new URL(window.location.href)
|
|
||||||
url.searchParams.set('_syncReload', Date.now().toString())
|
|
||||||
window.location.replace(url.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('hashchange', function () {
|
|
||||||
reloadOnce('hashchange')
|
|
||||||
})
|
|
||||||
|
|
||||||
window.setTimeout(function () {
|
|
||||||
var app = document.getElementById('app')
|
|
||||||
if (app && app.childElementCount === 0) {
|
|
||||||
reloadOnce('empty-app')
|
|
||||||
}
|
|
||||||
}, 1200)
|
|
||||||
})()
|
|
||||||
</script>
|
|
||||||
<script type="module" src="/src/main.ts"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
3655
package-lock.json
generated
3655
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
37
package.json
37
package.json
@ -1,37 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "yuanjing",
|
|
||||||
"private": true,
|
|
||||||
"version": "0.0.0",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite",
|
|
||||||
"build": "vite build",
|
|
||||||
"build:test": "vite build --mode test",
|
|
||||||
"build:prod": "vite build --mode production",
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
30
src/App.vue
30
src/App.vue
@ -1,30 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { onMounted } from 'vue';
|
|
||||||
import { useLoginStore } from './store/LoginStore';
|
|
||||||
import { ElConfigProvider } from 'element-plus'
|
|
||||||
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
|
||||||
const loginStore = useLoginStore();
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
// 初始化自动登出计时器
|
|
||||||
loginStore.initAutoLogout();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<el-config-provider :locale="zhCn">
|
|
||||||
<div>
|
|
||||||
<router-view>
|
|
||||||
</router-view>
|
|
||||||
</div>
|
|
||||||
</el-config-provider>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="less">
|
|
||||||
*{
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
@ -1,204 +0,0 @@
|
|||||||
import { get, getApiBaseURL, post } from '../utlis/request'
|
|
||||||
import type { TextToTextParams } from '@/types/model'
|
|
||||||
|
|
||||||
// 文生视频
|
|
||||||
export const textvideo = <T,D>(data:D):Promise<T> =>post('/llmage/vidu_inference.dspy',data)
|
|
||||||
|
|
||||||
// 图生视频
|
|
||||||
export const imagetovideo = <T,D>(data:D):Promise<T> =>post('/llmage/vidu_inference.dspy',data)
|
|
||||||
|
|
||||||
// 参考生视频
|
|
||||||
export const referencevideo = <T,D>(data:D):Promise<T> =>post('/llmage/video',data)
|
|
||||||
|
|
||||||
// 文生图
|
|
||||||
export const texttoimage = <T,D>(data:D):Promise<T> =>post('/llmage/v1/image/generations',data)
|
|
||||||
|
|
||||||
// 文生文
|
|
||||||
export const texttoText = <T,D>(data:D):Promise<T> =>post('/llmage/v1/chat/completions',data)
|
|
||||||
|
|
||||||
const isDonePayload = (payload: string): boolean => /^\[\s*DONE\s*\]$/i.test(payload.trim())
|
|
||||||
|
|
||||||
const isStructuredPayload = (payload: string): boolean => {
|
|
||||||
const firstChar = payload.trim().charAt(0)
|
|
||||||
return firstChar === '{' || firstChar === '['
|
|
||||||
}
|
|
||||||
|
|
||||||
const trimEventPayload = (payload: string): string => payload
|
|
||||||
.replace(/(?:\\r\\n|\\n|\r?\n)+$/g, '')
|
|
||||||
.trim()
|
|
||||||
|
|
||||||
const isCompletePayload = (payload: string): boolean => {
|
|
||||||
const text = payload.trim()
|
|
||||||
if (!text) return false
|
|
||||||
if (isDonePayload(text) || !isStructuredPayload(text)) return true
|
|
||||||
|
|
||||||
let depth = 0
|
|
||||||
let inString = false
|
|
||||||
let escaped = false
|
|
||||||
|
|
||||||
for (const char of text) {
|
|
||||||
if (escaped) {
|
|
||||||
escaped = false
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (char === '\\') {
|
|
||||||
escaped = inString
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (char === '"') {
|
|
||||||
inString = !inString
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inString) continue
|
|
||||||
|
|
||||||
if (char === '{' || char === '[') {
|
|
||||||
depth += 1
|
|
||||||
} else if (char === '}' || char === ']') {
|
|
||||||
depth -= 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return depth === 0 && !inString
|
|
||||||
}
|
|
||||||
|
|
||||||
const extractTextFromPayload = (payload: string): string => {
|
|
||||||
const normalizedPayload = trimEventPayload(payload)
|
|
||||||
if (!normalizedPayload || isDonePayload(normalizedPayload)) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(normalizedPayload)
|
|
||||||
const contentCandidates = [
|
|
||||||
parsed?.choices?.[0]?.delta?.content,
|
|
||||||
parsed?.choices?.[0]?.message?.content,
|
|
||||||
parsed?.data?.content,
|
|
||||||
parsed?.data?.answer,
|
|
||||||
parsed?.content,
|
|
||||||
parsed?.answer,
|
|
||||||
parsed?.text,
|
|
||||||
]
|
|
||||||
const content = contentCandidates.find((item): item is string => typeof item === 'string' && item.length > 0)
|
|
||||||
|
|
||||||
return content || ''
|
|
||||||
} catch {
|
|
||||||
return isStructuredPayload(normalizedPayload) ? '' : normalizedPayload
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const readPayloadsFromBuffer = (buffer: string, flush = false): { payloads: string[]; rest: string } => {
|
|
||||||
const payloads: string[] = []
|
|
||||||
let rest = buffer
|
|
||||||
const dataMarkerPattern = /(?:^|\r?\n|\\n)data:\s*/
|
|
||||||
const nextDataMarkerPattern = /(?:\r?\n|\\n)data:\s*/
|
|
||||||
|
|
||||||
while (rest) {
|
|
||||||
const startMatch = dataMarkerPattern.exec(rest)
|
|
||||||
if (!startMatch) {
|
|
||||||
return {
|
|
||||||
payloads: flush && rest.trim() ? [...payloads, rest] : payloads,
|
|
||||||
rest: flush ? '' : rest,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const payloadStart = startMatch.index + startMatch[0].length
|
|
||||||
const candidateText = rest.slice(payloadStart)
|
|
||||||
const nextMatch = nextDataMarkerPattern.exec(candidateText)
|
|
||||||
|
|
||||||
if (nextMatch) {
|
|
||||||
payloads.push(trimEventPayload(candidateText.slice(0, nextMatch.index)))
|
|
||||||
rest = candidateText.slice(nextMatch.index)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const candidatePayload = trimEventPayload(candidateText)
|
|
||||||
if (flush || isCompletePayload(candidatePayload)) {
|
|
||||||
payloads.push(candidatePayload)
|
|
||||||
rest = ''
|
|
||||||
} else {
|
|
||||||
rest = rest.slice(startMatch.index)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
return { payloads, rest }
|
|
||||||
}
|
|
||||||
|
|
||||||
const extractTextFromChunk = (chunk: string): string => {
|
|
||||||
const { payloads } = readPayloadsFromBuffer(chunk, true)
|
|
||||||
return payloads.reduce((result, payload) => result + extractTextFromPayload(payload), '')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 文生文流式输出
|
|
||||||
export const textToTextStream = async (
|
|
||||||
data: TextToTextParams,
|
|
||||||
onMessage: (content: string) => void,
|
|
||||||
signal?: AbortSignal
|
|
||||||
) => {
|
|
||||||
const response = await fetch(`${getApiBaseURL()}/llmage/v1/chat/completions`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
credentials: 'include',
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
signal,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`文生文请求失败:${response.status}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response.body) {
|
|
||||||
const text = await response.text()
|
|
||||||
onMessage(extractTextFromChunk(text))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const reader = response.body.getReader()
|
|
||||||
const decoder = new TextDecoder('utf-8')
|
|
||||||
let pendingText = ''
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const { done, value } = await reader.read()
|
|
||||||
if (done) break
|
|
||||||
|
|
||||||
pendingText += decoder.decode(value, { stream: true })
|
|
||||||
const { payloads, rest } = readPayloadsFromBuffer(pendingText)
|
|
||||||
pendingText = rest
|
|
||||||
|
|
||||||
payloads.forEach((payload) => {
|
|
||||||
const content = extractTextFromPayload(payload)
|
|
||||||
if (content) {
|
|
||||||
onMessage(content)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pendingText += decoder.decode()
|
|
||||||
const { payloads } = readPayloadsFromBuffer(pendingText, true)
|
|
||||||
payloads.forEach((payload) => {
|
|
||||||
const content = extractTextFromPayload(payload)
|
|
||||||
if (content) {
|
|
||||||
onMessage(content)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取所有模型
|
|
||||||
export const AllModel = <T,D>(data:D):Promise<T> =>get('/llmage/get_type_llms.dspy',data)
|
|
||||||
|
|
||||||
// 查询当前模型价格
|
|
||||||
export const queryModelPrice = <T,D>(data:D):Promise<T> =>post('/platformbiz/product_query_price.dspy',data)
|
|
||||||
|
|
||||||
// 获取当天任务列表
|
|
||||||
export const getTodayTask = <T,D = undefined>(data?:D):Promise<T> =>get('/llmage/get_my_asynctasks.dspy',data)
|
|
||||||
|
|
||||||
// 获取任务状态
|
|
||||||
export const getAsyncTaskStatus = <T,D>(data:D):Promise<T> =>get('/llmage/get_asynctask_status.dspy',data)
|
|
||||||
|
|
||||||
// 历史记录
|
|
||||||
export const getHistoryRecord = <T,D>(data:D):Promise<T> =>get('/llmage/api/get_inference_history.dspy',data)
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
import { get, post } from '@/utlis/request';
|
|
||||||
|
|
||||||
// 获取当前账号下可查看的 API Key 应用列表
|
|
||||||
export const getApiKeyList = <T>(): Promise<T> => get('/dapi/downapps.dspy');
|
|
||||||
|
|
||||||
// 申请新的 API Key
|
|
||||||
export const applyApiKey = <T, D>(data: D): Promise<T> => post('/dapi/apply_apikey.dspy', data);
|
|
||||||
|
|
||||||
// 根据应用 id 查看对应的 API Key 详情
|
|
||||||
export const getApiKeyDetail = <T, D>(id:D ): Promise<T> => get('/dapi/get_apikey.dspy', id);
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
import { get, post } from '../utlis/request'
|
|
||||||
|
|
||||||
// 获取视频列表
|
|
||||||
export const getVideos = <T, D>(data: D): Promise<T> => get('https://vidu.opencomputing.cn/v1/feed/list', data)
|
|
||||||
|
|
||||||
// 充值
|
|
||||||
export const recharge = <T, D>(data: D): Promise<T> => post('/unipay/user_recharge.dspy', data)
|
|
||||||
|
|
||||||
// 开户
|
|
||||||
export const openAccount = <T>(): Promise<T> => post('/accounting/open_customer_accounts.dspy')
|
|
||||||
|
|
||||||
// 余额
|
|
||||||
export const balance = <T>(): Promise<T> => get('/accounting/mybalance.dspy')
|
|
||||||
|
|
||||||
// 充值记录
|
|
||||||
export const consumptionRecord = <T,D>(data:D):Promise<T> => get('/platformbiz/query_orders.dspy',data)
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
import {get, post} from '@/utlis/request'
|
|
||||||
|
|
||||||
export const Code = <T,D>(data:D):Promise<T> =>get('/rbac/gen_sms_code.dspy',data)
|
|
||||||
|
|
||||||
export const LoginApi = <T,D>(data:D):Promise<T> =>post('/rbac/phone_login.dspy',data)
|
|
||||||
@ -1,539 +0,0 @@
|
|||||||
/* 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;
|
|
||||||
}
|
|
||||||
@ -1,832 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8"/>
|
|
||||||
<title>iconfont Demo</title>
|
|
||||||
<link rel="shortcut icon" href="//img.alicdn.com/imgextra/i4/O1CN01Z5paLz1O0zuCC7osS_!!6000000001644-55-tps-83-82.svg" type="image/x-icon"/>
|
|
||||||
<link rel="icon" type="image/svg+xml" href="//img.alicdn.com/imgextra/i4/O1CN01Z5paLz1O0zuCC7osS_!!6000000001644-55-tps-83-82.svg"/>
|
|
||||||
<link rel="stylesheet" href="https://g.alicdn.com/thx/cube/1.3.2/cube.min.css">
|
|
||||||
<link rel="stylesheet" href="demo.css">
|
|
||||||
<link rel="stylesheet" href="iconfont.css">
|
|
||||||
<script src="iconfont.js"></script>
|
|
||||||
<!-- jQuery -->
|
|
||||||
<script src="https://a1.alicdn.com/oss/uploads/2018/12/26/7bfddb60-08e8-11e9-9b04-53e73bb6408b.js"></script>
|
|
||||||
<!-- 代码高亮 -->
|
|
||||||
<script src="https://a1.alicdn.com/oss/uploads/2018/12/26/a3f714d0-08e6-11e9-8a15-ebf944d7534c.js"></script>
|
|
||||||
<style>
|
|
||||||
.main .logo {
|
|
||||||
margin-top: 0;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main .logo a {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main .logo .sub-title {
|
|
||||||
margin-left: 0.5em;
|
|
||||||
font-size: 22px;
|
|
||||||
color: #fff;
|
|
||||||
background: linear-gradient(-45deg, #3967FF, #B500FE);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="main">
|
|
||||||
<h1 class="logo"><a href="https://www.iconfont.cn/" title="iconfont 首页" target="_blank">
|
|
||||||
<img width="200" src="https://img.alicdn.com/imgextra/i3/O1CN01Mn65HV1FfSEzR6DKv_!!6000000000514-55-tps-228-59.svg">
|
|
||||||
|
|
||||||
</a></h1>
|
|
||||||
<div class="nav-tabs">
|
|
||||||
<ul id="tabs" class="dib-box">
|
|
||||||
<li class="dib active"><span>Unicode</span></li>
|
|
||||||
<li class="dib"><span>Font class</span></li>
|
|
||||||
<li class="dib"><span>Symbol</span></li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<a href="https://www.iconfont.cn/manage/index?manage_type=myprojects&projectId=5136943" target="_blank" class="nav-more">查看项目</a>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="tab-container">
|
|
||||||
<div class="content unicode" style="display: block;">
|
|
||||||
<ul class="icon_lists dib-box">
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">下载</div>
|
|
||||||
<div class="code-name">&#xe621;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">箭头_上下切换_o</div>
|
|
||||||
<div class="code-name">&#xeb90;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">搜索</div>
|
|
||||||
<div class="code-name">&#xe619;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">我的创作</div>
|
|
||||||
<div class="code-name">&#xe61e;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">上</div>
|
|
||||||
<div class="code-name">&#xe62f;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">下</div>
|
|
||||||
<div class="code-name">&#xe611;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">消息</div>
|
|
||||||
<div class="code-name">&#xe8bd;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">17A发送</div>
|
|
||||||
<div class="code-name">&#xe67d;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">414126695</div>
|
|
||||||
<div class="code-name">&#xe606;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">apikey</div>
|
|
||||||
<div class="code-name">&#xe617;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">消费记录</div>
|
|
||||||
<div class="code-name">&#xe654;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">余额</div>
|
|
||||||
<div class="code-name">&#xe615;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">支付宝支付</div>
|
|
||||||
<div class="code-name">&#xe634;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">警告</div>
|
|
||||||
<div class="code-name">&#xe84f;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">加</div>
|
|
||||||
<div class="code-name">&#xe716;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">验证码</div>
|
|
||||||
<div class="code-name">&#xe623;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">003_手机号-53</div>
|
|
||||||
<div class="code-name">&#xe61c;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">数字人</div>
|
|
||||||
<div class="code-name">&#xe603;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">音频生成</div>
|
|
||||||
<div class="code-name">&#xe604;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">左</div>
|
|
||||||
<div class="code-name">&#xe622;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">右</div>
|
|
||||||
<div class="code-name">&#xe60f;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">img2video</div>
|
|
||||||
<div class="code-name">&#xe600;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">image-edit</div>
|
|
||||||
<div class="code-name">&#xe601;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">无限画布</div>
|
|
||||||
<div class="code-name">&#xe602;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">图片生成</div>
|
|
||||||
<div class="code-name">&#xe636;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">首页</div>
|
|
||||||
<div class="code-name">&#xe62e;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">个人中心</div>
|
|
||||||
<div class="code-name">&#xe63c;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont"></span>
|
|
||||||
<div class="name">发现</div>
|
|
||||||
<div class="code-name">&#xe605;</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
</ul>
|
|
||||||
<div class="article markdown">
|
|
||||||
<h2 id="unicode-">Unicode 引用</h2>
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<p>Unicode 是字体在网页端最原始的应用方式,特点是:</p>
|
|
||||||
<ul>
|
|
||||||
<li>支持按字体的方式去动态调整图标大小,颜色等等。</li>
|
|
||||||
<li>默认情况下不支持多色,直接添加多色图标会自动去色。</li>
|
|
||||||
</ul>
|
|
||||||
<blockquote>
|
|
||||||
<p>注意:新版 iconfont 支持两种方式引用多色图标:SVG symbol 引用方式和彩色字体图标模式。(使用彩色字体图标需要在「编辑项目」中开启「彩色」选项后并重新生成。)</p>
|
|
||||||
</blockquote>
|
|
||||||
<p>Unicode 使用步骤如下:</p>
|
|
||||||
<h3 id="-font-face">第一步:拷贝项目下面生成的 <code>@font-face</code></h3>
|
|
||||||
<pre><code class="language-css"
|
|
||||||
>@font-face {
|
|
||||||
font-family: 'iconfont';
|
|
||||||
src: url('iconfont.woff2?t=1780995416168') format('woff2'),
|
|
||||||
url('iconfont.woff?t=1780995416168') format('woff'),
|
|
||||||
url('iconfont.ttf?t=1780995416168') format('truetype');
|
|
||||||
}
|
|
||||||
</code></pre>
|
|
||||||
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
|
|
||||||
<pre><code class="language-css"
|
|
||||||
>.iconfont {
|
|
||||||
font-family: "iconfont" !important;
|
|
||||||
font-size: 16px;
|
|
||||||
font-style: normal;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
</code></pre>
|
|
||||||
<h3 id="-">第三步:挑选相应图标并获取字体编码,应用于页面</h3>
|
|
||||||
<pre>
|
|
||||||
<code class="language-html"
|
|
||||||
><span class="iconfont">&#x33;</span>
|
|
||||||
</code></pre>
|
|
||||||
<blockquote>
|
|
||||||
<p>"iconfont" 是你项目下的 font-family。可以通过编辑项目查看,默认是 "iconfont"。</p>
|
|
||||||
</blockquote>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="content font-class">
|
|
||||||
<ul class="icon_lists dib-box">
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-xiazai"></span>
|
|
||||||
<div class="name">
|
|
||||||
下载
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-xiazai
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-jiantou_shangxiaqiehuan_o"></span>
|
|
||||||
<div class="name">
|
|
||||||
箭头_上下切换_o
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-jiantou_shangxiaqiehuan_o
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-sousuo"></span>
|
|
||||||
<div class="name">
|
|
||||||
搜索
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-sousuo
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-wodechuangzuo"></span>
|
|
||||||
<div class="name">
|
|
||||||
我的创作
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-wodechuangzuo
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-shang"></span>
|
|
||||||
<div class="name">
|
|
||||||
上
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-shang
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-xia"></span>
|
|
||||||
<div class="name">
|
|
||||||
下
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-xia
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-xiaoxi"></span>
|
|
||||||
<div class="name">
|
|
||||||
消息
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-xiaoxi
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-a-17Afasong"></span>
|
|
||||||
<div class="name">
|
|
||||||
17A发送
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-a-17Afasong
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-a-414126695"></span>
|
|
||||||
<div class="name">
|
|
||||||
414126695
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-a-414126695
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-apikey"></span>
|
|
||||||
<div class="name">
|
|
||||||
apikey
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-apikey
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-xiaofeijilu"></span>
|
|
||||||
<div class="name">
|
|
||||||
消费记录
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-xiaofeijilu
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-yue"></span>
|
|
||||||
<div class="name">
|
|
||||||
余额
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-yue
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-zhifubaozhifu"></span>
|
|
||||||
<div class="name">
|
|
||||||
支付宝支付
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-zhifubaozhifu
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-jinggao"></span>
|
|
||||||
<div class="name">
|
|
||||||
警告
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-jinggao
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-jia"></span>
|
|
||||||
<div class="name">
|
|
||||||
加
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-jia
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-yanzhengma"></span>
|
|
||||||
<div class="name">
|
|
||||||
验证码
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-yanzhengma
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-a-003_shoujihao-53"></span>
|
|
||||||
<div class="name">
|
|
||||||
003_手机号-53
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-a-003_shoujihao-53
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-a-387845729"></span>
|
|
||||||
<div class="name">
|
|
||||||
数字人
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-a-387845729
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-a-387846145"></span>
|
|
||||||
<div class="name">
|
|
||||||
音频生成
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-a-387846145
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-zuo"></span>
|
|
||||||
<div class="name">
|
|
||||||
左
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-zuo
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-you"></span>
|
|
||||||
<div class="name">
|
|
||||||
右
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-you
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-img2video"></span>
|
|
||||||
<div class="name">
|
|
||||||
img2video
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-img2video
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-image-edit"></span>
|
|
||||||
<div class="name">
|
|
||||||
image-edit
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-image-edit
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-a-386885351"></span>
|
|
||||||
<div class="name">
|
|
||||||
无限画布
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-a-386885351
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-tupianshengcheng"></span>
|
|
||||||
<div class="name">
|
|
||||||
图片生成
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-tupianshengcheng
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-shouye"></span>
|
|
||||||
<div class="name">
|
|
||||||
首页
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-shouye
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-gerenzhongxin"></span>
|
|
||||||
<div class="name">
|
|
||||||
个人中心
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-gerenzhongxin
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<span class="icon iconfont icon-faxian"></span>
|
|
||||||
<div class="name">
|
|
||||||
发现
|
|
||||||
</div>
|
|
||||||
<div class="code-name">.icon-faxian
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
</ul>
|
|
||||||
<div class="article markdown">
|
|
||||||
<h2 id="font-class-">font-class 引用</h2>
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<p>font-class 是 Unicode 使用方式的一种变种,主要是解决 Unicode 书写不直观,语意不明确的问题。</p>
|
|
||||||
<p>与 Unicode 使用方式相比,具有如下特点:</p>
|
|
||||||
<ul>
|
|
||||||
<li>相比于 Unicode 语意明确,书写更直观。可以很容易分辨这个 icon 是什么。</li>
|
|
||||||
<li>因为使用 class 来定义图标,所以当要替换图标时,只需要修改 class 里面的 Unicode 引用。</li>
|
|
||||||
</ul>
|
|
||||||
<p>使用步骤如下:</p>
|
|
||||||
<h3 id="-fontclass-">第一步:引入项目下面生成的 fontclass 代码:</h3>
|
|
||||||
<pre><code class="language-html"><link rel="stylesheet" href="./iconfont.css">
|
|
||||||
</code></pre>
|
|
||||||
<h3 id="-">第二步:挑选相应图标并获取类名,应用于页面:</h3>
|
|
||||||
<pre><code class="language-html"><span class="iconfont icon-xxx"></span>
|
|
||||||
</code></pre>
|
|
||||||
<blockquote>
|
|
||||||
<p>"
|
|
||||||
iconfont" 是你项目下的 font-family。可以通过编辑项目查看,默认是 "iconfont"。</p>
|
|
||||||
</blockquote>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="content symbol">
|
|
||||||
<ul class="icon_lists dib-box">
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-xiazai"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">下载</div>
|
|
||||||
<div class="code-name">#icon-xiazai</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-jiantou_shangxiaqiehuan_o"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">箭头_上下切换_o</div>
|
|
||||||
<div class="code-name">#icon-jiantou_shangxiaqiehuan_o</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-sousuo"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">搜索</div>
|
|
||||||
<div class="code-name">#icon-sousuo</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-wodechuangzuo"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">我的创作</div>
|
|
||||||
<div class="code-name">#icon-wodechuangzuo</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-shang"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">上</div>
|
|
||||||
<div class="code-name">#icon-shang</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-xia"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">下</div>
|
|
||||||
<div class="code-name">#icon-xia</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-xiaoxi"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">消息</div>
|
|
||||||
<div class="code-name">#icon-xiaoxi</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-a-17Afasong"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">17A发送</div>
|
|
||||||
<div class="code-name">#icon-a-17Afasong</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-a-414126695"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">414126695</div>
|
|
||||||
<div class="code-name">#icon-a-414126695</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-apikey"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">apikey</div>
|
|
||||||
<div class="code-name">#icon-apikey</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-xiaofeijilu"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">消费记录</div>
|
|
||||||
<div class="code-name">#icon-xiaofeijilu</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-yue"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">余额</div>
|
|
||||||
<div class="code-name">#icon-yue</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-zhifubaozhifu"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">支付宝支付</div>
|
|
||||||
<div class="code-name">#icon-zhifubaozhifu</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-jinggao"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">警告</div>
|
|
||||||
<div class="code-name">#icon-jinggao</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-jia"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">加</div>
|
|
||||||
<div class="code-name">#icon-jia</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-yanzhengma"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">验证码</div>
|
|
||||||
<div class="code-name">#icon-yanzhengma</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-a-003_shoujihao-53"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">003_手机号-53</div>
|
|
||||||
<div class="code-name">#icon-a-003_shoujihao-53</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-a-387845729"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">数字人</div>
|
|
||||||
<div class="code-name">#icon-a-387845729</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-a-387846145"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">音频生成</div>
|
|
||||||
<div class="code-name">#icon-a-387846145</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-zuo"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">左</div>
|
|
||||||
<div class="code-name">#icon-zuo</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-you"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">右</div>
|
|
||||||
<div class="code-name">#icon-you</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-img2video"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">img2video</div>
|
|
||||||
<div class="code-name">#icon-img2video</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-image-edit"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">image-edit</div>
|
|
||||||
<div class="code-name">#icon-image-edit</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-a-386885351"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">无限画布</div>
|
|
||||||
<div class="code-name">#icon-a-386885351</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-tupianshengcheng"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">图片生成</div>
|
|
||||||
<div class="code-name">#icon-tupianshengcheng</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-shouye"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">首页</div>
|
|
||||||
<div class="code-name">#icon-shouye</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-gerenzhongxin"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">个人中心</div>
|
|
||||||
<div class="code-name">#icon-gerenzhongxin</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dib">
|
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-faxian"></use>
|
|
||||||
</svg>
|
|
||||||
<div class="name">发现</div>
|
|
||||||
<div class="code-name">#icon-faxian</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
</ul>
|
|
||||||
<div class="article markdown">
|
|
||||||
<h2 id="symbol-">Symbol 引用</h2>
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<p>这是一种全新的使用方式,应该说这才是未来的主流,也是平台目前推荐的用法。相关介绍可以参考这篇<a href="">文章</a>
|
|
||||||
这种用法其实是做了一个 SVG 的集合,与另外两种相比具有如下特点:</p>
|
|
||||||
<ul>
|
|
||||||
<li>支持多色图标了,不再受单色限制。</li>
|
|
||||||
<li>通过一些技巧,支持像字体那样,通过 <code>font-size</code>, <code>color</code> 来调整样式。</li>
|
|
||||||
<li>兼容性较差,支持 IE9+,及现代浏览器。</li>
|
|
||||||
<li>浏览器渲染 SVG 的性能一般,还不如 png。</li>
|
|
||||||
</ul>
|
|
||||||
<p>使用步骤如下:</p>
|
|
||||||
<h3 id="-symbol-">第一步:引入项目下面生成的 symbol 代码:</h3>
|
|
||||||
<pre><code class="language-html"><script src="./iconfont.js"></script>
|
|
||||||
</code></pre>
|
|
||||||
<h3 id="-css-">第二步:加入通用 CSS 代码(引入一次就行):</h3>
|
|
||||||
<pre><code class="language-html"><style>
|
|
||||||
.icon {
|
|
||||||
width: 1em;
|
|
||||||
height: 1em;
|
|
||||||
vertical-align: -0.15em;
|
|
||||||
fill: currentColor;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</code></pre>
|
|
||||||
<h3 id="-">第三步:挑选相应图标并获取类名,应用于页面:</h3>
|
|
||||||
<pre><code class="language-html"><svg class="icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon-xxx"></use>
|
|
||||||
</svg>
|
|
||||||
</code></pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
$(document).ready(function () {
|
|
||||||
$('.tab-container .content:first').show()
|
|
||||||
|
|
||||||
$('#tabs li').click(function (e) {
|
|
||||||
var tabContent = $('.tab-container .content')
|
|
||||||
var index = $(this).index()
|
|
||||||
|
|
||||||
if ($(this).hasClass('active')) {
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
$('#tabs li').removeClass('active')
|
|
||||||
$(this).addClass('active')
|
|
||||||
|
|
||||||
tabContent.hide().eq(index).fadeIn()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,127 +0,0 @@
|
|||||||
@font-face {
|
|
||||||
font-family: "iconfont"; /* Project id 5136943 */
|
|
||||||
src: url('iconfont.woff2?t=1780995416168') format('woff2'),
|
|
||||||
url('iconfont.woff?t=1780995416168') format('woff'),
|
|
||||||
url('iconfont.ttf?t=1780995416168') format('truetype');
|
|
||||||
}
|
|
||||||
|
|
||||||
.iconfont {
|
|
||||||
font-family: "iconfont" !important;
|
|
||||||
font-size: 16px;
|
|
||||||
font-style: normal;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-xiazai:before {
|
|
||||||
content: "\e621";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-jiantou_shangxiaqiehuan_o:before {
|
|
||||||
content: "\eb90";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-sousuo:before {
|
|
||||||
content: "\e619";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-wodechuangzuo:before {
|
|
||||||
content: "\e61e";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-shang:before {
|
|
||||||
content: "\e62f";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-xia:before {
|
|
||||||
content: "\e611";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-xiaoxi:before {
|
|
||||||
content: "\e8bd";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-a-17Afasong:before {
|
|
||||||
content: "\e67d";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-a-414126695:before {
|
|
||||||
content: "\e606";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-apikey:before {
|
|
||||||
content: "\e617";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-xiaofeijilu:before {
|
|
||||||
content: "\e654";
|
|
||||||
}
|
|
||||||
|
|
||||||
.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
@ -1,205 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "5136943",
|
|
||||||
"name": "yuanjing",
|
|
||||||
"font_family": "iconfont",
|
|
||||||
"css_prefix_text": "icon-",
|
|
||||||
"description": "",
|
|
||||||
"glyphs": [
|
|
||||||
{
|
|
||||||
"icon_id": "25554760",
|
|
||||||
"name": "下载",
|
|
||||||
"font_class": "xiazai",
|
|
||||||
"unicode": "e621",
|
|
||||||
"unicode_decimal": 58913
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon_id": "5387926",
|
|
||||||
"name": "箭头_上下切换_o",
|
|
||||||
"font_class": "jiantou_shangxiaqiehuan_o",
|
|
||||||
"unicode": "eb90",
|
|
||||||
"unicode_decimal": 60304
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon_id": "9306187",
|
|
||||||
"name": "搜索",
|
|
||||||
"font_class": "sousuo",
|
|
||||||
"unicode": "e619",
|
|
||||||
"unicode_decimal": 58905
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon_id": "5826834",
|
|
||||||
"name": "我的创作",
|
|
||||||
"font_class": "wodechuangzuo",
|
|
||||||
"unicode": "e61e",
|
|
||||||
"unicode_decimal": 58910
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon_id": "1526208",
|
|
||||||
"name": "上",
|
|
||||||
"font_class": "shang",
|
|
||||||
"unicode": "e62f",
|
|
||||||
"unicode_decimal": 58927
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon_id": "14313193",
|
|
||||||
"name": "下",
|
|
||||||
"font_class": "xia",
|
|
||||||
"unicode": "e611",
|
|
||||||
"unicode_decimal": 58897
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon_id": "11372722",
|
|
||||||
"name": "消息",
|
|
||||||
"font_class": "xiaoxi",
|
|
||||||
"unicode": "e8bd",
|
|
||||||
"unicode_decimal": 59581
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon_id": "29522659",
|
|
||||||
"name": "17A发送",
|
|
||||||
"font_class": "a-17Afasong",
|
|
||||||
"unicode": "e67d",
|
|
||||||
"unicode_decimal": 59005
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon_id": "47386728",
|
|
||||||
"name": "414126695",
|
|
||||||
"font_class": "a-414126695",
|
|
||||||
"unicode": "e606",
|
|
||||||
"unicode_decimal": 58886
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon_id": "44608574",
|
|
||||||
"name": "apikey",
|
|
||||||
"font_class": "apikey",
|
|
||||||
"unicode": "e617",
|
|
||||||
"unicode_decimal": 58903
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon_id": "1020316",
|
|
||||||
"name": "消费记录",
|
|
||||||
"font_class": "xiaofeijilu",
|
|
||||||
"unicode": "e654",
|
|
||||||
"unicode_decimal": 58964
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"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.
|
Before Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB |
@ -1,262 +0,0 @@
|
|||||||
.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);
|
|
||||||
}
|
|
||||||
@ -1,393 +0,0 @@
|
|||||||
// 头部容器
|
|
||||||
.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, 12, 18, 0.74);
|
|
||||||
backdrop-filter: blur(16px);
|
|
||||||
|
|
||||||
|
|
||||||
@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: 700;
|
|
||||||
color: #fff;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 操作区
|
|
||||||
.header-actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 14px;
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==============================================
|
|
||||||
// 【一体化】余额 + 充值按钮(核心修改)
|
|
||||||
// ==============================================
|
|
||||||
.balance-recharge-combo {
|
|
||||||
height: 52px;
|
|
||||||
padding: 0 10px 0 12px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #fff;
|
|
||||||
background: linear-gradient(180deg, rgba(31, 35, 46, 0.95), rgba(18, 21, 30, 0.96));
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
||||||
border-radius: 18px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
box-shadow: 0 10px 26px rgba(0, 0, 0, 0.18);
|
|
||||||
|
|
||||||
.balance-main {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.balance-icon-wrap {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
border-radius: 12px;
|
|
||||||
background: linear-gradient(135deg, rgba(139, 124, 255, 0.22), rgba(246, 184, 106, 0.18));
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.iconfont {
|
|
||||||
font-size: 16px;
|
|
||||||
color: #f6b86a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.balance-copy {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.balance-label {
|
|
||||||
font-size: 11px;
|
|
||||||
color: rgba(255, 255, 255, 0.48);
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.balance-text {
|
|
||||||
font-weight: 700;
|
|
||||||
color: #fff;
|
|
||||||
line-height: 1.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.balance-unit {
|
|
||||||
margin-left: 3px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
// color: rgba(255, 255, 255, 0.72);
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.combo-divider {
|
|
||||||
width: 1px;
|
|
||||||
height: 24px;
|
|
||||||
background: rgba(255, 255, 255, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.recharge-text {
|
|
||||||
min-width: 72px;
|
|
||||||
height: 36px;
|
|
||||||
padding: 0 14px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #fff;
|
|
||||||
font-weight: 600;
|
|
||||||
background: linear-gradient(135deg, #6e5cff, #8b7cff);
|
|
||||||
box-shadow: 0 10px 24px rgba(110, 92, 255, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
transform: translateY(-1px);
|
|
||||||
border-color: rgba(139, 124, 255, 0.26);
|
|
||||||
box-shadow: 0 14px 30px rgba(0, 0, 0, 0.24);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
height: 42px;
|
|
||||||
padding: 0 8px 0 10px;
|
|
||||||
font-size: 13px;
|
|
||||||
gap: 6px;
|
|
||||||
|
|
||||||
.balance-icon-wrap {
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.balance-label {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.combo-divider {
|
|
||||||
height: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recharge-text {
|
|
||||||
min-width: 60px;
|
|
||||||
height: 30px;
|
|
||||||
padding: 0 10px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 登录按钮
|
|
||||||
.btn {
|
|
||||||
min-width: 120px;
|
|
||||||
height: 48px;
|
|
||||||
font-size: 16px;
|
|
||||||
background-image: linear-gradient(135deg, #6e5cff, #8b7cff);
|
|
||||||
border-radius: 16px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #fff;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
box-shadow: 0 12px 28px rgba(110, 92, 255, 0.3);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-image: linear-gradient(135deg, #7d6dff, #978bff);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 16px 36px rgba(110, 92, 255, 0.34);
|
|
||||||
}
|
|
||||||
|
|
||||||
@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: #8b7cff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-trigger {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 0 16px 0 12px;
|
|
||||||
height: 52px;
|
|
||||||
background: rgba(255, 255, 255, 0.04);
|
|
||||||
border-radius: 18px;
|
|
||||||
color: #fff;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
||||||
box-shadow: 0 10px 26px rgba(0, 0, 0, 0.14);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.07);
|
|
||||||
border-color: rgba(139, 124, 255, 0.2);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-avatar {
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
border-radius: 11px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
// font-size: 50px;
|
|
||||||
.iconfont{
|
|
||||||
font-size: 28px;
|
|
||||||
}
|
|
||||||
// font-weight: 700;
|
|
||||||
// color: #fff;
|
|
||||||
// background: linear-gradient(135deg, rgba(139, 124, 255, 0.9), rgba(246, 184, 106, 0.72));
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-name {
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 600;
|
|
||||||
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.56);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
height: 36px;
|
|
||||||
padding: 0 12px;
|
|
||||||
|
|
||||||
.user-avatar {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-name {
|
|
||||||
max-width: 80px;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 用户下拉菜单
|
|
||||||
.user-dropdown {
|
|
||||||
position: absolute;
|
|
||||||
top: 100%;
|
|
||||||
right: 0;
|
|
||||||
margin-top: 8px;
|
|
||||||
width: 220px;
|
|
||||||
background: rgba(16, 19, 28, 0.98);
|
|
||||||
backdrop-filter: blur(14px);
|
|
||||||
border-radius: 16px;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
||||||
box-shadow: 0 18px 40px 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(16, 19, 28, 0.98);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 下拉菜单项
|
|
||||||
.dropdown-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 12px 16px;
|
|
||||||
color: #fff;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.user-id {
|
|
||||||
justify-content: space-between;
|
|
||||||
font-size: 14px;
|
|
||||||
|
|
||||||
.item-label {
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-value {
|
|
||||||
color: #8b7cff;
|
|
||||||
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.06);
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
@ -1,148 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
||||||
:deep(.el-upload--picture-card) {
|
|
||||||
width: 140px !important;
|
|
||||||
height: 140px !important;
|
|
||||||
}
|
|
||||||
@ -1,181 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
:deep(.el-upload--picture-card) {
|
|
||||||
width: 140px !important;
|
|
||||||
height: 140px !important;
|
|
||||||
}
|
|
||||||
@ -1,151 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
||||||
@ -1,183 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,547 +0,0 @@
|
|||||||
@primary: #8b7cff;
|
|
||||||
@primary-dark: #6e5cff;
|
|
||||||
@accent: #f6b86a;
|
|
||||||
@bg-dark: rgba(10, 12, 18, 0.94);
|
|
||||||
@glass-bg: linear-gradient(180deg, rgba(24, 28, 39, 0.96), rgba(15, 18, 27, 0.98));
|
|
||||||
@panel-bg: rgba(255, 255, 255, 0.04);
|
|
||||||
@panel-hover: rgba(255, 255, 255, 0.08);
|
|
||||||
@border-color: rgba(255, 255, 255, 0.08);
|
|
||||||
@text-light: #f7f8fb;
|
|
||||||
@text-secondary: rgba(255, 255, 255, 0.64);
|
|
||||||
@text-muted: rgba(255, 255, 255, 0.44);
|
|
||||||
|
|
||||||
// 通用动画
|
|
||||||
.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:
|
|
||||||
radial-gradient(circle at top, rgba(139, 124, 255, 0.2), transparent 35%),
|
|
||||||
rgba(0, 0, 0, 0.68);
|
|
||||||
backdrop-filter: blur(14px);
|
|
||||||
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(20px);
|
|
||||||
border-radius: 28px;
|
|
||||||
padding: 30px 30px 24px;
|
|
||||||
transition: transform 0.2s ease;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
||||||
box-shadow: 0 24px 70px rgba(0, 0, 0, 0.45);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-start;
|
|
||||||
margin-bottom: 28px;
|
|
||||||
|
|
||||||
.header-copy {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: 28px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: @text-light;
|
|
||||||
margin: 0;
|
|
||||||
letter-spacing: 0.02em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-desc {
|
|
||||||
margin: 0;
|
|
||||||
max-width: 360px;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.65;
|
|
||||||
color: @text-secondary;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-btn {
|
|
||||||
background: rgba(255, 255, 255, 0.04);
|
|
||||||
border: 1px solid @border-color;
|
|
||||||
font-size: 18px;
|
|
||||||
cursor: pointer;
|
|
||||||
color: @text-secondary;
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
border-radius: 12px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
transition: all 0.2s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
color: #fff;
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.provider-section {
|
|
||||||
margin-bottom: 24px;
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
font-size: 13px;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
color: @text-secondary;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.provider-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.provider-btn {
|
|
||||||
background: @panel-bg;
|
|
||||||
border: 1px solid @border-color;
|
|
||||||
border-radius: 18px;
|
|
||||||
padding: 16px 18px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: @text-light;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.25s ease;
|
|
||||||
backdrop-filter: blur(8px);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-start;
|
|
||||||
gap: 12px;
|
|
||||||
|
|
||||||
.provider-name {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.provider-tag {
|
|
||||||
margin-left: auto;
|
|
||||||
padding: 4px 10px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: rgba(246, 184, 106, 0.14);
|
|
||||||
color: @accent;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
border-color: rgba(139, 124, 255, 0.4);
|
|
||||||
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.2);
|
|
||||||
background: @panel-hover;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background: linear-gradient(135deg, rgba(139, 124, 255, 0.18), rgba(246, 184, 106, 0.08));
|
|
||||||
border-color: rgba(139, 124, 255, 0.45);
|
|
||||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.iconfont {
|
|
||||||
font-size: 22px;
|
|
||||||
color: #165dff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.amount-section {
|
|
||||||
margin-bottom: 32px;
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
font-size: 13px;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
color: @text-secondary;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.amount-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: 12px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
|
|
||||||
@media (min-width: 480px) {
|
|
||||||
grid-template-columns: repeat(5, 1fr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.amount-btn {
|
|
||||||
min-height: 76px;
|
|
||||||
background: @panel-bg;
|
|
||||||
border: 1px solid @border-color;
|
|
||||||
border-radius: 18px;
|
|
||||||
padding: 12px 0;
|
|
||||||
font-size: 22px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: @text-light;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.25s ease;
|
|
||||||
backdrop-filter: blur(8px);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 4px;
|
|
||||||
|
|
||||||
.currency {
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 400;
|
|
||||||
color: @text-secondary;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
border-color: rgba(139, 124, 255, 0.4);
|
|
||||||
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.18);
|
|
||||||
background: @panel-hover;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background: linear-gradient(180deg, rgba(139, 124, 255, 0.24), rgba(139, 124, 255, 0.12));
|
|
||||||
border-color: rgba(139, 124, 255, 0.48);
|
|
||||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-btn {
|
|
||||||
font-size: 16px;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 6px;
|
|
||||||
|
|
||||||
.currency {
|
|
||||||
font-size: 18px;
|
|
||||||
color: @accent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected-info {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
gap: 12px;
|
|
||||||
padding-top: 2px;
|
|
||||||
|
|
||||||
.selected-info__item {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 16px 18px;
|
|
||||||
border-radius: 18px;
|
|
||||||
background: rgba(255, 255, 255, 0.04);
|
|
||||||
border: 1px solid @border-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected-info__item--highlight {
|
|
||||||
background: linear-gradient(135deg, rgba(139, 124, 255, 0.16), rgba(246, 184, 106, 0.08));
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected-info__label {
|
|
||||||
font-size: 12px;
|
|
||||||
letter-spacing: 0.06em;
|
|
||||||
color: @text-muted;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected-info__value {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: @text-light;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected-amount {
|
|
||||||
font-size: 28px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirm-btn {
|
|
||||||
width: 100%;
|
|
||||||
background: linear-gradient(135deg, @primary-dark, @primary);
|
|
||||||
border: none;
|
|
||||||
border-radius: 18px;
|
|
||||||
padding: 16px 0;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #fff;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 12px;
|
|
||||||
transition: all 0.3s;
|
|
||||||
box-shadow: 0 16px 36px rgba(110, 92, 255, 0.32);
|
|
||||||
letter-spacing: 0.06em;
|
|
||||||
|
|
||||||
.btn-icon {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
transition: transform 0.2s;
|
|
||||||
|
|
||||||
&.loading {
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover:not(:disabled) {
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 20px 42px rgba(110, 92, 255, 0.38);
|
|
||||||
filter: brightness(1.06);
|
|
||||||
|
|
||||||
.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.recharge-tips {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
|
||||||
margin-top: 14px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: @text-muted;
|
|
||||||
|
|
||||||
span:last-child {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-modal-mask {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: rgba(0, 0, 0, 0.76);
|
|
||||||
backdrop-filter: blur(16px);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 1100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-dialog {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 600px;
|
|
||||||
background: linear-gradient(180deg, rgba(23, 26, 37, 0.98), rgba(15, 18, 27, 0.99));
|
|
||||||
backdrop-filter: blur(18px);
|
|
||||||
border-radius: 24px;
|
|
||||||
border: 1px solid @border-color;
|
|
||||||
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.45);
|
|
||||||
overflow: hidden;
|
|
||||||
animation: slideUp 0.25s ease;
|
|
||||||
|
|
||||||
.dialog-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin: 20px 30px;
|
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 22px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: @text-light;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-btn {
|
|
||||||
width: 34px;
|
|
||||||
height: 34px;
|
|
||||||
border-radius: 10px;
|
|
||||||
border: 1px solid @border-color;
|
|
||||||
background: rgba(255, 255, 255, 0.04);
|
|
||||||
font-size: 18px;
|
|
||||||
cursor: pointer;
|
|
||||||
color: @text-secondary;
|
|
||||||
transition: all 0.2s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: #fff;
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-body {
|
|
||||||
padding:20px 30px;
|
|
||||||
|
|
||||||
.input-group {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
background: rgba(255, 255, 255, 0.04);
|
|
||||||
border-radius: 18px;
|
|
||||||
border: 1px solid @border-color;
|
|
||||||
transition: all 0.2s;
|
|
||||||
|
|
||||||
&:focus-within {
|
|
||||||
border-color: rgba(139, 124, 255, 0.46);
|
|
||||||
box-shadow: 0 0 0 4px rgba(139, 124, 255, 0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-currency {
|
|
||||||
font-size: 28px;
|
|
||||||
font-weight: 600;
|
|
||||||
padding: 0 16px;
|
|
||||||
color: @accent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-input {
|
|
||||||
flex: 1;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
padding: 16px 16px 16px 0;
|
|
||||||
font-size: 26px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: @text-light;
|
|
||||||
outline: none;
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: rgba(255, 255, 255, 0.28);
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-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: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-footer {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 12px 24px 28px;
|
|
||||||
|
|
||||||
.dialog-btn {
|
|
||||||
flex: 1;
|
|
||||||
padding: 13px 0;
|
|
||||||
border-radius: 16px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
|
|
||||||
&.cancel {
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
border: 1px solid @border-color;
|
|
||||||
color: @text-light;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.confirm {
|
|
||||||
background: linear-gradient(135deg, @primary-dark, @primary);
|
|
||||||
color: #fff;
|
|
||||||
box-shadow: 0 12px 28px rgba(110, 92, 255, 0.28);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
transform: translateY(-1px);
|
|
||||||
filter: brightness(1.05);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slideUp {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(30px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
.recharge-container {
|
|
||||||
padding: 24px 20px 20px;
|
|
||||||
border-radius: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header {
|
|
||||||
.header-desc {
|
|
||||||
max-width: none;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.amount-grid {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected-info {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recharge-tips {
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
span:last-child {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,258 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
||||||
@ -1,355 +0,0 @@
|
|||||||
|
|
||||||
.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, none);
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 状态徽章样式 */
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,206 +0,0 @@
|
|||||||
: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;
|
|
||||||
resize: vertical;
|
|
||||||
outline: none !important;
|
|
||||||
}
|
|
||||||
:deep(.el-upload--picture-card) {
|
|
||||||
width: 140px !important;
|
|
||||||
height: 140px !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;
|
|
||||||
padding: 14px 16px !important;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: #fff !important;
|
|
||||||
resize: none;
|
|
||||||
outline: none !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: rgba(255, 255, 255, 0.36) !important;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
:deep(.el-textarea) .el-input__count {
|
|
||||||
background: transparent;
|
|
||||||
color: rgba(255, 255, 255, 0.5);
|
|
||||||
}
|
|
||||||
.card {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-color: #1a1c20;
|
|
||||||
border: none;
|
|
||||||
flex: 1;
|
|
||||||
padding: 16px;
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
.card :deep(.el-card__body) {
|
|
||||||
padding: 12px 16px;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
.reference-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
height: 100%;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.upload-section {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 16px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.upload-item {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 200px;
|
|
||||||
}
|
|
||||||
.upload-item .upload-label {
|
|
||||||
font-size: 14px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
color: rgba(255, 255, 255, 0.9);
|
|
||||||
}
|
|
||||||
.upload-item :deep(.el-upload--picture-card) {
|
|
||||||
background-color: rgba(0, 0, 0, 0.32);
|
|
||||||
border: 1px dashed rgba(255, 255, 255, 0.3);
|
|
||||||
border-radius: 10px;
|
|
||||||
width: 100%;
|
|
||||||
height: 100px;
|
|
||||||
line-height: 100px;
|
|
||||||
}
|
|
||||||
.upload-item :deep(.el-upload--picture-card):hover {
|
|
||||||
border-color: #409eff;
|
|
||||||
}
|
|
||||||
.upload-item :deep(.el-upload-list--picture-card) .el-upload-list__item {
|
|
||||||
width: 100%;
|
|
||||||
height: 100px;
|
|
||||||
background-color: rgba(0, 0, 0, 0.32);
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
.required-star {
|
|
||||||
color: #ff4d4f;
|
|
||||||
margin-left: 4px;
|
|
||||||
}
|
|
||||||
.input {
|
|
||||||
width: 100%;
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
.btmcard {
|
|
||||||
width: 100%;
|
|
||||||
background-color: #1a1c20;
|
|
||||||
border: none;
|
|
||||||
margin-top: 16px;
|
|
||||||
padding: 16px;
|
|
||||||
border-radius: 12px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
.select-content :deep(.el-input-number) {
|
|
||||||
width: 100%;
|
|
||||||
background-color: rgba(0, 0, 0, 0.32);
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
.select-content :deep(.el-input-number) .el-input-number__decrease,
|
|
||||||
.select-content :deep(.el-input-number) .el-input-number__increase {
|
|
||||||
background-color: transparent;
|
|
||||||
color: rgba(255, 255, 255, 0.6);
|
|
||||||
}
|
|
||||||
.select-content :deep(.el-input-number) .el-input-number__decrease:hover,
|
|
||||||
.select-content :deep(.el-input-number) .el-input-number__increase:hover {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.select-content :deep(.el-input-number) .el-input__inner {
|
|
||||||
background-color: transparent;
|
|
||||||
color: #fff;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
.select-content :deep(.el-input) .el-input__inner {
|
|
||||||
background-color: rgba(0, 0, 0, 0.32);
|
|
||||||
border: none;
|
|
||||||
border-radius: 10px;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.select-content :deep(.el-input) .el-input__inner::placeholder {
|
|
||||||
color: rgba(255, 255, 255, 0.5);
|
|
||||||
}
|
|
||||||
.select-content :deep(.el-textarea) .el-textarea__inner {
|
|
||||||
border: none !important;
|
|
||||||
background-color: rgba(0, 0, 0, 0.32) !important;
|
|
||||||
border-radius: 10px !important;
|
|
||||||
color: #fff !important;
|
|
||||||
}
|
|
||||||
.audio-wrapper {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.upload-btn {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.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__inner {
|
|
||||||
background-color: #35383F !important;
|
|
||||||
border: none !important;
|
|
||||||
color: #fff !important;
|
|
||||||
}
|
|
||||||
.el-select .el-input__suffix-inner {
|
|
||||||
color: #fff !important;
|
|
||||||
}
|
|
||||||
:deep(.el-upload-list__item) {
|
|
||||||
transition: none !important;
|
|
||||||
}
|
|
||||||
@ -1,253 +0,0 @@
|
|||||||
: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;
|
|
||||||
resize: vertical;
|
|
||||||
outline: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-upload--picture-card) {
|
|
||||||
width: 140px !important;
|
|
||||||
height: 140px !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;
|
|
||||||
padding: 14px 16px !important;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: #fff !important;
|
|
||||||
resize: none;
|
|
||||||
outline: none !important;
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
border: none !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
outline: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: rgba(255, 255, 255, 0.36) !important;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-input__count {
|
|
||||||
background: transparent;
|
|
||||||
color: rgba(255, 255, 255, 0.5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.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: 16px;
|
|
||||||
height: 100%;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-section {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 16px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-item {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 200px;
|
|
||||||
|
|
||||||
.upload-label {
|
|
||||||
font-size: 14px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
color: rgba(255, 255, 255, 0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-upload--picture-card) {
|
|
||||||
background-color: rgba(0, 0, 0, 0.32);
|
|
||||||
border: 1px dashed rgba(255, 255, 255, 0.3);
|
|
||||||
border-radius: 10px;
|
|
||||||
width: 100%;
|
|
||||||
height: 100px;
|
|
||||||
line-height: 100px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: #409eff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-upload-list--picture-card) {
|
|
||||||
.el-upload-list__item {
|
|
||||||
width: 100%;
|
|
||||||
height: 100px;
|
|
||||||
background-color: rgba(0, 0, 0, 0.32);
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.required-star {
|
|
||||||
color: #ff4d4f;
|
|
||||||
margin-left: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input {
|
|
||||||
width: 100%;
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btmcard {
|
|
||||||
width: 100%;
|
|
||||||
background-color: #1a1c20;
|
|
||||||
border: none;
|
|
||||||
margin-top: 16px;
|
|
||||||
padding: 16px;
|
|
||||||
border-radius: 12px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
: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;
|
|
||||||
|
|
||||||
:deep(.el-input-number) {
|
|
||||||
width: 100%;
|
|
||||||
background-color: rgba(0, 0, 0, 0.32);
|
|
||||||
border-radius: 10px;
|
|
||||||
|
|
||||||
.el-input-number__decrease,
|
|
||||||
.el-input-number__increase {
|
|
||||||
background-color: transparent;
|
|
||||||
color: rgba(255, 255, 255, 0.6);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-input__inner {
|
|
||||||
background-color: transparent;
|
|
||||||
color: #fff;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-input) {
|
|
||||||
.el-input__inner {
|
|
||||||
background-color: rgba(0, 0, 0, 0.32);
|
|
||||||
border: none;
|
|
||||||
border-radius: 10px;
|
|
||||||
color: #fff;
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: rgba(255, 255, 255, 0.5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-textarea) {
|
|
||||||
.el-textarea__inner {
|
|
||||||
border: none !important;
|
|
||||||
background-color: rgba(0, 0, 0, 0.32) !important;
|
|
||||||
border-radius: 10px !important;
|
|
||||||
color: #fff !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.audio-wrapper {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-btn {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-upload-list__item) {
|
|
||||||
transition: none !important;
|
|
||||||
}
|
|
||||||
@ -1,429 +0,0 @@
|
|||||||
.reference-to-video {
|
|
||||||
height: auto;
|
|
||||||
min-height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px!important;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
: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%;
|
|
||||||
background-color: #1a1c20;
|
|
||||||
border: none;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
min-height: fit-content;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
padding: 16px;
|
|
||||||
border-radius: 12px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: visible;
|
|
||||||
|
|
||||||
:deep(.el-card__body) {
|
|
||||||
height: auto;
|
|
||||||
padding: 12px 16px;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.reference-container {
|
|
||||||
padding: 12px;
|
|
||||||
flex: none;
|
|
||||||
gap: 20px;
|
|
||||||
color: #fff;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reference-header {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 18px;
|
|
||||||
padding: 8px 0 4px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reference-header__copy {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reference-header__title {
|
|
||||||
font-size: 22px;
|
|
||||||
line-height: 1.35;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reference-header__desc {
|
|
||||||
font-size: 15px;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: rgba(255, 255, 255, 0.72);
|
|
||||||
}
|
|
||||||
|
|
||||||
.reference-header__actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hidden-upload-input {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reference-action-btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
height: 46px;
|
|
||||||
padding: 0 22px;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
border-radius: 14px;
|
|
||||||
background: rgba(255, 255, 255, 0.04);
|
|
||||||
color: #fff;
|
|
||||||
font-size: 16px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.08);
|
|
||||||
border-color: rgba(255, 255, 255, 0.18);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
opacity: 0.6;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.reference-action-btn__icon {
|
|
||||||
font-size: 26px;
|
|
||||||
line-height: 1;
|
|
||||||
font-weight: 300;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reference-assets {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reference-asset-card {
|
|
||||||
width: 180px;
|
|
||||||
overflow: hidden;
|
|
||||||
border-radius: 12px;
|
|
||||||
background: rgba(255, 255, 255, 0.04);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.reference-asset-card__image {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
height: 110px;
|
|
||||||
object-fit: cover;
|
|
||||||
background: rgba(0, 0, 0, 0.24);
|
|
||||||
}
|
|
||||||
|
|
||||||
.reference-asset-card--audio {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reference-asset-card__audio {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
min-height: 110px;
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reference-asset-card__audio-icon {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 44px;
|
|
||||||
height: 44px;
|
|
||||||
border-radius: 12px;
|
|
||||||
background: rgba(255, 255, 255, 0.12);
|
|
||||||
color: #fff;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reference-asset-card__audio-info {
|
|
||||||
min-width: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reference-asset-card__audio-title {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reference-asset-card__audio-name {
|
|
||||||
max-width: 100px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: rgba(255, 255, 255, 0.64);
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-preview-media {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
max-height: 70vh;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-preview-audio {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reference-asset-card__meta {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 10px 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reference-asset-card__label {
|
|
||||||
font-size: 13px;
|
|
||||||
color: rgba(255, 255, 255, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.reference-asset-card__actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input {
|
|
||||||
flex: 1;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
margin-top: 10px;
|
|
||||||
:deep(.el-textarea) {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-textarea__inner) {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 150px;
|
|
||||||
}
|
|
||||||
|
|
||||||
: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%;
|
|
||||||
background-color: #1a1c20;
|
|
||||||
border: none;
|
|
||||||
flex-shrink: 0;
|
|
||||||
padding: 16px;
|
|
||||||
border-radius: 12px;
|
|
||||||
overflow: visible;
|
|
||||||
|
|
||||||
:deep(.el-card__body) {
|
|
||||||
padding: 12px 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.select {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
padding: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.required-star {
|
|
||||||
margin-left: 4px;
|
|
||||||
color: #ff8f8f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select-content {
|
|
||||||
width: 50%;
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
: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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.uploaded-action {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: rgba(255, 255, 255, 0.12);
|
|
||||||
color: #fff;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.2s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: rgba(255, 255, 255, 0.24);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-icon {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,222 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
||||||
:deep(.el-upload--picture-card) {
|
|
||||||
width: 140px !important;
|
|
||||||
height: 140px !important;
|
|
||||||
}
|
|
||||||
.upload-section {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 16px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.upload-item {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 200px;
|
|
||||||
}
|
|
||||||
.upload-item .upload-label {
|
|
||||||
font-size: 14px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
color: rgba(255, 255, 255, 0.9);
|
|
||||||
}
|
|
||||||
.upload-item :deep(.el-upload--picture-card) {
|
|
||||||
background-color: rgba(0, 0, 0, 0.32);
|
|
||||||
border: 1px dashed rgba(255, 255, 255, 0.3);
|
|
||||||
border-radius: 10px;
|
|
||||||
width: 100%;
|
|
||||||
height: 100px;
|
|
||||||
line-height: 100px;
|
|
||||||
}
|
|
||||||
.upload-item :deep(.el-upload--picture-card):hover {
|
|
||||||
border-color: #409eff;
|
|
||||||
}
|
|
||||||
.upload-item :deep(.el-upload-list--picture-card) .el-upload-list__item {
|
|
||||||
width: 100%;
|
|
||||||
height: 100px;
|
|
||||||
background-color: rgba(0, 0, 0, 0.32);
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
@ -1,259 +0,0 @@
|
|||||||
|
|
||||||
.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;
|
|
||||||
padding: 12px;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
padding: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
:deep(.el-upload--picture-card) {
|
|
||||||
width: 140px !important;
|
|
||||||
height: 140px !important;
|
|
||||||
}
|
|
||||||
.upload-section {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 16px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-item {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 200px;
|
|
||||||
|
|
||||||
.upload-label {
|
|
||||||
font-size: 14px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
color: rgba(255, 255, 255, 0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-upload--picture-card) {
|
|
||||||
background-color: rgba(0, 0, 0, 0.32);
|
|
||||||
border: 1px dashed rgba(255, 255, 255, 0.3);
|
|
||||||
border-radius: 10px;
|
|
||||||
width: 100%;
|
|
||||||
height: 100px;
|
|
||||||
line-height: 100px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: #409eff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-upload-list--picture-card) {
|
|
||||||
.el-upload-list__item {
|
|
||||||
width: 100%;
|
|
||||||
height: 100px;
|
|
||||||
background-color: rgba(0, 0, 0, 0.32);
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,307 +0,0 @@
|
|||||||
.apikey-page {
|
|
||||||
height: 100%;
|
|
||||||
padding-bottom: 40px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
.apikey-card {
|
|
||||||
height: 100%;
|
|
||||||
background: #1e2024;
|
|
||||||
border: 1px solid #2a2d32;
|
|
||||||
border-radius: 16px;
|
|
||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
.apikey-card :deep(.el-card__body) {
|
|
||||||
padding: 28px 32px;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.page-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 24px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
.page-header .header-main .title-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.page-header .header-main .title-row .iconfont {
|
|
||||||
font-size: 26px;
|
|
||||||
color: #00c4b3;
|
|
||||||
}
|
|
||||||
.page-header .header-main .header-desc {
|
|
||||||
margin: 10px 0 0;
|
|
||||||
color: rgba(255, 255, 255, 0.65);
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
.page-header .header-actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
.page-header .header-actions .refresh-btn {
|
|
||||||
background: #25282e;
|
|
||||||
border-color: #3a3e45;
|
|
||||||
color: rgba(255, 255, 255, 0.88);
|
|
||||||
}
|
|
||||||
.guide-card {
|
|
||||||
margin-bottom: 24px;
|
|
||||||
padding: 24px 28px;
|
|
||||||
border-radius: 14px;
|
|
||||||
background: #25282e;
|
|
||||||
border: 1px solid #32363d;
|
|
||||||
}
|
|
||||||
.guide-section + .guide-section {
|
|
||||||
margin-top: 24px;
|
|
||||||
}
|
|
||||||
.guide-title {
|
|
||||||
margin-bottom: 14px;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
.guide-list {
|
|
||||||
margin: 0;
|
|
||||||
padding-left: 24px;
|
|
||||||
color: rgba(255, 255, 255, 0.86);
|
|
||||||
line-height: 1.9;
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
.guide-list li + li {
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
.empty-state {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.empty-state {
|
|
||||||
flex-direction: column;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.empty-state img {
|
|
||||||
width: 220px;
|
|
||||||
height: 220px;
|
|
||||||
object-fit: contain;
|
|
||||||
opacity: 0.55;
|
|
||||||
}
|
|
||||||
.empty-title {
|
|
||||||
font-size: 20px;
|
|
||||||
color: #fff;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.login-desc,
|
|
||||||
.empty-desc {
|
|
||||||
font-size: 14px;
|
|
||||||
color: rgba(255, 255, 255, 0.6);
|
|
||||||
line-height: 1.7;
|
|
||||||
}
|
|
||||||
.stats-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
||||||
gap: 16px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
.stat-card {
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 14px;
|
|
||||||
background: linear-gradient(135deg, rgba(44, 255, 248, 0.08), rgba(44, 104, 255, 0.08));
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
||||||
}
|
|
||||||
.stat-card .stat-label {
|
|
||||||
font-size: 13px;
|
|
||||||
color: rgba(255, 255, 255, 0.6);
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
.stat-card .stat-value {
|
|
||||||
font-size: 28px;
|
|
||||||
color: #fff;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
.stat-card .stat-value.success {
|
|
||||||
color: #3fe0b5;
|
|
||||||
}
|
|
||||||
.stat-card .stat-value.warning {
|
|
||||||
color: #f5b04c;
|
|
||||||
}
|
|
||||||
.toolbar {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 16px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
.toolbar .toolbar-left {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
.toolbar .toolbar-right {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.toolbar .toolbar-tip {
|
|
||||||
font-size: 13px;
|
|
||||||
color: rgba(255, 255, 255, 0.45);
|
|
||||||
}
|
|
||||||
.search-input {
|
|
||||||
width: 320px;
|
|
||||||
}
|
|
||||||
.table-wrapper {
|
|
||||||
flex: 1;
|
|
||||||
overflow: auto;
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
.apikey-table {
|
|
||||||
width: 100%;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
.apikey-table :deep(.el-table__header-wrapper) .el-table__header th {
|
|
||||||
background-color: #1e2024;
|
|
||||||
color: rgba(255, 255, 255, 0.75);
|
|
||||||
border-bottom: 1px solid #2a2d32;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.apikey-table :deep(.el-table__body-wrapper) .el-table__body tr {
|
|
||||||
background-color: #1e2024;
|
|
||||||
transition: background 0.2s;
|
|
||||||
}
|
|
||||||
.apikey-table :deep(.el-table__body-wrapper) .el-table__body tr:hover td {
|
|
||||||
background-color: #25282e !important;
|
|
||||||
}
|
|
||||||
.apikey-table :deep(.el-table__body-wrapper) .el-table__body tr td {
|
|
||||||
border-bottom: 1px solid #2a2d32;
|
|
||||||
color: rgba(255, 255, 255, 0.88);
|
|
||||||
}
|
|
||||||
.apikey-table :deep(.el-table__row--striped) td {
|
|
||||||
background-color: #1a1c20 !important;
|
|
||||||
}
|
|
||||||
.apikey-table .key-mask {
|
|
||||||
font-family: Consolas, 'Courier New', monospace;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #87f5eb;
|
|
||||||
background: #14161a;
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 6px;
|
|
||||||
letter-spacing: 0.3px;
|
|
||||||
}
|
|
||||||
.view-key-content .view-key-name,
|
|
||||||
.view-key-content .view-key-value {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
.view-key-content .view-key-name .label,
|
|
||||||
.view-key-content .view-key-value .label {
|
|
||||||
font-weight: 600;
|
|
||||||
color: rgba(255, 255, 255, 0.75);
|
|
||||||
display: inline-block;
|
|
||||||
width: 90px;
|
|
||||||
}
|
|
||||||
.view-key-content .view-key-name .value,
|
|
||||||
.view-key-content .view-key-value .value {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.view-key-content .key-box {
|
|
||||||
margin-top: 8px;
|
|
||||||
padding: 14px;
|
|
||||||
background: #14161a;
|
|
||||||
border-radius: 12px;
|
|
||||||
border: 1px solid #2a2d32;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 16px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
.view-key-content .key-box code {
|
|
||||||
font-family: Consolas, 'Courier New', monospace;
|
|
||||||
color: #87f5eb;
|
|
||||||
font-size: 14px;
|
|
||||||
word-break: break-all;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
.view-key-content .view-key-tip {
|
|
||||||
margin-top: 16px;
|
|
||||||
padding: 10px 12px;
|
|
||||||
background: rgba(245, 176, 76, 0.12);
|
|
||||||
border-radius: 8px;
|
|
||||||
color: #f5b04c;
|
|
||||||
font-size: 13px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
.view-key-content .view-key-tip i {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
.dialog-footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
.full-width {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
:deep(.el-input__wrapper),
|
|
||||||
:deep(.el-textarea__inner),
|
|
||||||
:deep(.el-select__wrapper) {
|
|
||||||
background-color: #25282e !important;
|
|
||||||
border: 1px solid #3a3e45 !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
:deep(.el-input__inner),
|
|
||||||
:deep(.el-textarea__inner) {
|
|
||||||
color: rgba(255, 255, 255, 0.92) !important;
|
|
||||||
}
|
|
||||||
:deep(.el-radio) {
|
|
||||||
margin-right: 20px;
|
|
||||||
color: rgba(255, 255, 255, 0.85);
|
|
||||||
}
|
|
||||||
:deep(.el-dialog) {
|
|
||||||
background: #1e2024;
|
|
||||||
border: 1px solid #2a2d32;
|
|
||||||
border-radius: 16px;
|
|
||||||
}
|
|
||||||
:deep(.el-dialog__title),
|
|
||||||
:deep(.el-form-item__label) {
|
|
||||||
color: #fff !important;
|
|
||||||
}
|
|
||||||
:deep(.el-button--primary.is-link) {
|
|
||||||
color: #3fe0b5 !important;
|
|
||||||
}
|
|
||||||
:deep(.el-button--danger.is-link) {
|
|
||||||
color: #f56c6c !important;
|
|
||||||
}
|
|
||||||
:deep(.el-table__inner-wrapper) {
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
@media (max-width: 1200px) {
|
|
||||||
.stats-grid {
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.apikey-card :deep(.el-card__body) {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
.stats-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
.toolbar {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
.search-input {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.table-wrapper {
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,415 +0,0 @@
|
|||||||
.apikey-page {
|
|
||||||
height: 100%;
|
|
||||||
padding-bottom: 40px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.apikey-card {
|
|
||||||
height: 100%;
|
|
||||||
background: #1e2024;
|
|
||||||
border: 1px solid #2a2d32;
|
|
||||||
border-radius: 16px;
|
|
||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
|
||||||
|
|
||||||
:deep(.el-card__body) {
|
|
||||||
padding: 28px 32px;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 24px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
|
|
||||||
.header-main {
|
|
||||||
.title-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #fff;
|
|
||||||
|
|
||||||
.iconfont {
|
|
||||||
font-size: 26px;
|
|
||||||
color: #00c4b3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-desc {
|
|
||||||
margin: 10px 0 0;
|
|
||||||
color: rgba(255, 255, 255, 0.65);
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
|
|
||||||
.refresh-btn {
|
|
||||||
background: #25282e;
|
|
||||||
border-color: #3a3e45;
|
|
||||||
color: rgba(255, 255, 255, 0.88);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.guide-card {
|
|
||||||
margin-bottom: 24px;
|
|
||||||
padding: 24px 28px;
|
|
||||||
border-radius: 14px;
|
|
||||||
background: #25282e;
|
|
||||||
border: 1px solid #32363d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.guide-section+.guide-section {
|
|
||||||
margin-top: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.guide-title {
|
|
||||||
margin-bottom: 14px;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.guide-list {
|
|
||||||
margin: 0;
|
|
||||||
padding-left: 24px;
|
|
||||||
color: rgba(255, 255, 255, 0.86);
|
|
||||||
line-height: 1.9;
|
|
||||||
font-size: 15px;
|
|
||||||
|
|
||||||
li+li {
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state {
|
|
||||||
flex-direction: column;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 220px;
|
|
||||||
height: 220px;
|
|
||||||
object-fit: contain;
|
|
||||||
opacity: 0.55;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-title {
|
|
||||||
font-size: 20px;
|
|
||||||
color: #fff;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-desc,
|
|
||||||
.empty-desc {
|
|
||||||
font-size: 14px;
|
|
||||||
color: rgba(255, 255, 255, 0.6);
|
|
||||||
line-height: 1.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
||||||
gap: 16px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-card {
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 14px;
|
|
||||||
background: linear-gradient(135deg, rgba(44, 255, 248, 0.08), rgba(44, 104, 255, 0.08));
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
||||||
|
|
||||||
.stat-label {
|
|
||||||
font-size: 13px;
|
|
||||||
color: rgba(255, 255, 255, 0.6);
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-value {
|
|
||||||
font-size: 28px;
|
|
||||||
color: #fff;
|
|
||||||
font-weight: 700;
|
|
||||||
|
|
||||||
&.success {
|
|
||||||
color: #3fe0b5;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.warning {
|
|
||||||
color: #f5b04c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 16px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
|
|
||||||
.toolbar-left {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar-right {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar-tip {
|
|
||||||
font-size: 13px;
|
|
||||||
color: rgba(255, 255, 255, 0.45);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input {
|
|
||||||
width: 320px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-wrapper {
|
|
||||||
flex: 1;
|
|
||||||
overflow: auto;
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.apikey-table {
|
|
||||||
width: 100%;
|
|
||||||
background: transparent;
|
|
||||||
|
|
||||||
:deep(.el-table) {
|
|
||||||
--el-table-bg-color: #1e2024;
|
|
||||||
--el-table-tr-bg-color: #1e2024;
|
|
||||||
--el-table-row-hover-bg-color: #25282e;
|
|
||||||
--el-table-current-row-bg-color: #25282e;
|
|
||||||
--el-table-header-bg-color: #1e2024;
|
|
||||||
--el-table-fixed-box-shadow: none;
|
|
||||||
--el-table-border-color: #2a2d32;
|
|
||||||
--el-table-text-color: rgba(255, 255, 255, 0.88);
|
|
||||||
--el-table-header-text-color: rgba(255, 255, 255, 0.75);
|
|
||||||
--el-bg-color: #1e2024;
|
|
||||||
--el-fill-color-lighter: #1a1c20;
|
|
||||||
background: transparent;
|
|
||||||
|
|
||||||
// Hide Element Plus default bottom divider on dark backgrounds.
|
|
||||||
&::before {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-table__inner-wrapper),
|
|
||||||
:deep(.el-table__header-wrapper),
|
|
||||||
:deep(.el-table__body-wrapper),
|
|
||||||
:deep(.el-table__fixed),
|
|
||||||
:deep(.el-table__fixed-right),
|
|
||||||
:deep(.el-table__fixed-right-patch) {
|
|
||||||
background: #1e2024 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-table__header-wrapper) {
|
|
||||||
.el-table__header th {
|
|
||||||
background-color: #1e2024;
|
|
||||||
color: rgba(255, 255, 255, 0.75);
|
|
||||||
border-bottom: 1px solid #2a2d32;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-table__body-wrapper) {
|
|
||||||
.el-table__body tr {
|
|
||||||
background-color: #1e2024;
|
|
||||||
transition: background 0.2s;
|
|
||||||
|
|
||||||
&:hover td {
|
|
||||||
background-color: #25282e !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
td {
|
|
||||||
border-bottom: 1px solid #2a2d32;
|
|
||||||
color: rgba(255, 255, 255, 0.88);
|
|
||||||
background-color: #1e2024;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-table__row--striped) {
|
|
||||||
td {
|
|
||||||
background-color: #1a1c20 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-table__cell) {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-table__fixed-right::before),
|
|
||||||
:deep(.el-table__fixed::before) {
|
|
||||||
background-color: #2a2d32 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.key-mask {
|
|
||||||
font-family: Consolas, 'Courier New', monospace;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #87f5eb;
|
|
||||||
background: #14161a;
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 6px;
|
|
||||||
letter-spacing: 0.3px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.view-key-content {
|
|
||||||
|
|
||||||
.view-key-name,
|
|
||||||
.view-key-value {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
|
|
||||||
.label {
|
|
||||||
font-weight: 600;
|
|
||||||
color: rgba(255, 255, 255, 0.75);
|
|
||||||
display: inline-block;
|
|
||||||
width: 90px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.value {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.key-box {
|
|
||||||
margin-top: 8px;
|
|
||||||
padding: 14px;
|
|
||||||
background: #14161a;
|
|
||||||
border-radius: 12px;
|
|
||||||
border: 1px solid #2a2d32;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 16px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
|
|
||||||
code {
|
|
||||||
font-family: Consolas, 'Courier New', monospace;
|
|
||||||
color: #87f5eb;
|
|
||||||
font-size: 14px;
|
|
||||||
word-break: break-all;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.view-key-tip {
|
|
||||||
margin-top: 16px;
|
|
||||||
padding: 10px 12px;
|
|
||||||
background: rgba(245, 176, 76, 0.12);
|
|
||||||
border-radius: 8px;
|
|
||||||
color: #f5b04c;
|
|
||||||
font-size: 13px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
i {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.full-width {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-input__wrapper),
|
|
||||||
:deep(.el-textarea__inner),
|
|
||||||
:deep(.el-select__wrapper) {
|
|
||||||
background-color: #25282e !important;
|
|
||||||
border: 1px solid #3a3e45 !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-input__inner),
|
|
||||||
:deep(.el-textarea__inner) {
|
|
||||||
color: rgba(255, 255, 255, 0.92) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-radio) {
|
|
||||||
margin-right: 20px;
|
|
||||||
color: rgba(255, 255, 255, 0.85);
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-dialog) {
|
|
||||||
background: #1e2024;
|
|
||||||
border: 1px solid #2a2d32;
|
|
||||||
border-radius: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-dialog__title),
|
|
||||||
:deep(.el-form-item__label) {
|
|
||||||
color: #fff !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-button--primary.is-link) {
|
|
||||||
color: #3fe0b5 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-button--danger.is-link) {
|
|
||||||
color: #f56c6c !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-table__inner-wrapper) {
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1200px) {
|
|
||||||
.stats-grid {
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.apikey-card :deep(.el-card__body) {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-wrapper {
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 496 B |
@ -1,163 +0,0 @@
|
|||||||
<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">
|
|
||||||
<div class="balance-main">
|
|
||||||
<span class="balance-icon-wrap">
|
|
||||||
<i class="iconfont icon-yue"></i>
|
|
||||||
</span>
|
|
||||||
<div class="balance-copy">
|
|
||||||
<span class="balance-label">账户余额</span>
|
|
||||||
<span class="balance-text">
|
|
||||||
{{ formattedBalance }}
|
|
||||||
<span class="balance-unit">元</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span class="combo-divider"></span>
|
|
||||||
<span class="recharge-text">充值</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 用户信息 -->
|
|
||||||
<div class="user-profile" :class="{ 'dropdown-open': userDropdownVisible }">
|
|
||||||
<div class="user-trigger" @click="toggleUserDropdown">
|
|
||||||
<div class="user-avatar">
|
|
||||||
<i class="iconfont icon-a-414126695"></i>
|
|
||||||
</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">用户名:</span>
|
|
||||||
<span class="item-value">{{ loginStore.userInfo?.user?.name || loginStore.userInfo?.user?.username }}</span>
|
|
||||||
</div>
|
|
||||||
<!-- <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, computed } 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 userDisplayName = computed(() => loginStore.userInfo?.user?.name || loginStore.userInfo?.user?.username || '用户');
|
|
||||||
// const userInitial = computed(() => userDisplayName.value.trim().charAt(0).toUpperCase() || 'U');
|
|
||||||
const formattedBalance = computed(() => Number(balances.value || 0).toLocaleString('zh-CN'));
|
|
||||||
|
|
||||||
// 切换下拉菜单
|
|
||||||
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>();
|
|
||||||
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>
|
|
||||||
@ -1,96 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="login-prompt" :class="{ 'is-full-height': fullHeight }">
|
|
||||||
<div class="login-prompt__content">
|
|
||||||
<div class="login-prompt__title">{{ title }}</div>
|
|
||||||
<div v-if="description" class="login-prompt__description">{{ description }}</div>
|
|
||||||
<el-button class="login-prompt__button" @click="handleLogin">
|
|
||||||
{{ buttonText }}
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
title?: string;
|
|
||||||
description?: string;
|
|
||||||
buttonText?: string;
|
|
||||||
fullHeight?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
withDefaults(defineProps<Props>(), {
|
|
||||||
title: '请先登录',
|
|
||||||
description: '登录后即可继续使用当前功能。',
|
|
||||||
buttonText: '立即登录',
|
|
||||||
fullHeight: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const handleLogin = () => {
|
|
||||||
router.push('/login');
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="less">
|
|
||||||
.login-prompt {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 40px 24px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
&.is-full-height {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-prompt__content {
|
|
||||||
width: min(460px, 100%);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 18px;
|
|
||||||
padding: 34px 36px;
|
|
||||||
// border: 1px solid rgba(148, 216, 255, 0.08);
|
|
||||||
border-radius: 18px;
|
|
||||||
// background:
|
|
||||||
// linear-gradient(180deg, rgba(18, 28, 42, 0.42), rgba(12, 18, 28, 0.18));
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-prompt__title {
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: rgba(255, 255, 255, 0.92);
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-prompt__description {
|
|
||||||
max-width: 390px;
|
|
||||||
line-height: 1.75;
|
|
||||||
color: rgba(235, 244, 255, 0.58);
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-prompt__button {
|
|
||||||
min-width: 148px;
|
|
||||||
height: 44px;
|
|
||||||
margin-top: 8px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 12px;
|
|
||||||
color: #07101a;
|
|
||||||
background: linear-gradient(270deg, #94d8ff 11.11%, #e0ddff);
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 700;
|
|
||||||
box-shadow: 0 12px 28px rgba(148, 216, 255, 0.18);
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:focus {
|
|
||||||
color: #07101a;
|
|
||||||
background: linear-gradient(270deg, #a8e0ff 11.11%, #efedff);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,289 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="recharge-modal-mask" @click.self="closeModal">
|
|
||||||
<div class="recharge-container">
|
|
||||||
<!-- 头部 -->
|
|
||||||
<div class="modal-header">
|
|
||||||
<div class="header-copy">
|
|
||||||
<h2>充值账户</h2>
|
|
||||||
<p class="header-desc">选择金额后跳转到安全支付页面,余额到账后可立即使用。</p>
|
|
||||||
</div>
|
|
||||||
<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>
|
|
||||||
<span class="provider-tag">推荐</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">
|
|
||||||
<div class="selected-info__item">
|
|
||||||
<span class="selected-info__label">充值渠道</span>
|
|
||||||
<strong class="selected-info__value">支付宝</strong>
|
|
||||||
</div>
|
|
||||||
<div class="selected-info__item selected-info__item--highlight">
|
|
||||||
<span class="selected-info__label">到账金额</span>
|
|
||||||
<strong class="selected-amount">¥{{ selectedAmount }}</strong>
|
|
||||||
</div>
|
|
||||||
</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 class="recharge-tips">
|
|
||||||
<span>支付由第三方渠道完成</span>
|
|
||||||
<span>如长时间未到账,请刷新余额或联系客服</span>
|
|
||||||
</div>
|
|
||||||
</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;
|
|
||||||
|
|
||||||
// 关闭自定义弹窗
|
|
||||||
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'
|
|
||||||
};
|
|
||||||
|
|
||||||
// 充值响应类型
|
|
||||||
interface RechargeResponse {
|
|
||||||
status: string;
|
|
||||||
message?: string;
|
|
||||||
data: { url: string } | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调用充值接口
|
|
||||||
const res = await recharge<RechargeResponse, typeof rechargeParams>(rechargeParams);
|
|
||||||
|
|
||||||
// 处理充值结果
|
|
||||||
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">
|
|
||||||
@import url('@/assets/less/Recharge/Recharge.less');
|
|
||||||
</style>
|
|
||||||
@ -1,772 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<el-card class="rig">
|
|
||||||
<!-- 未登录状态 -->
|
|
||||||
<LoginPrompt
|
|
||||||
v-if="!isLoggedIn"
|
|
||||||
class="login-require"
|
|
||||||
title="请先登录"
|
|
||||||
description="登录后可查看生成任务和创作结果。"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 已登录状态 -->
|
|
||||||
<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" popper-class="custom-select-popup" @change="handleFilterChange">
|
|
||||||
<el-option label="全部状态" value="" />
|
|
||||||
<el-option label="已创建" value="CREATED" />
|
|
||||||
<el-option label="运行中" value="RUNNING" />
|
|
||||||
<el-option label="排队中" value="PENDING" />
|
|
||||||
<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" color="#ffffff">
|
|
||||||
<Loading />
|
|
||||||
</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>{{ getTaskTypeText(task) }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 提示词 + 模型信息 -->
|
|
||||||
<div class="task-prompt-wrapper">
|
|
||||||
<div class="prompt-content">
|
|
||||||
<div class="video-preview">
|
|
||||||
<div class="preview-bg" :style="getPreviewBgStyle(task.cover_url || task.image_url)">
|
|
||||||
<img
|
|
||||||
v-if="isImageTask(task)"
|
|
||||||
:src="task.image_url || task.cover_url"
|
|
||||||
alt="图片预览"
|
|
||||||
class="preview-video"
|
|
||||||
/>
|
|
||||||
<video
|
|
||||||
v-else
|
|
||||||
: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="!getTaskMediaUrl(task) && isInProgressStatus(task.status)" class="generating-status">
|
|
||||||
<div class="spinner"></div>
|
|
||||||
<div class="generating-text">{{ getStatusText(task.status) }}</div>
|
|
||||||
<div class="progress-wrap">
|
|
||||||
<div class="progress-track">
|
|
||||||
<div class="progress-fill" :style="{ width: `${getTaskProgress(task.status)}%` }"></div>
|
|
||||||
</div>
|
|
||||||
<div class="progress-value">{{ getTaskProgress(task.status) }}%</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- 视频播放器 -->
|
|
||||||
<img
|
|
||||||
v-else-if="isImageTask(task)"
|
|
||||||
:src="getTaskMediaUrl(task)"
|
|
||||||
alt="生成图片"
|
|
||||||
class="task-video task-image"
|
|
||||||
/>
|
|
||||||
<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: !getTaskMediaUrl(task) || downloadingTaskIds.has(task.taskid) }"
|
|
||||||
@click="getTaskMediaUrl(task) && !downloadingTaskIds.has(task.taskid) ? downloadVideo(task) : null"
|
|
||||||
:title="getTaskMediaUrl(task) ? '下载作品' : '暂无可下载的作品'"
|
|
||||||
>
|
|
||||||
<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 { 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';
|
|
||||||
import LoginPrompt from '@/components/LoginPrompt/LoginPrompt.vue';
|
|
||||||
|
|
||||||
// ==================== 实例和状态 ====================
|
|
||||||
const loginStore = useLoginStore();
|
|
||||||
const props = defineProps<{
|
|
||||||
llmcatelogid?: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
// ==================== 响应式数据 ====================
|
|
||||||
/** 登录状态 */
|
|
||||||
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 => isInProgressStatus(task.status));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 筛选特定状态的任务
|
|
||||||
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 handleFilterChange = () => {};
|
|
||||||
|
|
||||||
const normalizeTaskStatus = (status?: string): string => {
|
|
||||||
const upperStatus = String(status || '').toUpperCase();
|
|
||||||
const statusMap: Record<string, string> = {
|
|
||||||
CREATED: 'CREATED',
|
|
||||||
PENDING: 'PENDING',
|
|
||||||
QUEUEING: 'PENDING',
|
|
||||||
RUNNING: 'RUNNING',
|
|
||||||
PROCESSING: 'RUNNING',
|
|
||||||
SUCCEEDED: 'SUCCEEDED',
|
|
||||||
COMPLETED: 'SUCCEEDED',
|
|
||||||
FAILED: 'FAILED'
|
|
||||||
};
|
|
||||||
|
|
||||||
return statusMap[upperStatus] || upperStatus;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isInProgressStatus = (status?: string) => {
|
|
||||||
const normalizedStatus = normalizeTaskStatus(status);
|
|
||||||
return normalizedStatus === 'CREATED' || normalizedStatus === 'PENDING' || normalizedStatus === 'RUNNING';
|
|
||||||
};
|
|
||||||
|
|
||||||
const isImageTask = (task: TaskItem) => {
|
|
||||||
return task.action === 't2i' || (!!task.image_url && !task.video_url);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTaskMediaUrl = (task: TaskItem) => {
|
|
||||||
return task.video_url || task.image_url || '';
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTaskTypeText = (task: TaskItem) => {
|
|
||||||
if (task.action === 't2v') return '文生视频';
|
|
||||||
if (task.action === 'i2v') return '图生视频';
|
|
||||||
if (task.action === 'r2v') return '参考生视频';
|
|
||||||
if (task.action === 't2i' || isImageTask(task)) return '文生图';
|
|
||||||
return '视频生成';
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTaskProgress = (status?: string): number => {
|
|
||||||
const normalizedStatus = normalizeTaskStatus(status);
|
|
||||||
const progressMap: Record<string, number> = {
|
|
||||||
CREATED: 15,
|
|
||||||
PENDING: 35,
|
|
||||||
RUNNING: 70,
|
|
||||||
SUCCEEDED: 100,
|
|
||||||
FAILED: 100
|
|
||||||
};
|
|
||||||
|
|
||||||
return progressMap[normalizedStatus] ?? 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPreviewBgStyle = (coverUrl?: string) => {
|
|
||||||
if (!coverUrl) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
'--bg-image-url': `url("${coverUrl}")`
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const readStringField = (source: unknown, keys: string[]) => {
|
|
||||||
if (!source || typeof source !== 'object') return '';
|
|
||||||
const record = source as Record<string, unknown>;
|
|
||||||
|
|
||||||
for (const key of keys) {
|
|
||||||
const value = record[key];
|
|
||||||
if (typeof value === 'string' && value) return value;
|
|
||||||
if (Array.isArray(value) && typeof value[0] === 'string') return value[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
};
|
|
||||||
|
|
||||||
const getOutputImageUrl = (output: unknown) => {
|
|
||||||
const directUrl = readStringField(output, [
|
|
||||||
'image',
|
|
||||||
'image_url',
|
|
||||||
'url',
|
|
||||||
'imageUrl',
|
|
||||||
'output_image',
|
|
||||||
'output_url',
|
|
||||||
'b64_json',
|
|
||||||
'images',
|
|
||||||
]);
|
|
||||||
if (directUrl) return directUrl;
|
|
||||||
|
|
||||||
if (output && typeof output === 'object') {
|
|
||||||
const record = output as Record<string, unknown>;
|
|
||||||
const dataUrl = readStringField(record.data, ['image', 'image_url', 'url', 'b64_json', 'images']);
|
|
||||||
if (dataUrl) return dataUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTaskAction = (input: Record<string, unknown>, output: { video?: string; usage?: { action?: string } }) => {
|
|
||||||
// 优先用 output usage 里的 action
|
|
||||||
if (output.usage?.action) return output.usage.action;
|
|
||||||
// 再用 catelogid
|
|
||||||
const catelogid = input.catelogid as string;
|
|
||||||
if (catelogid === 'r2v') return 'r2v';
|
|
||||||
if (catelogid === 'i2v') return 'i2v';
|
|
||||||
if (catelogid === 't2v') return 't2v';
|
|
||||||
if (catelogid === 't2i') return 't2i';
|
|
||||||
// 有视频输出 → 视频任务
|
|
||||||
if (output.video) return 't2v';
|
|
||||||
// 纯图片输出 → 图片任务
|
|
||||||
if (getOutputImageUrl(output)) return 't2i';
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取状态显示文本
|
|
||||||
* @param status 状态值
|
|
||||||
*/
|
|
||||||
const getStatusText = (status: string): string => {
|
|
||||||
const statusMap: Record<string, string> = {
|
|
||||||
'CREATED': '已创建',
|
|
||||||
'PENDING': '排队中',
|
|
||||||
'RUNNING': '运行中',
|
|
||||||
'SUCCEEDED': '成功',
|
|
||||||
'FAILED': '失败'
|
|
||||||
};
|
|
||||||
const normalizedStatus = normalizeTaskStatus(status);
|
|
||||||
return statusMap[normalizedStatus] || normalizedStatus;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 下载视频
|
|
||||||
* @param task 任务对象
|
|
||||||
*/
|
|
||||||
const downloadVideo = async (task: TaskItem) => {
|
|
||||||
// 检查登录状态
|
|
||||||
if (!isLoggedIn.value) {
|
|
||||||
ElMessage.warning('请先登录');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mediaUrl = getTaskMediaUrl(task);
|
|
||||||
|
|
||||||
// 检查作品地址是否存在
|
|
||||||
if (!mediaUrl) {
|
|
||||||
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(mediaUrl);
|
|
||||||
|
|
||||||
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}.${isImageTask(task) ? 'png' : '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 = mediaUrl;
|
|
||||||
link.download = `${isImageTask(task) ? 'image' : 'video'}_${task.taskid}_${new Date().getTime()}.${isImageTask(task) ? 'png' : '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;
|
|
||||||
image_url?: string;
|
|
||||||
url?: string;
|
|
||||||
images?: string[];
|
|
||||||
imageUrl?: string;
|
|
||||||
output_image?: string;
|
|
||||||
output_url?: string;
|
|
||||||
b64_json?: string;
|
|
||||||
data?: unknown;
|
|
||||||
}
|
|
||||||
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 = normalizeTaskStatus(res.data.resposne.status || 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;
|
|
||||||
}
|
|
||||||
const imageUrl = getOutputImageUrl(res.data.resposne);
|
|
||||||
if (imageUrl) {
|
|
||||||
taskList.value[taskIndex].cover_url = imageUrl;
|
|
||||||
taskList.value[taskIndex].image_url = imageUrl;
|
|
||||||
if (!taskList.value[taskIndex].video_url) {
|
|
||||||
taskList.value[taskIndex].action = 't2i';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`查询任务状态失败 (${taskId}):`, error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 轮询所有进行中的任务状态
|
|
||||||
*/
|
|
||||||
const pollTasksStatus = () => {
|
|
||||||
// 获取所有进行中的任务
|
|
||||||
const processingTasks = taskList.value.filter(task => isInProgressStatus(task.status));
|
|
||||||
|
|
||||||
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 () => {
|
|
||||||
if (!isLoggedIn.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoading.value = true;
|
|
||||||
try {
|
|
||||||
const taskParams = props.llmcatelogid ? { llmcatelogid: props.llmcatelogid } : undefined;
|
|
||||||
// 调用任务列表接口
|
|
||||||
const res = await getTodayTask<TaskResult, { llmcatelogid: string } | undefined>(taskParams);
|
|
||||||
|
|
||||||
if (res.status === 'ok' && res.data?.tasks && Array.isArray(res.data.tasks)) {
|
|
||||||
// 转换后端数据格式为前端期望的格式
|
|
||||||
taskList.value = res.data.tasks.map((task) => {
|
|
||||||
const output = task.ioinfo?.output || [];
|
|
||||||
const firstOutput = output[0] || {};
|
|
||||||
// 优先找有 video 的输出,其次找有图片的,最后才兜底到第一条
|
|
||||||
const finalOutput = output.find((o) => o.video) || output.find((o) => getOutputImageUrl(o)) || firstOutput;
|
|
||||||
const input = task.ioinfo?.input || {};
|
|
||||||
const imageUrl = getOutputImageUrl(finalOutput);
|
|
||||||
const action = getTaskAction(input as unknown as Record<string, unknown>, finalOutput) || props.llmcatelogid;
|
|
||||||
const usageOutput = finalOutput?.usage as { model?: string; resolution?: string } | undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
taskid: task.taskid,
|
|
||||||
status: normalizeTaskStatus(task.status || finalOutput.status || 'CREATED'),
|
|
||||||
prompt: input.prompt,
|
|
||||||
cover_url: imageUrl || undefined,
|
|
||||||
image_url: imageUrl && !finalOutput.video ? imageUrl : undefined,
|
|
||||||
video_url: finalOutput.video || undefined,
|
|
||||||
duration: input.duration,
|
|
||||||
resolution: input.resolution || input.size || usageOutput?.resolution || '',
|
|
||||||
llmid: input.llmid,
|
|
||||||
model: input.model || usageOutput?.model || '',
|
|
||||||
use_time: task.use_time,
|
|
||||||
action,
|
|
||||||
} as TaskItem;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 加载完成后,启动轮询(如果有进行中的任务)
|
|
||||||
const hasProcessingTasks = taskList.value.some(task => isInProgressStatus(task.status));
|
|
||||||
|
|
||||||
if (hasProcessingTasks) {
|
|
||||||
startPolling();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
taskList.value = [];
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取任务列表失败:', error);
|
|
||||||
taskList.value = [];
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ==================== 生命周期 ====================
|
|
||||||
/**
|
|
||||||
* 组件挂载时加载任务列表
|
|
||||||
*/
|
|
||||||
onMounted(() => {
|
|
||||||
if (isLoggedIn.value) {
|
|
||||||
loadTaskList();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 组件卸载时停止轮询
|
|
||||||
*/
|
|
||||||
onUnmounted(() => {
|
|
||||||
stopPolling();
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 监听登录状态变化
|
|
||||||
* 登录时加载任务列表,登出时清空任务列表并停止轮询
|
|
||||||
*/
|
|
||||||
watch(() => loginStore.isLoggedIn, (newValue) => {
|
|
||||||
if (newValue) {
|
|
||||||
loadTaskList();
|
|
||||||
} else {
|
|
||||||
taskList.value = [];
|
|
||||||
stopPolling();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.llmcatelogid,
|
|
||||||
() => {
|
|
||||||
if (isLoggedIn.value) {
|
|
||||||
stopPolling();
|
|
||||||
loadTaskList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// ==================== 组件暴露 ====================
|
|
||||||
/**
|
|
||||||
* 向父组件暴露的方法
|
|
||||||
*/
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-wrap {
|
|
||||||
width: min(320px, 68%);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-track {
|
|
||||||
flex: 1;
|
|
||||||
height: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: rgba(255, 255, 255, 0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-fill {
|
|
||||||
height: 100%;
|
|
||||||
border-radius: inherit;
|
|
||||||
background: linear-gradient(90deg, #2cfff8 0%, #94d8ff 48%, #e0ddff 100%);
|
|
||||||
box-shadow: 0 0 18px rgba(44, 255, 248, 0.34);
|
|
||||||
transition: width 0.35s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-value {
|
|
||||||
min-width: 38px;
|
|
||||||
color: rgba(255, 255, 255, 0.72);
|
|
||||||
font-size: 13px;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
</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;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,665 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="img-img-panel">
|
|
||||||
<div v-if="!selectedModel" class="model-loading">
|
|
||||||
{{ modelLoading ? '模型加载中...' : '暂无配置模型,请等待管理员配置' }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template v-else>
|
|
||||||
<el-card class="prompt-card">
|
|
||||||
<div class="upload-area">
|
|
||||||
<input
|
|
||||||
ref="imageInputRef"
|
|
||||||
class="hidden-upload-input"
|
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
@change="handleImageSelect"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div v-if="imagePreviewUrl" class="image-preview">
|
|
||||||
<img :src="imagePreviewUrl" alt="参考图片" />
|
|
||||||
<button type="button" class="remove-image" @click="removeImage">删除</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button v-else type="button" class="upload-card" @click="triggerImageUpload">
|
|
||||||
<span class="upload-plus">+</span>
|
|
||||||
<span>上传参考图片</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="prompt-editor">
|
|
||||||
<el-input
|
|
||||||
v-model="prompt"
|
|
||||||
class="prompt-input"
|
|
||||||
:autosize="{ minRows: 8, maxRows: 8 }"
|
|
||||||
type="textarea"
|
|
||||||
maxlength="3000"
|
|
||||||
show-word-limit
|
|
||||||
:placeholder="promptPlaceholder"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<div v-if="configPanelVisible" class="config-panel">
|
|
||||||
<div
|
|
||||||
v-for="field in configFields"
|
|
||||||
:key="field.name"
|
|
||||||
class="config-section"
|
|
||||||
>
|
|
||||||
<div class="config-title">
|
|
||||||
{{ field.displayLabel }}
|
|
||||||
<span v-if="field.required" class="required-star">*</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="config-content">
|
|
||||||
<div v-if="field.uitype === 'code'" class="option-grid">
|
|
||||||
<button
|
|
||||||
v-for="option in field.data"
|
|
||||||
:key="option.value"
|
|
||||||
type="button"
|
|
||||||
class="option-btn"
|
|
||||||
:class="{ active: configData[field.name] === option.value }"
|
|
||||||
@click="configData[field.name] = option.value"
|
|
||||||
>
|
|
||||||
{{ option.text || option.value }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else-if="field.isQuantity" class="option-grid">
|
|
||||||
<button
|
|
||||||
v-for="num in field.quantityOptions"
|
|
||||||
:key="num"
|
|
||||||
type="button"
|
|
||||||
class="option-btn"
|
|
||||||
:class="{ active: Number(configData[field.name]) === num }"
|
|
||||||
@click="configData[field.name] = num"
|
|
||||||
>
|
|
||||||
{{ num }}个
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-input-number
|
|
||||||
v-else-if="field.uitype === 'int'"
|
|
||||||
v-model="configData[field.name]"
|
|
||||||
:min="1"
|
|
||||||
:max="field.max || 99"
|
|
||||||
:step="1"
|
|
||||||
controls-position="right"
|
|
||||||
style="width: 100%"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<el-input
|
|
||||||
v-else
|
|
||||||
v-model="configData[field.name]"
|
|
||||||
:placeholder="field.placeholder || `请输入${field.displayLabel}`"
|
|
||||||
clearable
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-row">
|
|
||||||
<el-select
|
|
||||||
v-model="currentModelId"
|
|
||||||
class="model-select"
|
|
||||||
placeholder="请选择模型"
|
|
||||||
popper-class="custom-select-popup"
|
|
||||||
@change="onModelChange"
|
|
||||||
>
|
|
||||||
<el-option
|
|
||||||
v-for="item in modelOptions"
|
|
||||||
:key="item.value"
|
|
||||||
:label="item.label"
|
|
||||||
:value="item.value"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="config-trigger"
|
|
||||||
:class="{ active: configPanelVisible }"
|
|
||||||
@click="configPanelVisible = !configPanelVisible"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
v-for="item in summaryItems"
|
|
||||||
:key="item.name"
|
|
||||||
class="summary-item"
|
|
||||||
>
|
|
||||||
{{ item.text }}
|
|
||||||
</span>
|
|
||||||
<span class="trigger-arrow">
|
|
||||||
<i :class="['iconfont', configPanelVisible ? 'icon-shang' : 'icon-xia']"></i>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { AllModel, queryModelPrice, texttoimage } from '@/apis/Model';
|
|
||||||
import type {
|
|
||||||
AllModelResponse,
|
|
||||||
InputField,
|
|
||||||
Model,
|
|
||||||
PriceQueryParams,
|
|
||||||
PriceQueryResponse,
|
|
||||||
TextToImageParams,
|
|
||||||
TextToImageResponse,
|
|
||||||
} from '@/types/model';
|
|
||||||
import { ElMessage } from 'element-plus';
|
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
|
||||||
|
|
||||||
type ConfigValue = string | number | boolean;
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'price-update', price: number): void;
|
|
||||||
(e: 'prompt-change', isEmpty: boolean): void;
|
|
||||||
(e: 'task-created', taskid: string): void;
|
|
||||||
(e: 'image-generated', result: TextToImageResponse): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const prompt = ref('');
|
|
||||||
const models = ref<Model[]>([]);
|
|
||||||
const currentModelId = ref('');
|
|
||||||
const configData = ref<Record<string, ConfigValue>>({});
|
|
||||||
const configPanelVisible = ref(false);
|
|
||||||
const generating = ref(false);
|
|
||||||
const modelLoading = ref(true);
|
|
||||||
const imageInputRef = ref<HTMLInputElement | null>(null);
|
|
||||||
const imageFileField = ref('image_file');
|
|
||||||
const imageData = ref('');
|
|
||||||
const imagePreviewUrl = ref('');
|
|
||||||
let priceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
|
|
||||||
const selectedModel = computed(() => models.value.find((item) => item.id === currentModelId.value));
|
|
||||||
|
|
||||||
const modelOptions = computed(() => models.value.map((item) => ({
|
|
||||||
label: item.model || item.name,
|
|
||||||
value: item.id,
|
|
||||||
})));
|
|
||||||
|
|
||||||
const promptPlaceholder = computed(() => {
|
|
||||||
const promptField = selectedModel.value?.input_fields?.find((field) => field.name === 'prompt');
|
|
||||||
return promptField?.placeholder || promptField?.label || '结合参考图片,描述你想生成的图片内容';
|
|
||||||
});
|
|
||||||
|
|
||||||
const isImageField = (field: InputField) => {
|
|
||||||
const name = field.name.toLowerCase();
|
|
||||||
return field.uitype === 'image' || name.includes('image') || name.includes('img') || name.includes('file');
|
|
||||||
};
|
|
||||||
|
|
||||||
const configFields = computed(() => {
|
|
||||||
return (selectedModel.value?.input_fields || [])
|
|
||||||
.filter((field) => field.name !== 'prompt' && !isImageField(field))
|
|
||||||
.map((field) => {
|
|
||||||
const value = configData.value[field.name];
|
|
||||||
const isQuantity = isQuantityName(field.name) && field.uitype === 'int';
|
|
||||||
const displayLabel = field.label || field.lable || field.name;
|
|
||||||
const selectedOption = field.data?.find((option) => option.value === value);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...field,
|
|
||||||
displayLabel,
|
|
||||||
isQuantity,
|
|
||||||
quantityOptions: isQuantity
|
|
||||||
? Array.from({ length: Math.min(field.max || 4, 4) }, (_, index) => index + 1)
|
|
||||||
: [],
|
|
||||||
summaryText: selectedOption?.text || (isQuantity ? `${value || 1}个` : String(value || displayLabel)),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const summaryItems = computed(() => configFields.value.slice(0, 4).map((field) => ({
|
|
||||||
name: field.name,
|
|
||||||
text: field.summaryText,
|
|
||||||
})));
|
|
||||||
|
|
||||||
const isQuantityName = (name: string) => {
|
|
||||||
const lowerName = name.toLowerCase();
|
|
||||||
return ['num', 'count', 'quantity', 'batch_size', 'n'].some((key) => lowerName.includes(key));
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultConfigValue = (field: InputField): ConfigValue => {
|
|
||||||
if (field.defaultvalue !== undefined && field.defaultvalue !== null) return field.defaultvalue;
|
|
||||||
if (field.uitype === 'code' && field.data?.length) return field.data[0].value;
|
|
||||||
if (field.uitype === 'int') return 1;
|
|
||||||
return '';
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetConfig = () => {
|
|
||||||
const nextConfig: Record<string, ConfigValue> = {};
|
|
||||||
const imageField = selectedModel.value?.input_fields?.find(isImageField);
|
|
||||||
imageFileField.value = imageField?.name || 'image_file';
|
|
||||||
|
|
||||||
(selectedModel.value?.input_fields || [])
|
|
||||||
.filter((field) => field.name !== 'prompt' && !isImageField(field))
|
|
||||||
.forEach((field) => {
|
|
||||||
nextConfig[field.name] = defaultConfigValue(field);
|
|
||||||
});
|
|
||||||
|
|
||||||
configData.value = nextConfig;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onModelChange = () => {
|
|
||||||
resetConfig();
|
|
||||||
configPanelVisible.value = false;
|
|
||||||
fetchPrice();
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadImageToImageModels = async () => {
|
|
||||||
interface ModelParams {
|
|
||||||
llmcatelogid: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
modelLoading.value = true;
|
|
||||||
try {
|
|
||||||
const res = await AllModel<AllModelResponse, ModelParams>({
|
|
||||||
llmcatelogid: 'i2i',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.status === 'ok' && res.data?.length) {
|
|
||||||
models.value = res.data;
|
|
||||||
currentModelId.value = res.data[0]?.id || '';
|
|
||||||
resetConfig();
|
|
||||||
fetchPrice();
|
|
||||||
} else {
|
|
||||||
models.value = [];
|
|
||||||
currentModelId.value = '';
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
modelLoading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const triggerImageUpload = () => {
|
|
||||||
imageInputRef.value?.click();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleImageSelect = (event: Event) => {
|
|
||||||
const input = event.target as HTMLInputElement;
|
|
||||||
const file = input.files?.[0];
|
|
||||||
if (!file) return;
|
|
||||||
if (!file.type.startsWith('image/')) {
|
|
||||||
ElMessage.warning('请上传图片文件');
|
|
||||||
input.value = '';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = () => {
|
|
||||||
imageData.value = String(reader.result || '');
|
|
||||||
imagePreviewUrl.value = imageData.value;
|
|
||||||
emitPromptState();
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeImage = () => {
|
|
||||||
imageData.value = '';
|
|
||||||
imagePreviewUrl.value = '';
|
|
||||||
if (imageInputRef.value) imageInputRef.value.value = '';
|
|
||||||
emitPromptState();
|
|
||||||
};
|
|
||||||
|
|
||||||
const emitPromptState = () => {
|
|
||||||
emit('prompt-change', !prompt.value.trim() || !imageData.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildPriceParams = (): PriceQueryParams | null => {
|
|
||||||
if (!selectedModel.value) return null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
product_type: 'llm',
|
|
||||||
product_id: selectedModel.value.id,
|
|
||||||
config_data: {
|
|
||||||
action: selectedModel.value.apiname,
|
|
||||||
has_image: !!imageData.value,
|
|
||||||
...configData.value,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildGenerateParams = (): TextToImageParams | null => {
|
|
||||||
if (!selectedModel.value) return null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
model: selectedModel.value.model,
|
|
||||||
catelogid: 'i2i',
|
|
||||||
prompt: prompt.value.trim(),
|
|
||||||
[imageFileField.value]: imageData.value,
|
|
||||||
...configData.value,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchPrice = () => {
|
|
||||||
if (priceTimer) clearTimeout(priceTimer);
|
|
||||||
|
|
||||||
priceTimer = setTimeout(async () => {
|
|
||||||
const params = buildPriceParams();
|
|
||||||
if (!params) {
|
|
||||||
emit('price-update', 0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await queryModelPrice<PriceQueryResponse, PriceQueryParams>(params);
|
|
||||||
emit('price-update', res.status === 'ok' && res.data ? res.data.amount : 0);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('图生图价格查询失败:', error);
|
|
||||||
emit('price-update', 0);
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
};
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => [currentModelId.value, configData.value],
|
|
||||||
() => fetchPrice(),
|
|
||||||
{ deep: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => [prompt.value, imageData.value],
|
|
||||||
() => emitPromptState(),
|
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
const generate = async () => {
|
|
||||||
if (!imageData.value) {
|
|
||||||
ElMessage.warning('请上传参考图片');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!prompt.value.trim()) {
|
|
||||||
ElMessage.warning('请输入提示词');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = buildGenerateParams();
|
|
||||||
if (!params) {
|
|
||||||
ElMessage.warning('模型未加载完成');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
generating.value = true;
|
|
||||||
try {
|
|
||||||
const res = await texttoimage<TextToImageResponse, TextToImageParams>(params);
|
|
||||||
const taskid = res.data?.taskid || res.taskid;
|
|
||||||
const images = res.image || res.data?.image || [];
|
|
||||||
|
|
||||||
if (String(res.status).toUpperCase() === 'SUCCEEDED' && images.length) {
|
|
||||||
ElMessage.success('图生图生成成功');
|
|
||||||
emit('image-generated', res);
|
|
||||||
resetForm();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (['CREATED', 'PENDING', 'RUNNING'].includes(String(res.status).toUpperCase())) {
|
|
||||||
ElMessage.success('图生图任务已提交,请稍后在任务列表中查看');
|
|
||||||
if (taskid) emit('task-created', taskid);
|
|
||||||
resetForm();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
ElMessage.error(res.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 = '';
|
|
||||||
removeImage();
|
|
||||||
resetConfig();
|
|
||||||
emit('price-update', 0);
|
|
||||||
emit('prompt-change', true);
|
|
||||||
};
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
generate,
|
|
||||||
resetForm,
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
loadImageToImageModels();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="less">
|
|
||||||
.img-img-panel {
|
|
||||||
position: relative;
|
|
||||||
height: 100%;
|
|
||||||
min-height: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 0 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.model-loading {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: 12px;
|
|
||||||
color: rgba(255, 255, 255, 0.62);
|
|
||||||
background: #1a1c20;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt-card {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
||||||
border-radius: 12px;
|
|
||||||
background: #1a1c20;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
:deep(.el-card__body) {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.hidden-upload-input {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-area {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-card,
|
|
||||||
.image-preview {
|
|
||||||
width: 160px;
|
|
||||||
height: 160px;
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-card {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 8px;
|
|
||||||
border: 1px dashed rgba(255, 255, 255, 0.24);
|
|
||||||
color: rgba(255, 255, 255, 0.72);
|
|
||||||
background: rgba(0, 0, 0, 0.22);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-plus {
|
|
||||||
font-size: 34px;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-preview {
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
background: rgba(0, 0, 0, 0.22);
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.remove-image {
|
|
||||||
position: absolute;
|
|
||||||
right: 8px;
|
|
||||||
bottom: 8px;
|
|
||||||
height: 28px;
|
|
||||||
padding: 0 10px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
color: #fff;
|
|
||||||
background: rgba(0, 0, 0, 0.62);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt-editor {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt-input {
|
|
||||||
flex: 1;
|
|
||||||
|
|
||||||
:deep(.el-textarea) {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-textarea__inner) {
|
|
||||||
height: 100% !important;
|
|
||||||
resize: none;
|
|
||||||
border: none !important;
|
|
||||||
border-radius: 10px !important;
|
|
||||||
color: #fff !important;
|
|
||||||
background: rgba(0, 0, 0, 0.32) !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
padding: 14px 16px !important;
|
|
||||||
line-height: 1.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-input__count) {
|
|
||||||
background: transparent;
|
|
||||||
color: rgba(255, 255, 255, 0.48);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-panel {
|
|
||||||
position: absolute;
|
|
||||||
left: 16px;
|
|
||||||
right: 16px;
|
|
||||||
bottom: 64px;
|
|
||||||
z-index: 10;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 16px;
|
|
||||||
border-radius: 12px;
|
|
||||||
background: #2b2d30;
|
|
||||||
box-shadow: 0 18px 50px rgba(0, 0, 0, 0.32);
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-section + .config-section {
|
|
||||||
margin-top: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-title {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
color: rgba(255, 255, 255, 0.64);
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.required-star {
|
|
||||||
margin-left: 4px;
|
|
||||||
color: #f56c6c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(88px, 1fr));
|
|
||||||
gap: 8px;
|
|
||||||
border-radius: 10px;
|
|
||||||
background: rgba(0, 0, 0, 0.18);
|
|
||||||
padding: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-btn {
|
|
||||||
height: 34px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
color: rgba(255, 255, 255, 0.76);
|
|
||||||
background: transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
color: #fff;
|
|
||||||
background: rgba(255, 255, 255, 0.14);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-row {
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
min-height: 52px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.model-select {
|
|
||||||
flex: 0 0 160px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-trigger {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 0 14px;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
||||||
border-radius: 12px;
|
|
||||||
color: #fff;
|
|
||||||
background: #252729;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
border-color: rgba(69, 238, 120, 0.8);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-item {
|
|
||||||
position: relative;
|
|
||||||
color: rgba(255, 255, 255, 0.82);
|
|
||||||
font-size: 14px;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
& + .summary-item {
|
|
||||||
padding-left: 12px;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 50%;
|
|
||||||
width: 1px;
|
|
||||||
height: 16px;
|
|
||||||
background: rgba(255, 255, 255, 0.12);
|
|
||||||
transform: translateY(-50%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.trigger-arrow {
|
|
||||||
margin-left: auto;
|
|
||||||
color: rgba(255, 255, 255, 0.72);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,266 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div class="left">
|
|
||||||
<div class="tab">
|
|
||||||
<ImgTap :activeTab="activeTab" @tabChange="handleTabChange" />
|
|
||||||
</div>
|
|
||||||
<div class="content">
|
|
||||||
<ReferenceImg
|
|
||||||
ref="referenceImgRef"
|
|
||||||
v-if="activeTab === 'reference'"
|
|
||||||
@price-update="handleReferencePriceUpdate"
|
|
||||||
@prompt-change="handleReferencePromptChange"
|
|
||||||
@task-created="handleTaskCreated"
|
|
||||||
@image-generated="handleImageGenerated"
|
|
||||||
/>
|
|
||||||
<TextImg
|
|
||||||
ref="textImgRef"
|
|
||||||
v-else-if="activeTab === 'text'"
|
|
||||||
@price-update="handleTextPriceUpdate"
|
|
||||||
@prompt-change="handleTextPromptChange"
|
|
||||||
@task-created="handleTaskCreated"
|
|
||||||
@image-generated="handleImageGenerated"
|
|
||||||
/>
|
|
||||||
<ImgImg
|
|
||||||
ref="imgImgRef"
|
|
||||||
v-else-if="activeTab === 'image'"
|
|
||||||
@price-update="handleImagePriceUpdate"
|
|
||||||
@prompt-change="handleImagePromptChange"
|
|
||||||
@task-created="handleTaskCreated"
|
|
||||||
@image-generated="handleImageGenerated"
|
|
||||||
/>
|
|
||||||
</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, onMounted, onUnmounted, watch, computed } from 'vue';
|
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
|
||||||
import ImgTap from './ImgTap.vue';
|
|
||||||
import ReferenceImg from './ReferenceImg.vue';
|
|
||||||
import TextImg from './TextImg.vue';
|
|
||||||
import ImgImg from './ImgImg.vue';
|
|
||||||
import { debounce } from '@/utlis/debounce';
|
|
||||||
import type { TextToImageResponse } from '@/types/model';
|
|
||||||
|
|
||||||
const route = useRoute();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
interface TextImgExposed {
|
|
||||||
generate: () => Promise<boolean>;
|
|
||||||
resetForm: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ImgImgExposed {
|
|
||||||
generate: () => Promise<boolean>;
|
|
||||||
resetForm: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ReferenceImgExposed {
|
|
||||||
generate: () => Promise<boolean>;
|
|
||||||
resetForm: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 当前激活的tab
|
|
||||||
const activeTab = ref('reference');
|
|
||||||
const referenceImgRef = ref<ReferenceImgExposed | null>(null);
|
|
||||||
const textImgRef = ref<TextImgExposed | null>(null);
|
|
||||||
const imgImgRef = ref<ImgImgExposed | null>(null);
|
|
||||||
const textPrice = ref(0);
|
|
||||||
const imagePrice = ref(0);
|
|
||||||
const referencePrice = ref(0);
|
|
||||||
const isReferencePromptEmpty = ref(true);
|
|
||||||
const isTextPromptEmpty = ref(true);
|
|
||||||
const isImagePromptEmpty = ref(true);
|
|
||||||
const isCreating = ref(false);
|
|
||||||
|
|
||||||
const handleTextPriceUpdate = (price: number) => {
|
|
||||||
textPrice.value = price;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTextPromptChange = (isEmpty: boolean) => {
|
|
||||||
isTextPromptEmpty.value = isEmpty;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleReferencePriceUpdate = (price: number) => {
|
|
||||||
referencePrice.value = price;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleReferencePromptChange = (isEmpty: boolean) => {
|
|
||||||
isReferencePromptEmpty.value = isEmpty;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleImagePriceUpdate = (price: number) => {
|
|
||||||
imagePrice.value = price;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleImagePromptChange = (isEmpty: boolean) => {
|
|
||||||
isImagePromptEmpty.value = isEmpty;
|
|
||||||
};
|
|
||||||
|
|
||||||
const currentPrice = computed(() => {
|
|
||||||
if (activeTab.value === 'text') {
|
|
||||||
return textPrice.value;
|
|
||||||
}
|
|
||||||
if (activeTab.value === 'image') {
|
|
||||||
return imagePrice.value;
|
|
||||||
}
|
|
||||||
return referencePrice.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
const isCreateButtonDisabled = computed(() => {
|
|
||||||
if (isCreating.value) return true;
|
|
||||||
if (activeTab.value === 'reference') return isReferencePromptEmpty.value;
|
|
||||||
if (activeTab.value === 'text') return isTextPromptEmpty.value;
|
|
||||||
if (activeTab.value === 'image') return isImagePromptEmpty.value;
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'task-created', taskid: string): void;
|
|
||||||
(e: 'image-generated', result: TextToImageResponse): void;
|
|
||||||
(e: 'generation-start'): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const handleTaskCreated = (taskid: string) => {
|
|
||||||
emit('task-created', taskid);
|
|
||||||
if (activeTab.value === 'text') {
|
|
||||||
textImgRef.value?.resetForm?.();
|
|
||||||
}
|
|
||||||
if (activeTab.value === 'image') {
|
|
||||||
imgImgRef.value?.resetForm?.();
|
|
||||||
}
|
|
||||||
if (activeTab.value === 'reference') {
|
|
||||||
referenceImgRef.value?.resetForm?.();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleImageGenerated = (result: TextToImageResponse) => {
|
|
||||||
emit('image-generated', result);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCreate = debounce(async () => {
|
|
||||||
if (isCreateButtonDisabled.value) return;
|
|
||||||
|
|
||||||
isCreating.value = true;
|
|
||||||
emit('generation-start');
|
|
||||||
try {
|
|
||||||
if (activeTab.value === 'text') {
|
|
||||||
await textImgRef.value?.generate?.();
|
|
||||||
}
|
|
||||||
if (activeTab.value === 'image') {
|
|
||||||
await imgImgRef.value?.generate?.();
|
|
||||||
}
|
|
||||||
if (activeTab.value === 'reference') {
|
|
||||||
await referenceImgRef.value?.generate?.();
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
isCreating.value = false;
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
// 处理tab切换
|
|
||||||
const handleTabChange = debounce((tab: string) => {
|
|
||||||
activeTab.value = tab;
|
|
||||||
// 更新路由
|
|
||||||
router.push(`/ImgGenerate/${tab}`);
|
|
||||||
}, 300);
|
|
||||||
|
|
||||||
// 根据路由设置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 if (path.includes('/ImgGenerate/image')) {
|
|
||||||
activeTab.value = 'image';
|
|
||||||
} else {
|
|
||||||
// 默认显示reference
|
|
||||||
activeTab.value = 'reference';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 监听路由变化
|
|
||||||
watch(() => route.path, () => {
|
|
||||||
updateActiveTabFromRoute();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 组件挂载时初始化
|
|
||||||
onMounted(() => {
|
|
||||||
updateActiveTabFromRoute();
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
handleTabChange.cancel();
|
|
||||||
handleCreate.cancel();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<style scoped lang='less'>
|
|
||||||
.left{
|
|
||||||
width: 100%;
|
|
||||||
height: calc(100vh - 80px);
|
|
||||||
// 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;
|
|
||||||
gap: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
&:hover {
|
|
||||||
background-color: rgba(255, 255, 255, 0.02) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-info {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
opacity: 0.55;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,89 +0,0 @@
|
|||||||
<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
|
|
||||||
class="tab-item"
|
|
||||||
:class="{ active: activeTab === 'image' }"
|
|
||||||
@click="switchTab('image')"
|
|
||||||
>
|
|
||||||
图生图
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script setup lang='ts'>
|
|
||||||
|
|
||||||
// 接收父组件传递的activeTab
|
|
||||||
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 {
|
|
||||||
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>
|
|
||||||
@ -1,784 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="reference-img-panel">
|
|
||||||
<div v-if="!selectedModel" class="model-loading">
|
|
||||||
{{ modelLoading ? '模型加载中...' : '暂无配置模型,请等待管理员配置' }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template v-else>
|
|
||||||
<el-card class="prompt-card">
|
|
||||||
<div class="reference-upload">
|
|
||||||
<div class="reference-copy">
|
|
||||||
<div class="reference-title">参考生图</div>
|
|
||||||
<div class="reference-desc">上传参考图片,结合提示词生成全新图片</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<input
|
|
||||||
ref="imageInputRef"
|
|
||||||
class="hidden-upload-input"
|
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
multiple
|
|
||||||
@change="handleImageSelect"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<button type="button" class="upload-action" @click="triggerImageUpload">
|
|
||||||
<span class="upload-action__icon">+</span>
|
|
||||||
<span>添加图片</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="referenceImages.length" class="image-list">
|
|
||||||
<div
|
|
||||||
v-for="image in referenceImages"
|
|
||||||
:key="image.id"
|
|
||||||
class="image-card"
|
|
||||||
>
|
|
||||||
<img :src="image.url" :alt="image.name" />
|
|
||||||
<button type="button" class="remove-image" @click="removeImage(image.id)">删除</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="prompt-editor">
|
|
||||||
<el-input
|
|
||||||
v-model="prompt"
|
|
||||||
class="prompt-input"
|
|
||||||
:autosize="{ minRows: 10, maxRows: 10 }"
|
|
||||||
type="textarea"
|
|
||||||
maxlength="3000"
|
|
||||||
show-word-limit
|
|
||||||
:placeholder="promptPlaceholder"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<div v-if="configPanelVisible" class="config-panel">
|
|
||||||
<div
|
|
||||||
v-for="field in configFields"
|
|
||||||
:key="field.name"
|
|
||||||
class="config-section"
|
|
||||||
>
|
|
||||||
<div class="config-title">
|
|
||||||
{{ field.displayLabel }}
|
|
||||||
<span v-if="field.required" class="required-star">*</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="config-content">
|
|
||||||
<div v-if="field.uitype === 'code'" class="option-grid">
|
|
||||||
<button
|
|
||||||
v-for="option in field.data"
|
|
||||||
:key="option.value"
|
|
||||||
type="button"
|
|
||||||
class="option-btn"
|
|
||||||
:class="{ active: configData[field.name] === option.value }"
|
|
||||||
@click="configData[field.name] = option.value"
|
|
||||||
>
|
|
||||||
{{ option.text || option.value }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else-if="field.isQuantity" class="option-grid">
|
|
||||||
<button
|
|
||||||
v-for="num in field.quantityOptions"
|
|
||||||
:key="num"
|
|
||||||
type="button"
|
|
||||||
class="option-btn"
|
|
||||||
:class="{ active: Number(configData[field.name]) === num }"
|
|
||||||
@click="configData[field.name] = num"
|
|
||||||
>
|
|
||||||
{{ num }}个
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-input-number
|
|
||||||
v-else-if="field.uitype === 'int'"
|
|
||||||
v-model="configData[field.name]"
|
|
||||||
:min="1"
|
|
||||||
:max="field.max || 99"
|
|
||||||
:step="1"
|
|
||||||
controls-position="right"
|
|
||||||
style="width: 100%"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<el-input
|
|
||||||
v-else
|
|
||||||
v-model="configData[field.name]"
|
|
||||||
:placeholder="field.placeholder || `请输入${field.displayLabel}`"
|
|
||||||
clearable
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-row">
|
|
||||||
<el-select
|
|
||||||
v-model="currentModelId"
|
|
||||||
class="model-select"
|
|
||||||
placeholder="请选择模型"
|
|
||||||
popper-class="custom-select-popup"
|
|
||||||
@change="onModelChange"
|
|
||||||
>
|
|
||||||
<el-option
|
|
||||||
v-for="item in modelOptions"
|
|
||||||
:key="item.value"
|
|
||||||
:label="item.label"
|
|
||||||
:value="item.value"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="config-trigger"
|
|
||||||
:class="{ active: configPanelVisible }"
|
|
||||||
@click="configPanelVisible = !configPanelVisible"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
v-for="item in summaryItems"
|
|
||||||
:key="item.name"
|
|
||||||
class="summary-item"
|
|
||||||
>
|
|
||||||
{{ item.text }}
|
|
||||||
</span>
|
|
||||||
<span class="trigger-arrow">
|
|
||||||
<i :class="['iconfont', configPanelVisible ? 'icon-shang' : 'icon-xia']"></i>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { AllModel, queryModelPrice, texttoimage } from '@/apis/Model';
|
|
||||||
import type {
|
|
||||||
AllModelResponse,
|
|
||||||
InputField,
|
|
||||||
Model,
|
|
||||||
PriceQueryParams,
|
|
||||||
PriceQueryResponse,
|
|
||||||
TextToImageParams,
|
|
||||||
TextToImageResponse,
|
|
||||||
} from '@/types/model';
|
|
||||||
import { ElMessage } from 'element-plus';
|
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
|
||||||
|
|
||||||
type ConfigValue = string | number | boolean;
|
|
||||||
|
|
||||||
interface ReferenceImage {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
url: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'price-update', price: number): void;
|
|
||||||
(e: 'prompt-change', isEmpty: boolean): void;
|
|
||||||
(e: 'task-created', taskid: string): void;
|
|
||||||
(e: 'image-generated', result: TextToImageResponse): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const prompt = ref('');
|
|
||||||
const models = ref<Model[]>([]);
|
|
||||||
const currentModelId = ref('');
|
|
||||||
const configData = ref<Record<string, ConfigValue>>({});
|
|
||||||
const configPanelVisible = ref(false);
|
|
||||||
const generating = ref(false);
|
|
||||||
const modelLoading = ref(true);
|
|
||||||
const imageInputRef = ref<HTMLInputElement | null>(null);
|
|
||||||
const referenceImages = ref<ReferenceImage[]>([]);
|
|
||||||
let priceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
|
|
||||||
const selectedModel = computed(() => models.value.find((item) => item.id === currentModelId.value));
|
|
||||||
|
|
||||||
const modelOptions = computed(() => models.value.map((item) => ({
|
|
||||||
label: item.model || item.name,
|
|
||||||
value: item.id,
|
|
||||||
})));
|
|
||||||
|
|
||||||
const promptPlaceholder = computed(() => {
|
|
||||||
const promptField = selectedModel.value?.input_fields?.find((field) => field.name === 'prompt');
|
|
||||||
return promptField?.placeholder || promptField?.label || '结合参考图片,描述想要生成的内容';
|
|
||||||
});
|
|
||||||
|
|
||||||
const isReferenceImageField = (field: InputField) => {
|
|
||||||
const name = field.name.toLowerCase();
|
|
||||||
return field.uitype === 'image'
|
|
||||||
|| name.includes('image')
|
|
||||||
|| name.includes('img')
|
|
||||||
|| name.includes('reference')
|
|
||||||
|| name.includes('subject');
|
|
||||||
};
|
|
||||||
|
|
||||||
const configFields = computed(() => {
|
|
||||||
return (selectedModel.value?.input_fields || [])
|
|
||||||
.filter((field) => field.name !== 'prompt' && !isReferenceImageField(field))
|
|
||||||
.map((field) => {
|
|
||||||
const value = configData.value[field.name];
|
|
||||||
const isQuantity = isQuantityName(field.name) && field.uitype === 'int';
|
|
||||||
const displayLabel = field.label || field.lable || field.name;
|
|
||||||
const selectedOption = field.data?.find((option) => option.value === value);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...field,
|
|
||||||
displayLabel,
|
|
||||||
placeholder: field.placeholder,
|
|
||||||
isQuantity,
|
|
||||||
quantityOptions: isQuantity
|
|
||||||
? Array.from({ length: Math.min(field.max || 4, 4) }, (_, index) => index + 1)
|
|
||||||
: [],
|
|
||||||
summaryText: selectedOption?.text || (isQuantity ? `${value || 1}个` : String(value || displayLabel)),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const summaryItems = computed(() => configFields.value.slice(0, 4).map((field) => ({
|
|
||||||
name: field.name,
|
|
||||||
text: field.summaryText,
|
|
||||||
})));
|
|
||||||
|
|
||||||
const isQuantityName = (name: string) => {
|
|
||||||
const lowerName = name.toLowerCase();
|
|
||||||
return ['num', 'count', 'quantity', 'batch_size', 'n'].some((key) => lowerName.includes(key));
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultConfigValue = (field: InputField): ConfigValue => {
|
|
||||||
if (field.defaultvalue !== undefined && field.defaultvalue !== null) return field.defaultvalue;
|
|
||||||
if (field.uitype === 'code' && field.data?.length) return field.data[0].value;
|
|
||||||
if (field.uitype === 'int') return 1;
|
|
||||||
return '';
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetConfig = () => {
|
|
||||||
const nextConfig: Record<string, ConfigValue> = {};
|
|
||||||
(selectedModel.value?.input_fields || [])
|
|
||||||
.filter((field) => field.name !== 'prompt' && !isReferenceImageField(field))
|
|
||||||
.forEach((field) => {
|
|
||||||
nextConfig[field.name] = defaultConfigValue(field);
|
|
||||||
});
|
|
||||||
configData.value = nextConfig;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onModelChange = () => {
|
|
||||||
resetConfig();
|
|
||||||
configPanelVisible.value = false;
|
|
||||||
fetchPrice();
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadReferenceImageModels = async () => {
|
|
||||||
interface ModelParams {
|
|
||||||
llmcatelogid: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
modelLoading.value = true;
|
|
||||||
try {
|
|
||||||
const res = await AllModel<AllModelResponse, ModelParams>({
|
|
||||||
llmcatelogid: 'ref2i',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.status === 'ok' && res.data?.length) {
|
|
||||||
models.value = res.data;
|
|
||||||
currentModelId.value = res.data[0]?.id || '';
|
|
||||||
resetConfig();
|
|
||||||
fetchPrice();
|
|
||||||
} else {
|
|
||||||
models.value = [];
|
|
||||||
currentModelId.value = '';
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
modelLoading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const triggerImageUpload = () => {
|
|
||||||
imageInputRef.value?.click();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleImageSelect = async (event: Event) => {
|
|
||||||
const input = event.target as HTMLInputElement;
|
|
||||||
const files = Array.from(input.files || []);
|
|
||||||
if (!files.length) return;
|
|
||||||
|
|
||||||
const imageFiles = files.filter((file) => file.type.startsWith('image/'));
|
|
||||||
if (imageFiles.length !== files.length) {
|
|
||||||
ElMessage.warning('已忽略非图片文件');
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextImages = await Promise.all(imageFiles.map(readImageFile));
|
|
||||||
referenceImages.value = [...referenceImages.value, ...nextImages];
|
|
||||||
input.value = '';
|
|
||||||
emitPromptState();
|
|
||||||
};
|
|
||||||
|
|
||||||
const readImageFile = (file: File): Promise<ReferenceImage> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = () => {
|
|
||||||
resolve({
|
|
||||||
id: `${file.name}-${file.size}-${file.lastModified}`,
|
|
||||||
name: file.name,
|
|
||||||
url: String(reader.result || ''),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
reader.onerror = () => reject(reader.error);
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeImage = (id: string) => {
|
|
||||||
referenceImages.value = referenceImages.value.filter((image) => image.id !== id);
|
|
||||||
emitPromptState();
|
|
||||||
};
|
|
||||||
|
|
||||||
const emitPromptState = () => {
|
|
||||||
emit('prompt-change', !prompt.value.trim() || referenceImages.value.length === 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildPriceParams = (): PriceQueryParams | null => {
|
|
||||||
if (!selectedModel.value) return null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
product_type: 'llm',
|
|
||||||
product_id: selectedModel.value.id,
|
|
||||||
config_data: {
|
|
||||||
action: selectedModel.value.apiname,
|
|
||||||
image_count: referenceImages.value.length,
|
|
||||||
...configData.value,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const imageFieldNames = computed(() => {
|
|
||||||
return (selectedModel.value?.input_fields || [])
|
|
||||||
.filter(isReferenceImageField)
|
|
||||||
.map((field) => field.name);
|
|
||||||
});
|
|
||||||
|
|
||||||
const buildImagePayload = () => {
|
|
||||||
const payload: Record<string, unknown> = {};
|
|
||||||
const fields = imageFieldNames.value;
|
|
||||||
|
|
||||||
if (fields.length <= 1) {
|
|
||||||
payload[fields[0] || 'images'] = referenceImages.value.map((image) => image.url);
|
|
||||||
return payload;
|
|
||||||
}
|
|
||||||
|
|
||||||
referenceImages.value.forEach((image, index) => {
|
|
||||||
payload[fields[index] || `image${index + 1}_file`] = image.url;
|
|
||||||
});
|
|
||||||
|
|
||||||
return payload;
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildGenerateParams = (): TextToImageParams | null => {
|
|
||||||
if (!selectedModel.value) return null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
model: selectedModel.value.model,
|
|
||||||
catelogid: 'r2i',
|
|
||||||
prompt: prompt.value.trim(),
|
|
||||||
...buildImagePayload(),
|
|
||||||
...configData.value,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchPrice = () => {
|
|
||||||
if (priceTimer) clearTimeout(priceTimer);
|
|
||||||
|
|
||||||
priceTimer = setTimeout(async () => {
|
|
||||||
const params = buildPriceParams();
|
|
||||||
if (!params) {
|
|
||||||
emit('price-update', 0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await queryModelPrice<PriceQueryResponse, PriceQueryParams>(params);
|
|
||||||
emit('price-update', res.status === 'ok' && res.data ? res.data.amount : 0);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('参考生图价格查询失败:', error);
|
|
||||||
emit('price-update', 0);
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
};
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => [currentModelId.value, configData.value],
|
|
||||||
() => fetchPrice(),
|
|
||||||
{ deep: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => [prompt.value, referenceImages.value.length],
|
|
||||||
() => emitPromptState(),
|
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
const generate = async () => {
|
|
||||||
if (!referenceImages.value.length) {
|
|
||||||
ElMessage.warning('请上传参考图片');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!prompt.value.trim()) {
|
|
||||||
ElMessage.warning('请输入提示词');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = buildGenerateParams();
|
|
||||||
if (!params) {
|
|
||||||
ElMessage.warning('模型未加载完成');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
generating.value = true;
|
|
||||||
try {
|
|
||||||
const res = await texttoimage<TextToImageResponse, TextToImageParams>(params);
|
|
||||||
const taskid = res.data?.taskid || res.taskid;
|
|
||||||
const images = res.image || res.data?.image || [];
|
|
||||||
|
|
||||||
if (String(res.status).toUpperCase() === 'SUCCEEDED' && images.length) {
|
|
||||||
ElMessage.success('参考生图生成成功');
|
|
||||||
emit('image-generated', res);
|
|
||||||
resetForm();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (['CREATED', 'PENDING', 'RUNNING'].includes(String(res.status).toUpperCase())) {
|
|
||||||
ElMessage.success('参考生图任务已提交,请稍后在任务列表中查看');
|
|
||||||
if (taskid) emit('task-created', taskid);
|
|
||||||
resetForm();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
ElMessage.error(res.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 = '';
|
|
||||||
referenceImages.value = [];
|
|
||||||
resetConfig();
|
|
||||||
emit('price-update', 0);
|
|
||||||
emit('prompt-change', true);
|
|
||||||
};
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
generate,
|
|
||||||
resetForm,
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
loadReferenceImageModels();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="less">
|
|
||||||
.reference-img-panel {
|
|
||||||
position: relative;
|
|
||||||
height: 100%;
|
|
||||||
min-height: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 0 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.model-loading {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: 12px;
|
|
||||||
color: rgba(255, 255, 255, 0.62);
|
|
||||||
background: #1a1c20;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt-card {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
||||||
border-radius: 12px;
|
|
||||||
background: #1a1c20;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
:deep(.el-card__body) {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 14px;
|
|
||||||
padding: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.reference-upload {
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 16px;
|
|
||||||
padding: 8px 4px 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reference-title {
|
|
||||||
color: #fff;
|
|
||||||
font-size: 22px;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1.35;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reference-desc {
|
|
||||||
margin-top: 6px;
|
|
||||||
color: rgba(255, 255, 255, 0.62);
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hidden-upload-input {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-action {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
height: 38px;
|
|
||||||
padding: 0 14px;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
border-radius: 10px;
|
|
||||||
color: #fff;
|
|
||||||
background: rgba(255, 255, 255, 0.06);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-action__icon {
|
|
||||||
font-size: 20px;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-list {
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 10px;
|
|
||||||
max-height: 180px;
|
|
||||||
overflow-y: auto;
|
|
||||||
scrollbar-width: none;
|
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-card {
|
|
||||||
position: relative;
|
|
||||||
width: 118px;
|
|
||||||
height: 118px;
|
|
||||||
overflow: hidden;
|
|
||||||
border-radius: 10px;
|
|
||||||
background: rgba(0, 0, 0, 0.24);
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.remove-image {
|
|
||||||
position: absolute;
|
|
||||||
right: 6px;
|
|
||||||
bottom: 6px;
|
|
||||||
height: 26px;
|
|
||||||
padding: 0 8px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
color: #fff;
|
|
||||||
background: rgba(0, 0, 0, 0.62);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt-editor {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt-input {
|
|
||||||
flex: 1;
|
|
||||||
|
|
||||||
:deep(.el-textarea) {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-textarea__inner) {
|
|
||||||
height: 100% !important;
|
|
||||||
resize: none;
|
|
||||||
border: none !important;
|
|
||||||
border-radius: 10px !important;
|
|
||||||
color: #fff !important;
|
|
||||||
background: rgba(0, 0, 0, 0.32) !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
padding: 14px 16px !important;
|
|
||||||
line-height: 1.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-textarea__inner::placeholder) {
|
|
||||||
color: rgba(255, 255, 255, 0.42);
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-input__count) {
|
|
||||||
background: transparent;
|
|
||||||
color: rgba(255, 255, 255, 0.48);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-panel {
|
|
||||||
position: absolute;
|
|
||||||
left: 16px;
|
|
||||||
right: 16px;
|
|
||||||
bottom: 64px;
|
|
||||||
z-index: 10;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 16px;
|
|
||||||
border-radius: 12px;
|
|
||||||
background: #2b2d30;
|
|
||||||
box-shadow: 0 18px 50px rgba(0, 0, 0, 0.32);
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-section + .config-section {
|
|
||||||
margin-top: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-title {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
color: rgba(255, 255, 255, 0.64);
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.required-star {
|
|
||||||
margin-left: 4px;
|
|
||||||
color: #f56c6c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(88px, 1fr));
|
|
||||||
gap: 8px;
|
|
||||||
border-radius: 10px;
|
|
||||||
background: rgba(0, 0, 0, 0.18);
|
|
||||||
padding: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-btn {
|
|
||||||
height: 34px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
color: rgba(255, 255, 255, 0.76);
|
|
||||||
background: transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
color: #fff;
|
|
||||||
background: rgba(255, 255, 255, 0.14);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-row {
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
min-height: 52px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.model-select {
|
|
||||||
flex: 0 0 160px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-trigger {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 0 14px;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
||||||
border-radius: 12px;
|
|
||||||
color: #fff;
|
|
||||||
background: #252729;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
border-color: rgba(69, 238, 120, 0.8);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-item {
|
|
||||||
position: relative;
|
|
||||||
color: rgba(255, 255, 255, 0.82);
|
|
||||||
font-size: 14px;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
& + .summary-item {
|
|
||||||
padding-left: 12px;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 50%;
|
|
||||||
width: 1px;
|
|
||||||
height: 16px;
|
|
||||||
background: rgba(255, 255, 255, 0.12);
|
|
||||||
transform: translateY(-50%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.trigger-arrow {
|
|
||||||
margin-left: auto;
|
|
||||||
color: rgba(255, 255, 255, 0.72);
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-select__wrapper),
|
|
||||||
:deep(.el-input__wrapper),
|
|
||||||
:deep(.el-input-number .el-input__wrapper) {
|
|
||||||
width: 100% !important;
|
|
||||||
min-height: 52px;
|
|
||||||
border: none !important;
|
|
||||||
border-radius: 12px !important;
|
|
||||||
color: #fff !important;
|
|
||||||
background: #252729 !important;
|
|
||||||
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.12) inset !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-select__selected-item),
|
|
||||||
:deep(.el-input__inner) {
|
|
||||||
color: #fff !important;
|
|
||||||
}
|
|
||||||
</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;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,595 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="text-img-panel">
|
|
||||||
<div v-if="!selectedModel" class="model-loading">
|
|
||||||
{{ modelLoading ? '模型加载中...' : '暂无配置模型,请等待管理员配置' }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template v-else>
|
|
||||||
<el-card class="prompt-card">
|
|
||||||
<div class="prompt-editor">
|
|
||||||
<el-input
|
|
||||||
v-model="prompt"
|
|
||||||
class="prompt-input"
|
|
||||||
:autosize="{ minRows: 12, maxRows: 12 }"
|
|
||||||
type="textarea"
|
|
||||||
maxlength="3000"
|
|
||||||
show-word-limit
|
|
||||||
:placeholder="promptPlaceholder"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<div v-if="configPanelVisible" class="config-panel">
|
|
||||||
<div
|
|
||||||
v-for="field in configFields"
|
|
||||||
:key="field.name"
|
|
||||||
class="config-section"
|
|
||||||
>
|
|
||||||
<div class="config-title">
|
|
||||||
{{ field.displayLabel }}
|
|
||||||
<span v-if="field.required" class="required-star">*</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="config-content">
|
|
||||||
<div v-if="field.uitype === 'code'" class="option-grid">
|
|
||||||
<button
|
|
||||||
v-for="option in field.data"
|
|
||||||
:key="option.value"
|
|
||||||
type="button"
|
|
||||||
class="option-btn"
|
|
||||||
:class="{ active: configData[field.name] === option.value }"
|
|
||||||
@click="configData[field.name] = option.value"
|
|
||||||
>
|
|
||||||
{{ option.text || option.value }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else-if="field.isQuantity" class="option-grid">
|
|
||||||
<button
|
|
||||||
v-for="num in field.quantityOptions"
|
|
||||||
:key="num"
|
|
||||||
type="button"
|
|
||||||
class="option-btn"
|
|
||||||
:class="{ active: Number(configData[field.name]) === num }"
|
|
||||||
@click="configData[field.name] = num"
|
|
||||||
>
|
|
||||||
{{ num }}个
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-input-number
|
|
||||||
v-else-if="field.uitype === 'int'"
|
|
||||||
v-model="configData[field.name]"
|
|
||||||
:min="1"
|
|
||||||
:max="field.max || 99"
|
|
||||||
:step="1"
|
|
||||||
controls-position="right"
|
|
||||||
style="width: 100%"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<el-input
|
|
||||||
v-else
|
|
||||||
v-model="configData[field.name]"
|
|
||||||
:placeholder="field.placeholder || `请输入${field.displayLabel}`"
|
|
||||||
clearable
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-row">
|
|
||||||
<el-select
|
|
||||||
v-model="currentModelId"
|
|
||||||
class="model-select"
|
|
||||||
placeholder="请选择模型"
|
|
||||||
popper-class="custom-select-popup"
|
|
||||||
@change="onModelChange"
|
|
||||||
>
|
|
||||||
<el-option
|
|
||||||
v-for="item in modelOptions"
|
|
||||||
:key="item.value"
|
|
||||||
:label="item.label"
|
|
||||||
:value="item.value"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="config-trigger"
|
|
||||||
:class="{ active: configPanelVisible }"
|
|
||||||
@click="configPanelVisible = !configPanelVisible"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
v-for="item in summaryItems"
|
|
||||||
:key="item.name"
|
|
||||||
class="summary-item"
|
|
||||||
>
|
|
||||||
{{ item.text }}
|
|
||||||
</span>
|
|
||||||
<span class="trigger-arrow">
|
|
||||||
<i :class="['iconfont', configPanelVisible ? 'icon-shang' : 'icon-xia']"></i>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { AllModel, queryModelPrice, texttoimage } from '@/apis/Model';
|
|
||||||
import type {
|
|
||||||
AllModelResponse,
|
|
||||||
InputField,
|
|
||||||
Model,
|
|
||||||
PriceQueryParams,
|
|
||||||
PriceQueryResponse,
|
|
||||||
TextToImageParams,
|
|
||||||
TextToImageResponse,
|
|
||||||
} from '@/types/model';
|
|
||||||
import { ElMessage } from 'element-plus';
|
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
|
||||||
|
|
||||||
type ConfigValue = string | number | boolean;
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'price-update', price: number): void;
|
|
||||||
(e: 'prompt-change', isEmpty: boolean): void;
|
|
||||||
(e: 'task-created', taskid: string): void;
|
|
||||||
(e: 'image-generated', result: TextToImageResponse): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const prompt = ref('');
|
|
||||||
const models = ref<Model[]>([]);
|
|
||||||
const currentModelId = ref('');
|
|
||||||
const configData = ref<Record<string, ConfigValue>>({});
|
|
||||||
const configPanelVisible = ref(false);
|
|
||||||
const generating = ref(false);
|
|
||||||
const modelLoading = ref(true);
|
|
||||||
let priceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
const modelOptions = computed(() => models.value.map((item) => ({
|
|
||||||
label: item.model || item.name,
|
|
||||||
value: item.id,
|
|
||||||
})));
|
|
||||||
|
|
||||||
const selectedModel = computed(() => models.value.find((item) => item.id === currentModelId.value));
|
|
||||||
|
|
||||||
const promptPlaceholder = computed(() => {
|
|
||||||
const promptField = selectedModel.value?.input_fields?.find((field) => field.name === 'prompt');
|
|
||||||
return promptField?.placeholder || promptField?.label || '描述你想要生成的图片内容';
|
|
||||||
});
|
|
||||||
|
|
||||||
const configFields = computed(() => {
|
|
||||||
return (selectedModel.value?.input_fields || [])
|
|
||||||
.filter((field) => field.name !== 'prompt')
|
|
||||||
.map((field) => {
|
|
||||||
const value = configData.value[field.name];
|
|
||||||
const isQuantity = isQuantityName(field.name) && field.uitype === 'int';
|
|
||||||
const displayLabel = field.label || field.lable || field.name;
|
|
||||||
const selectedOption = field.data?.find((option) => option.value === value);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...field,
|
|
||||||
displayLabel,
|
|
||||||
placeholder: field.placeholder,
|
|
||||||
isQuantity,
|
|
||||||
quantityOptions: isQuantity
|
|
||||||
? Array.from({ length: Math.min(field.max || 4, 4) }, (_, index) => index + 1)
|
|
||||||
: [],
|
|
||||||
summaryText: selectedOption?.text || (isQuantity ? `${value || 1}个` : String(value || displayLabel)),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const summaryItems = computed(() => configFields.value.slice(0, 4).map((field) => ({
|
|
||||||
name: field.name,
|
|
||||||
text: field.summaryText,
|
|
||||||
})));
|
|
||||||
|
|
||||||
const defaultConfigValue = (field: InputField): ConfigValue => {
|
|
||||||
if (field.defaultvalue !== undefined && field.defaultvalue !== null) {
|
|
||||||
return field.defaultvalue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (field.uitype === 'code' && field.data?.length) {
|
|
||||||
return field.data[0].value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (field.uitype === 'int') {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetConfig = () => {
|
|
||||||
const nextConfig: Record<string, ConfigValue> = {};
|
|
||||||
(selectedModel.value?.input_fields || [])
|
|
||||||
.filter((field) => field.name !== 'prompt')
|
|
||||||
.forEach((field) => {
|
|
||||||
nextConfig[field.name] = defaultConfigValue(field);
|
|
||||||
});
|
|
||||||
configData.value = nextConfig;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onModelChange = () => {
|
|
||||||
resetConfig();
|
|
||||||
configPanelVisible.value = false;
|
|
||||||
fetchPrice();
|
|
||||||
};
|
|
||||||
|
|
||||||
const isQuantityName = (name: string) => {
|
|
||||||
const lowerName = name.toLowerCase();
|
|
||||||
return ['num', 'count', 'quantity', 'batch_size', 'n'].some((key) => lowerName.includes(key));
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadTextToImageModels = async () => {
|
|
||||||
interface ModelParams {
|
|
||||||
llmcatelogid: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
modelLoading.value = true;
|
|
||||||
try {
|
|
||||||
const res = await AllModel<AllModelResponse, ModelParams>({
|
|
||||||
llmcatelogid: 't2i',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.status === 'ok' && res.data?.length) {
|
|
||||||
models.value = res.data;
|
|
||||||
currentModelId.value = res.data[0]?.id || '';
|
|
||||||
resetConfig();
|
|
||||||
fetchPrice();
|
|
||||||
} else {
|
|
||||||
models.value = [];
|
|
||||||
currentModelId.value = '';
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
modelLoading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildPriceParams = (): PriceQueryParams | null => {
|
|
||||||
if (!selectedModel.value) return null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
product_type: 'llm',
|
|
||||||
product_id: selectedModel.value.id,
|
|
||||||
config_data: {
|
|
||||||
action: selectedModel.value.apiname,
|
|
||||||
...configData.value,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const buildGenerateParams = (): TextToImageParams | null => {
|
|
||||||
if (!selectedModel.value) return null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
model: selectedModel.value.model,
|
|
||||||
catelogid: 't2i',
|
|
||||||
prompt: prompt.value.trim(),
|
|
||||||
...configData.value,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchPrice = () => {
|
|
||||||
if (priceTimer) clearTimeout(priceTimer);
|
|
||||||
|
|
||||||
priceTimer = setTimeout(async () => {
|
|
||||||
const params = buildPriceParams();
|
|
||||||
if (!params) {
|
|
||||||
emit('price-update', 0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await queryModelPrice<PriceQueryResponse, PriceQueryParams>(params);
|
|
||||||
emit('price-update', res.status === 'ok' && res.data ? res.data.amount : 0);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('文生图价格查询失败:', error);
|
|
||||||
emit('price-update', 0);
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => [currentModelId.value, configData.value],
|
|
||||||
() => fetchPrice(),
|
|
||||||
{ deep: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => prompt.value,
|
|
||||||
(value) => {
|
|
||||||
emit('prompt-change', !value.trim());
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
const generate = async () => {
|
|
||||||
if (!prompt.value.trim()) {
|
|
||||||
ElMessage.warning('请输入提示词');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = buildGenerateParams();
|
|
||||||
if (!params) {
|
|
||||||
ElMessage.warning('模型未加载完成');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
generating.value = true;
|
|
||||||
try {
|
|
||||||
const res = await texttoimage<TextToImageResponse, TextToImageParams>(params);
|
|
||||||
const taskid = res.data?.taskid || res.taskid;
|
|
||||||
const images = res.image || res.data?.image || [];
|
|
||||||
|
|
||||||
if (String(res.status).toUpperCase() === 'SUCCEEDED' && images.length) {
|
|
||||||
ElMessage.success('图片生成成功');
|
|
||||||
emit('image-generated', res);
|
|
||||||
resetForm();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (['CREATED', 'PENDING', 'RUNNING'].includes(String(res.status).toUpperCase())) {
|
|
||||||
ElMessage.success('图片生成任务已提交,请稍后在任务列表中查看');
|
|
||||||
if (taskid) {
|
|
||||||
emit('task-created', taskid);
|
|
||||||
}
|
|
||||||
resetForm();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
ElMessage.error(res.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 = '';
|
|
||||||
resetConfig();
|
|
||||||
emit('price-update', 0);
|
|
||||||
emit('prompt-change', true);
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
generate,
|
|
||||||
resetForm,
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
loadTextToImageModels();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="less">
|
|
||||||
.text-img-panel {
|
|
||||||
position: relative;
|
|
||||||
height: 100%;
|
|
||||||
min-height: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 0 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.model-loading {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: 12px;
|
|
||||||
color: rgba(255, 255, 255, 0.62);
|
|
||||||
background: #1a1c20;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt-card {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
||||||
border-radius: 12px;
|
|
||||||
background: #1a1c20;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
:deep(.el-card__body) {
|
|
||||||
height: 100%;
|
|
||||||
padding: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt-editor {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt-input {
|
|
||||||
flex: 1;
|
|
||||||
|
|
||||||
:deep(.el-textarea) {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-textarea__inner) {
|
|
||||||
height: 100% !important;
|
|
||||||
resize: none;
|
|
||||||
border: none !important;
|
|
||||||
border-radius: 10px !important;
|
|
||||||
color: #fff !important;
|
|
||||||
background: rgba(0, 0, 0, 0.32) !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
padding: 14px 16px !important;
|
|
||||||
line-height: 1.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-textarea__inner::placeholder) {
|
|
||||||
color: rgba(255, 255, 255, 0.42);
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-input__count) {
|
|
||||||
background: transparent;
|
|
||||||
color: rgba(255, 255, 255, 0.48);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-panel {
|
|
||||||
position: absolute;
|
|
||||||
left: 16px;
|
|
||||||
right: 16px;
|
|
||||||
bottom: 64px;
|
|
||||||
z-index: 10;
|
|
||||||
// max-height: 48%;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 16px;
|
|
||||||
border-radius: 12px;
|
|
||||||
background: #2b2d30;
|
|
||||||
box-shadow: 0 18px 50px rgba(0, 0, 0, 0.32);
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-section + .config-section {
|
|
||||||
margin-top: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-title {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
color: rgba(255, 255, 255, 0.64);
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.required-star {
|
|
||||||
margin-left: 4px;
|
|
||||||
color: #f56c6c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-content {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(88px, 1fr));
|
|
||||||
gap: 8px;
|
|
||||||
border-radius: 10px;
|
|
||||||
background: rgba(0, 0, 0, 0.18);
|
|
||||||
padding: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-btn {
|
|
||||||
min-height: 42px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
color: rgba(255, 255, 255, 0.86);
|
|
||||||
background: transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&.active {
|
|
||||||
color: #fff;
|
|
||||||
background: rgba(255, 255, 255, 0.12);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-row {
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
min-height: 52px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.model-select {
|
|
||||||
flex: 0 0 160px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-trigger {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 0 14px;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
||||||
border-radius: 12px;
|
|
||||||
color: #fff;
|
|
||||||
background: #252729;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
border-color: rgba(69, 238, 120, 0.8);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-item {
|
|
||||||
position: relative;
|
|
||||||
min-width: 0;
|
|
||||||
color: rgba(255, 255, 255, 0.82);
|
|
||||||
font-size: 14px;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
& + .summary-item {
|
|
||||||
padding-left: 12px;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 50%;
|
|
||||||
width: 1px;
|
|
||||||
height: 16px;
|
|
||||||
background: rgba(255, 255, 255, 0.12);
|
|
||||||
transform: translateY(-50%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.trigger-arrow {
|
|
||||||
margin-left: auto;
|
|
||||||
color: rgba(255, 255, 255, 0.72);
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-select__wrapper),
|
|
||||||
:deep(.el-input__wrapper),
|
|
||||||
:deep(.el-input-number .el-input__wrapper) {
|
|
||||||
width: 100% !important;
|
|
||||||
min-height: 52px;
|
|
||||||
border: none !important;
|
|
||||||
border-radius: 12px !important;
|
|
||||||
color: #fff !important;
|
|
||||||
background: #252729 !important;
|
|
||||||
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.12) inset !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-select__selected-item),
|
|
||||||
:deep(.el-input__inner) {
|
|
||||||
color: #fff !important;
|
|
||||||
}
|
|
||||||
</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.is-selected,
|
|
||||||
.custom-select-popup .el-select-dropdown__item.is-hovering,
|
|
||||||
.custom-select-popup .el-select-dropdown__item:hover {
|
|
||||||
color: #fff !important;
|
|
||||||
background-color: #3a3d46 !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,147 +0,0 @@
|
|||||||
<template>
|
|
||||||
<!-- AI视频功能选择栏 -->
|
|
||||||
<div class="ai-video-function-bar">
|
|
||||||
<!-- 功能按钮列表 - 横向滚动容器 -->
|
|
||||||
<div class="function-btn-scroll-container">
|
|
||||||
<!-- 功能按钮项 -->
|
|
||||||
<button
|
|
||||||
v-for="(item, index) in functionList"
|
|
||||||
:key="index"
|
|
||||||
class="function-btn-item"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<!-- 按钮文本区域 -->
|
|
||||||
<div class="function-btn-text-wrapper">
|
|
||||||
<div class="function-btn-text">{{ item.name }}</div>
|
|
||||||
</div>
|
|
||||||
<!-- 按钮图片区域 -->
|
|
||||||
<img
|
|
||||||
:src="item.imageUrl"
|
|
||||||
:alt="item.name"
|
|
||||||
class="function-btn-image"
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { reactive } from 'vue';
|
|
||||||
|
|
||||||
// 定义功能列表数据类型
|
|
||||||
interface FunctionItem {
|
|
||||||
name: string;
|
|
||||||
imageUrl: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 功能列表数据(可根据实际需求扩展)
|
|
||||||
const functionList = reactive<FunctionItem[]>([
|
|
||||||
{
|
|
||||||
name: '参考生视频',
|
|
||||||
imageUrl: 'https://image01.vidu.zone/vidu/media-asset/c2v-68811cfd.webp',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '图生视频',
|
|
||||||
imageUrl: 'https://image01.vidu.zone/vidu/media-asset/i2v-6e715ae0.webp',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'AI模板',
|
|
||||||
imageUrl: 'https://image01.vidu.zone/vidu/media-asset/templates-bab5f63b.webp',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '视频换脸',
|
|
||||||
imageUrl: 'https://image01.vidu.zone/vidu/media-asset/face-7de6838a.webp',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '视频复刻',
|
|
||||||
imageUrl: 'https://image01.vidu.zone/vidu/media-asset/remake-09a26546.webp',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '一键成片',
|
|
||||||
imageUrl: 'https://image01.vidu.zone/vidu/media-asset/oneclick-beafcd9f.webp',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '视频编辑',
|
|
||||||
imageUrl: 'https://image01.vidu.zone/vidu/media-asset/videoedit-de4fb509.webp',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '视频增强',
|
|
||||||
imageUrl: 'https://image01.vidu.zone/vidu/media-asset/videoupscale-190d7537.webp',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="less">
|
|
||||||
// AI视频功能栏外层容器
|
|
||||||
.ai-video-function-bar {
|
|
||||||
padding-bottom: 0.25rem; // 对应原pb-1
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
// 确保父容器宽度不受限制(默认就是块级元素宽度100%)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 横向滚动容器
|
|
||||||
.function-btn-scroll-container {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem; // 对应原gap-4
|
|
||||||
overflow-x: auto; // 允许水平滚动,并显示滚动条
|
|
||||||
padding-bottom: 0.25rem; // 为滚动条预留空间,避免内容被遮挡
|
|
||||||
|
|
||||||
// 如果希望滚动条更美观,可自定义(但保留滚动功能)
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
height: 6px; // 滚动条高度
|
|
||||||
}
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
background-color: rgba(255, 255, 255, 0.3); // 滚动条颜色
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
&::-webkit-scrollbar-track {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 功能按钮项
|
|
||||||
.function-btn-item {
|
|
||||||
display: flex;
|
|
||||||
min-width: 13.75rem; // 对应原min-w-[220px]
|
|
||||||
flex: 1 0 13.75rem; // 对应原flex-[1_0_220px]
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 0.75rem; // 对应原gap-3
|
|
||||||
overflow: hidden;
|
|
||||||
border-radius: 1rem; // 对应原rounded-16(假设16px=1rem)
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.12); // 对应原border-system-white12
|
|
||||||
background-color: rgba(255, 255, 255, 0.08); // 对应原bg-system-white08
|
|
||||||
padding-left: 1rem; // 对应原pl-4
|
|
||||||
text-align: left;
|
|
||||||
transition: background-color 0.3s ease;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
// hover状态
|
|
||||||
&:hover {
|
|
||||||
background-color: rgba(255, 255, 255, 0.12); // 对应原hover:bg-system-white12
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 按钮文本容器
|
|
||||||
.function-btn-text-wrapper {
|
|
||||||
min-width: 0;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 按钮文本样式
|
|
||||||
.function-btn-text {
|
|
||||||
font-weight: 600; // 对应原font-semibold
|
|
||||||
font-size: 1rem; // 对应原text-base
|
|
||||||
color: #ffffff; // 对应原text-white
|
|
||||||
line-height: 1.4; // 对应原leading-[1.4]
|
|
||||||
}
|
|
||||||
|
|
||||||
// 按钮图片样式
|
|
||||||
.function-btn-image {
|
|
||||||
height: 5.625rem; // 对应原h-[90px]
|
|
||||||
width: 6.8125rem; // 对应原w-[109px]
|
|
||||||
flex-shrink: 0;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,115 +0,0 @@
|
|||||||
|
|
||||||
<template>
|
|
||||||
<div class="stencil-container">
|
|
||||||
<div class="stencil-top">
|
|
||||||
<div
|
|
||||||
class="stencil-item"
|
|
||||||
:class="{ active: activeTab === 'mixed' }"
|
|
||||||
@click="handleTabClick('mixed')"
|
|
||||||
>
|
|
||||||
<div class="item-title">推荐</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="stencil-item"
|
|
||||||
:class="{ active: activeTab === 'share_elements' }"
|
|
||||||
@click="handleTabClick('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="handleTabClick('short_film')"
|
|
||||||
>
|
|
||||||
<div class="item-title">短片</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="stencil-item"
|
|
||||||
:class="{ active: activeTab === 'tutorial' }"
|
|
||||||
@click="handleTabClick('tutorial')"
|
|
||||||
>
|
|
||||||
<div class="item-title">教程</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
defineProps<{
|
|
||||||
activeTab: string
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'tab-change', tabType: string): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const handleTabClick = (type: string) => {
|
|
||||||
emit('tab-change', type)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.stencil-container {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stencil-top {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 24px;
|
|
||||||
padding: 12px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stencil-item {
|
|
||||||
cursor: pointer;
|
|
||||||
position: relative;
|
|
||||||
padding-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stencil-item.active .item-title {
|
|
||||||
color: #fff;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stencil-item.active::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
bottom: 0;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
width: 24px;
|
|
||||||
height: 2px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background-color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,592 +0,0 @@
|
|||||||
<!-- components/index/StencilWaterfall.vue -->
|
|
||||||
<template>
|
|
||||||
<div class="content">
|
|
||||||
<div ref="container" class="waterfall-container" :style="{
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
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" @click="openDetail(item)">
|
|
||||||
<div class="video-container" :style="{ aspectRatio: item.aspectRatio }"
|
|
||||||
@mouseenter="handleMouseEnter(index)" @mouseleave="handleMouseLeave(index)">
|
|
||||||
<video :ref="(el) => setVideoRef(index, el as HTMLVideoElement | null)" :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 { useRouter } from 'vue-router'
|
|
||||||
import { getVideos } from '@/apis/home'
|
|
||||||
import { transformFeedItem, transformShareElementItem } from '@/utlis/transform'
|
|
||||||
import type { FeedResponse, ShareElementsResponse } from '@/types/home'
|
|
||||||
import type { StencilItem } from '@/types/home'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
activeTab: string // 由父组件传入当前激活的 Tab
|
|
||||||
gap?: number
|
|
||||||
columnCount?: number
|
|
||||||
items?: StencilItem[]
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'like', item: StencilItem): void
|
|
||||||
(e: 'makeSame', item: StencilItem): void
|
|
||||||
(e: 'loadMore'): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const DETAIL_STORAGE_KEY = 'material-square-detail'
|
|
||||||
|
|
||||||
// ==================== 数据状态 ====================
|
|
||||||
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(() => undefined)
|
|
||||||
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 openDetail = (item: StencilItem) => {
|
|
||||||
sessionStorage.setItem(
|
|
||||||
DETAIL_STORAGE_KEY,
|
|
||||||
JSON.stringify({
|
|
||||||
selectedId: item.id,
|
|
||||||
items: items.value,
|
|
||||||
activeTab: props.activeTab,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
router.push({
|
|
||||||
path: '/material-square/detail',
|
|
||||||
query: {
|
|
||||||
id: item.id,
|
|
||||||
tab: props.activeTab,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 布局相关 ====================
|
|
||||||
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
|
|
||||||
|
|
||||||
const getColumnCount = (): number => {
|
|
||||||
if (props.columnCount !== undefined) return props.columnCount
|
|
||||||
const width = window.innerWidth
|
|
||||||
if (width >= 1280) return 4
|
|
||||||
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 [num, den] = item.aspectRatio.split('/').map(Number)
|
|
||||||
heights.push(cardWidth.value / (num / den))
|
|
||||||
}
|
|
||||||
|
|
||||||
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: props.activeTab
|
|
||||||
})
|
|
||||||
|
|
||||||
const cards: StencilItem[] = []
|
|
||||||
if (props.activeTab === '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 () => {
|
|
||||||
if (loading.value || !hasMore.value || !pageToken.value) return
|
|
||||||
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const params: { channel_type: string; page_token?: string } = {
|
|
||||||
channel_type: props.activeTab,
|
|
||||||
}
|
|
||||||
if (pageToken.value) params.page_token = pageToken.value
|
|
||||||
|
|
||||||
const res: FeedResponse | ShareElementsResponse = await getVideos(params)
|
|
||||||
|
|
||||||
const newCards: StencilItem[] = []
|
|
||||||
if (props.activeTab === 'share_elements') {
|
|
||||||
const shareRes = res as ShareElementsResponse
|
|
||||||
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
|
|
||||||
for (const item of feedRes.list) {
|
|
||||||
const card = transformFeedItem(item)
|
|
||||||
if (card) newCards.push(card)
|
|
||||||
}
|
|
||||||
pageToken.value = feedRes.next_page_token ?? null
|
|
||||||
}
|
|
||||||
|
|
||||||
items.value = [...items.value, ...newCards]
|
|
||||||
hasMore.value = !!pageToken.value
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载更多失败', error)
|
|
||||||
hasMore.value = false
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const setupObserver = () => {
|
|
||||||
if (intersectionObserver) intersectionObserver.disconnect()
|
|
||||||
if (!loadMoreTrigger.value) 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 响应式监听 ====================
|
|
||||||
// 监听 activeTab 变化,重置并重新加载
|
|
||||||
watch(
|
|
||||||
() => props.activeTab,
|
|
||||||
() => {
|
|
||||||
items.value = []
|
|
||||||
pageToken.value = null
|
|
||||||
hasMore.value = true
|
|
||||||
containerHeight.value = 0
|
|
||||||
itemsWithPosition.value = []
|
|
||||||
loadFirstPage()
|
|
||||||
if (container.value) container.value.scrollTop = 0
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => items.value,
|
|
||||||
() => nextTick(() => layout()),
|
|
||||||
{ deep: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => getColumnCount(),
|
|
||||||
() => layout()
|
|
||||||
)
|
|
||||||
|
|
||||||
let resizeTimer: number
|
|
||||||
const handleResize = () => {
|
|
||||||
if (resizeTimer) clearTimeout(resizeTimer)
|
|
||||||
resizeTimer = window.setTimeout(() => layout(), 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
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>
|
|
||||||
.content {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 视频容器 */
|
|
||||||
.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>
|
|
||||||
@ -1,523 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div class="top">
|
|
||||||
<!-- 左侧菜单(浮层,半透明) -->
|
|
||||||
<div class="top-left">
|
|
||||||
<div
|
|
||||||
class="left-item"
|
|
||||||
:class="{ 'is-active': currentFeatureType === 'reference' }"
|
|
||||||
>
|
|
||||||
<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"
|
|
||||||
:class="{ 'is-active': currentFeatureType === 'image' }"
|
|
||||||
>
|
|
||||||
<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 { computed, 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)
|
|
||||||
|
|
||||||
const currentFeatureType = computed<'reference' | 'image'>(() => {
|
|
||||||
const currentSlide = slides[currentIndex.value]
|
|
||||||
return currentSlide?.subtitle.includes('图生视频') ? 'image' : 'reference'
|
|
||||||
})
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-active {
|
|
||||||
background: linear-gradient(90deg, rgba(255, 255, 255, 0.14) 0%, rgba(255, 255, 255, 0.04) 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-active .detail {
|
|
||||||
opacity: 0.72;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
transition: background-color 0.2s, color 0.2s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.left-item.is-active {
|
|
||||||
.btn {
|
|
||||||
background-color: rgba(255, 255, 255, 0.28);
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.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: 4px;
|
|
||||||
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, transform 0.2s;
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: 0.8;
|
|
||||||
transform: translateX(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
margin: -8px 0;
|
|
||||||
z-index: 2;
|
|
||||||
// border: 2px solid rgba(18, 20, 24, 0.72);
|
|
||||||
|
|
||||||
&.play-btn {
|
|
||||||
background-color: rgba(255, 255, 255, 0.24);
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.play-btn svg {
|
|
||||||
transform: rotate(90deg);
|
|
||||||
transform-origin: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.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>
|
|
||||||
@ -1,335 +0,0 @@
|
|||||||
<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-item index="/TextGenerate">
|
|
||||||
<div class="menu-item-content">
|
|
||||||
<i class="iconfont icon-xiaoxi"></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="bill">
|
|
||||||
<div class="menu-item-content">
|
|
||||||
<i class="iconfont icon-xiaofeijilu"></i>
|
|
||||||
<span class="menu-text">消费记录</span>
|
|
||||||
</div>
|
|
||||||
</el-menu-item> -->
|
|
||||||
<el-menu-item index="history-creation">
|
|
||||||
<div class="menu-item-content">
|
|
||||||
<i class="iconfont icon-wodechuangzuo"></i>
|
|
||||||
<span class="menu-text">历史创作</span>
|
|
||||||
</div>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item index="apikey">
|
|
||||||
<div class="menu-item-content">
|
|
||||||
<i class="iconfont icon-apikey"></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, 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 'bill':
|
|
||||||
router.push('/bill');
|
|
||||||
break;
|
|
||||||
case 'history-creation':
|
|
||||||
router.push('/history-creation');
|
|
||||||
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 '/TextGenerate':
|
|
||||||
router.push('/TextGenerate');
|
|
||||||
break;
|
|
||||||
case 'digital-human':
|
|
||||||
router.push('/digital-human');
|
|
||||||
break;
|
|
||||||
case 'material-square':
|
|
||||||
router.push('/material-square');
|
|
||||||
break;
|
|
||||||
case 'apikey':
|
|
||||||
router.push('/apikey');
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听路由变化,更新激活状态
|
|
||||||
const updateActiveIndex = () => {
|
|
||||||
const path = route.path
|
|
||||||
if (path.includes('/index')) {
|
|
||||||
activeIndex.value = 'index';
|
|
||||||
} else if (path.includes('/history-creation')) {
|
|
||||||
activeIndex.value = 'history-creation';
|
|
||||||
} else if (path.includes('/bill')) {
|
|
||||||
activeIndex.value = 'bill';
|
|
||||||
} 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('/TextGenerate')) {
|
|
||||||
activeIndex.value = '/TextGenerate';
|
|
||||||
} else if (path.includes('/digital-human')) {
|
|
||||||
activeIndex.value = 'digital-human';
|
|
||||||
} else if (path.includes('/material-square')) {
|
|
||||||
activeIndex.value = 'material-square';
|
|
||||||
} else if (path.includes('/apikey')) {
|
|
||||||
activeIndex.value = 'apikey';
|
|
||||||
}
|
|
||||||
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>
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,307 +0,0 @@
|
|||||||
<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"
|
|
||||||
@task-created="handleTaskCreated"
|
|
||||||
/>
|
|
||||||
<ImgVideo
|
|
||||||
ref="imgVideoRef"
|
|
||||||
v-else-if="activeTab === 'image'"
|
|
||||||
@price-update="handleImagePriceUpdate"
|
|
||||||
@prompt-change="handleImagePromptChange"
|
|
||||||
@task-created="handleTaskCreated"
|
|
||||||
/>
|
|
||||||
<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, onUnmounted, 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';
|
|
||||||
import { debounce } from '@/utlis/debounce';
|
|
||||||
import { useLoginStore } from '@/store/LoginStore';
|
|
||||||
|
|
||||||
// ==================== 子组件暴露的方法类型定义 ====================
|
|
||||||
/** 文生视频组件暴露的方法 */
|
|
||||||
interface TextVideoExposed {
|
|
||||||
generate: () => Promise<boolean>;
|
|
||||||
resetForm: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 图生视频组件暴露的方法 */
|
|
||||||
interface ImgVideoExposed {
|
|
||||||
generate: () => Promise<boolean>;
|
|
||||||
resetForm: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 参考生视频组件暴露的方法 */
|
|
||||||
interface ReferenceVideoExposed {
|
|
||||||
generate?: () => Promise<boolean>;
|
|
||||||
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 loginStore = useLoginStore();
|
|
||||||
|
|
||||||
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 isCreating = ref(false);
|
|
||||||
|
|
||||||
// 价格更新处理
|
|
||||||
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(() => {
|
|
||||||
if (!loginStore.isLoggedIn || isCreating.value) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (activeTab.value) {
|
|
||||||
case 'reference': return isPromptEmpty.value;
|
|
||||||
case 'text': return isTextPromptEmpty.value;
|
|
||||||
case 'image': return isImagePromptEmpty.value;
|
|
||||||
default: return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 切换标签页(类型已明确)
|
|
||||||
const handleTabChange = debounce((tab: 'reference' | 'image' | 'text') => {
|
|
||||||
activeTab.value = tab;
|
|
||||||
router.push(`/videoGenerate/${tab}`);
|
|
||||||
}, 300);
|
|
||||||
|
|
||||||
// 创作按钮点击
|
|
||||||
const handleCreate = debounce(async () => {
|
|
||||||
if (!loginStore.isLoggedIn) {
|
|
||||||
ElMessage.warning('请先登录后再创作');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isCreating.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isCreateButtonDisabled.value) {
|
|
||||||
ElMessage.warning('请先填写必要信息');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isCreating.value = true;
|
|
||||||
let result = false;
|
|
||||||
try {
|
|
||||||
switch (activeTab.value) {
|
|
||||||
case 'text':
|
|
||||||
if (textVideoRef.value?.generate) {
|
|
||||||
result = await textVideoRef.value.generate();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'image':
|
|
||||||
if (imgVideoRef.value?.generate) {
|
|
||||||
result = await imgVideoRef.value.generate();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'reference':
|
|
||||||
if (referenceVideoRef.value?.generate) {
|
|
||||||
result = await referenceVideoRef.value.generate();
|
|
||||||
} else {
|
|
||||||
ElMessage.info('参考生视频功能暂未开放');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
ElMessage.info('该功能暂未开放');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
} finally {
|
|
||||||
isCreating.value = false;
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
const emit = defineEmits<{ (e: 'task-created', taskid: string): void }>();
|
|
||||||
|
|
||||||
const handleTaskCreated = (taskid: string) => {
|
|
||||||
emit('task-created', taskid);
|
|
||||||
switch (activeTab.value) {
|
|
||||||
case 'text':
|
|
||||||
textVideoRef.value?.resetForm?.();
|
|
||||||
break;
|
|
||||||
case 'image':
|
|
||||||
imgVideoRef.value?.resetForm?.();
|
|
||||||
break;
|
|
||||||
case 'reference':
|
|
||||||
referenceVideoRef.value?.resetForm?.();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 路由同步
|
|
||||||
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());
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
handleTabChange.cancel();
|
|
||||||
handleCreate.cancel();
|
|
||||||
});
|
|
||||||
</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;
|
|
||||||
pointer-events: none;
|
|
||||||
// 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;
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
.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>
|
|
||||||
@ -1,94 +0,0 @@
|
|||||||
<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,使用联合类型
|
|
||||||
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>
|
|
||||||
31
src/main.ts
31
src/main.ts
@ -1,31 +0,0 @@
|
|||||||
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'
|
|
||||||
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
|
||||||
// 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(pinia)
|
|
||||||
app.use(router)
|
|
||||||
app.use(ElementPlus, {
|
|
||||||
locale: zhCn,
|
|
||||||
})
|
|
||||||
|
|
||||||
router.isReady().then(() => {
|
|
||||||
app.mount('#app')
|
|
||||||
})
|
|
||||||
@ -1,137 +0,0 @@
|
|||||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
|
||||||
import GetCookie from '@/views/Cookie/getCookie.vue'
|
|
||||||
|
|
||||||
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: '/getCookie',
|
|
||||||
name: 'getCookie',
|
|
||||||
component: GetCookie
|
|
||||||
},
|
|
||||||
// 首页
|
|
||||||
{
|
|
||||||
path: '/Homeindex',
|
|
||||||
name: 'homeindex',
|
|
||||||
component: () => import('@/views/Homeindex.vue'),
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: '/index',
|
|
||||||
name: 'index',
|
|
||||||
component: () => import('@/views/index/index.vue')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/homePage/index',
|
|
||||||
name: 'homePageIndex',
|
|
||||||
component: () => import('@/views/index/index.vue')
|
|
||||||
},
|
|
||||||
// 消费记录
|
|
||||||
{
|
|
||||||
path: '/bill',
|
|
||||||
name: 'bill',
|
|
||||||
component: () => import('@/views/Bill/Bill.vue')
|
|
||||||
},
|
|
||||||
// 历史创作
|
|
||||||
{
|
|
||||||
path: '/history-creation',
|
|
||||||
name: 'history-creation',
|
|
||||||
component: () => import('@/views/HistoryCreation.vue')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/history-creation/detail/:id',
|
|
||||||
name: 'history-creation-detail',
|
|
||||||
component: () => import('@/views/HistoryCreationDetail.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')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'image',
|
|
||||||
name: 'ImgGenerateImage',
|
|
||||||
component: () => import('@/views/ImgGenerate/ImgGenerate.vue')
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/TextGenerate',
|
|
||||||
name: 'TextGenerate',
|
|
||||||
component: () => import('@/views/TextGenerate/TextGenerate.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')
|
|
||||||
},
|
|
||||||
// apikey
|
|
||||||
{
|
|
||||||
path: '/apikey',
|
|
||||||
name: 'apikey',
|
|
||||||
component: () => import('@/views/APiKey/apikey.vue')
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/material-square/detail',
|
|
||||||
name: 'material-square-detail',
|
|
||||||
component: () => import('@/views/materialSquare/Detail.vue')
|
|
||||||
},
|
|
||||||
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
export default router
|
|
||||||
@ -1,211 +0,0 @@
|
|||||||
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';
|
|
||||||
import { clearApiBaseURL } from '@/utlis/request';
|
|
||||||
|
|
||||||
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) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = Date.now();
|
|
||||||
const elapsed = now - loginTimestamp.value;
|
|
||||||
const remaining = Math.max(0, 20 * 60 * 1000 - elapsed);
|
|
||||||
|
|
||||||
autoLogoutTimer = setTimeout(() => {
|
|
||||||
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 syncLogin = (data: { user: LoginUser } | LoginUser) => {
|
|
||||||
const nextUserInfo = 'user' in data ? data : { user: data };
|
|
||||||
|
|
||||||
openAccounts();
|
|
||||||
isLoggedIn.value = true;
|
|
||||||
userInfo.value = nextUserInfo;
|
|
||||||
accountList.value = [];
|
|
||||||
showAccountSelect.value = false;
|
|
||||||
loginTimestamp.value = Date.now();
|
|
||||||
startAutoLogoutTimer();
|
|
||||||
|
|
||||||
const path = redirectPath.value || '/';
|
|
||||||
redirectPath.value = null;
|
|
||||||
router.push(path);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 外部接口只同步 cookie 时,先恢复前端登录态
|
|
||||||
const syncCookieLogin = (user?: Partial<LoginUser>) => {
|
|
||||||
openAccounts();
|
|
||||||
isLoggedIn.value = true;
|
|
||||||
userInfo.value = {
|
|
||||||
user: {
|
|
||||||
id: user?.id || '',
|
|
||||||
username: user?.username || user?.name || '已登录用户',
|
|
||||||
password: '',
|
|
||||||
email: user?.email || '',
|
|
||||||
orgid: user?.orgid || '',
|
|
||||||
nick_name: user?.nick_name || null,
|
|
||||||
address: user?.address || null,
|
|
||||||
mobile: user?.mobile || user?.cellphone || '',
|
|
||||||
user_status: user?.user_status || '',
|
|
||||||
name: user?.name,
|
|
||||||
cellphone: user?.cellphone,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
accountList.value = [];
|
|
||||||
showAccountSelect.value = false;
|
|
||||||
loginTimestamp.value = Date.now();
|
|
||||||
startAutoLogoutTimer();
|
|
||||||
|
|
||||||
const path = redirectPath.value || '/';
|
|
||||||
redirectPath.value = null;
|
|
||||||
router.push(path);
|
|
||||||
};
|
|
||||||
|
|
||||||
const logoutS = () => {
|
|
||||||
// 保存当前路径
|
|
||||||
redirectPath.value = router.currentRoute.value.path;
|
|
||||||
clearAutoLogoutTimer();
|
|
||||||
isLoggedIn.value = false;
|
|
||||||
userInfo.value = null;
|
|
||||||
accountList.value = [];
|
|
||||||
showAccountSelect.value = false;
|
|
||||||
loginTimestamp.value = null;
|
|
||||||
clearApiBaseURL();
|
|
||||||
router.push('/login');
|
|
||||||
ElMessage.error('身份验证过期请重新登录');
|
|
||||||
};
|
|
||||||
// 登出
|
|
||||||
const logout = () => {
|
|
||||||
clearAutoLogoutTimer();
|
|
||||||
isLoggedIn.value = false;
|
|
||||||
userInfo.value = null;
|
|
||||||
accountList.value = [];
|
|
||||||
showAccountSelect.value = false;
|
|
||||||
loginTimestamp.value = null;
|
|
||||||
clearApiBaseURL();
|
|
||||||
router.push('/login');
|
|
||||||
};
|
|
||||||
|
|
||||||
// 初始化自动登出(页面刷新后调用,恢复计时器)
|
|
||||||
const initAutoLogout = () => {
|
|
||||||
if (isLoggedIn.value) {
|
|
||||||
// 如果没有时间戳(可能因为持久化缺少),用当前时间作为基准
|
|
||||||
if (!loginTimestamp.value) {
|
|
||||||
loginTimestamp.value = Date.now();
|
|
||||||
}
|
|
||||||
startAutoLogoutTimer();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
isLoggedIn,
|
|
||||||
userInfo,
|
|
||||||
accountList,
|
|
||||||
showAccountSelect,
|
|
||||||
redirectPath,
|
|
||||||
Login,
|
|
||||||
loginWithSelectedAccount,
|
|
||||||
syncLogin,
|
|
||||||
syncCookieLogin,
|
|
||||||
logout,
|
|
||||||
logoutS,
|
|
||||||
initAutoLogout,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
{
|
|
||||||
persist: true, // 持久化登录状态、userInfo、loginTimestamp 等
|
|
||||||
}
|
|
||||||
);
|
|
||||||
@ -1,79 +0,0 @@
|
|||||||
: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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
export interface ListApikeys {
|
|
||||||
id: string;
|
|
||||||
name: null;
|
|
||||||
apikeyid: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface ListResult {
|
|
||||||
apikeys: Array< ListApikeys >;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface AddParams {
|
|
||||||
appname: string;
|
|
||||||
description: string;
|
|
||||||
}
|
|
||||||
export interface AddResult{
|
|
||||||
status: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DetailData {
|
|
||||||
id: string;
|
|
||||||
name: null;
|
|
||||||
description: null;
|
|
||||||
secretkey: string;
|
|
||||||
allowedips: null;
|
|
||||||
orgid: string;
|
|
||||||
apikey: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface DetailResult {
|
|
||||||
status: string;
|
|
||||||
data: DetailData;
|
|
||||||
};
|
|
||||||
@ -1,296 +0,0 @@
|
|||||||
// 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;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
createdAt: string;
|
|
||||||
contentType: string;
|
|
||||||
displayTag?: string;
|
|
||||||
collectCount?: number;
|
|
||||||
recreateCount?: number;
|
|
||||||
creatorId?: string;
|
|
||||||
averageColor?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 余额
|
|
||||||
interface BalanceData {
|
|
||||||
account: string;
|
|
||||||
balance: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface BalanceResult {
|
|
||||||
status: string;
|
|
||||||
data: Array< BalanceData >;
|
|
||||||
};
|
|
||||||
@ -1,54 +0,0 @@
|
|||||||
// 验证码返回数据
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
@ -1,309 +0,0 @@
|
|||||||
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; // 显示标签
|
|
||||||
lable?: string; // 兼容后端历史拼写
|
|
||||||
uitype: string; // 控件类型: code, text, int, audio 等
|
|
||||||
required?: boolean; // 是否必填
|
|
||||||
defaultvalue?: string | number | boolean | null; // 默认值
|
|
||||||
placeholder?: string; // 输入提示
|
|
||||||
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 TextToImageParams {
|
|
||||||
model: string;
|
|
||||||
catelogid?: string;
|
|
||||||
prompt: string;
|
|
||||||
negative_prompt?: string;
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TextToImageResponse {
|
|
||||||
taskid?: string;
|
|
||||||
status: string;
|
|
||||||
message?: string;
|
|
||||||
image_count?: number;
|
|
||||||
image?: string[];
|
|
||||||
usage?: {
|
|
||||||
image_count?: number;
|
|
||||||
input_tokens?: number;
|
|
||||||
output_tokens?: number;
|
|
||||||
size?: string;
|
|
||||||
total_tokens?: number;
|
|
||||||
[key: string]: unknown;
|
|
||||||
};
|
|
||||||
data?: {
|
|
||||||
taskid?: string;
|
|
||||||
image?: string[];
|
|
||||||
image_count?: number;
|
|
||||||
usage?: TextToImageResponse['usage'];
|
|
||||||
} | 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;
|
|
||||||
catelogid?: string;
|
|
||||||
duration: number;
|
|
||||||
ratio: string;
|
|
||||||
resolution: string;
|
|
||||||
size?: 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;
|
|
||||||
image_url?: string;
|
|
||||||
url?: string;
|
|
||||||
images?: 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;
|
|
||||||
image_url?: string;
|
|
||||||
cover_url?: string;
|
|
||||||
duration?: number;
|
|
||||||
resolution?: string;
|
|
||||||
error_msg?: string;
|
|
||||||
model?: string;
|
|
||||||
use_time?: string;
|
|
||||||
action?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export interface ModelParams {
|
|
||||||
llmcatelogid: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 文生文
|
|
||||||
export interface TextToTextParams {
|
|
||||||
model: string;
|
|
||||||
catelogid?: string;
|
|
||||||
prompt: string;
|
|
||||||
sys_prompt?: string;
|
|
||||||
stream: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TextToTextChoice {
|
|
||||||
delta?: {
|
|
||||||
content?: string;
|
|
||||||
};
|
|
||||||
message?: {
|
|
||||||
content?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TextToTextResponse {
|
|
||||||
status?: string;
|
|
||||||
content?: string;
|
|
||||||
answer?: string;
|
|
||||||
text?: string;
|
|
||||||
data?: {
|
|
||||||
content?: string;
|
|
||||||
answer?: string;
|
|
||||||
};
|
|
||||||
choices?: TextToTextChoice[];
|
|
||||||
}
|
|
||||||
// 历史记录
|
|
||||||
export interface HistoryRecordParams {
|
|
||||||
page: number;
|
|
||||||
pageSize: number;
|
|
||||||
llmcatelogid: string;
|
|
||||||
}
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
// utils/cookie.ts
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取指定名称的 Cookie 值
|
|
||||||
*/
|
|
||||||
export function getCookie(name: string): string | null {
|
|
||||||
const value = `; ${document.cookie}`;
|
|
||||||
const parts = value.split(`; ${name}=`);
|
|
||||||
if (parts.length === 2) {
|
|
||||||
const raw = parts.pop()?.split(';').shift();
|
|
||||||
return raw ? decodeURIComponent(raw) : null;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置 Cookie
|
|
||||||
* @param name 名称
|
|
||||||
* @param value 值
|
|
||||||
* @param days 有效期(天),不传则为会话 Cookie
|
|
||||||
*/
|
|
||||||
export function setCookie(name: string, value: string, days?: number): void {
|
|
||||||
let expires = '';
|
|
||||||
if (days) {
|
|
||||||
const date = new Date();
|
|
||||||
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
|
|
||||||
expires = `; expires=${date.toUTCString()}`;
|
|
||||||
}
|
|
||||||
// 注意:为了安全,实际项目中可根据需要加上 Secure / SameSite 等属性
|
|
||||||
document.cookie = `${name}=${encodeURIComponent(value)}${expires}; path=/`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除 Cookie
|
|
||||||
*/
|
|
||||||
export function deleteCookie(name: string): void {
|
|
||||||
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 调试:打印所有可读 Cookie(非 HttpOnly)
|
|
||||||
*/
|
|
||||||
export function logAllCookies(): void {
|
|
||||||
if (!document.cookie) return;
|
|
||||||
const cookies = document.cookie.split('; ').reduce((acc, item) => {
|
|
||||||
const eqIndex = item.indexOf('=');
|
|
||||||
const key = item.slice(0, eqIndex);
|
|
||||||
const value = item.slice(eqIndex + 1);
|
|
||||||
acc[key] = decodeURIComponent(value);
|
|
||||||
return acc;
|
|
||||||
}, {} as Record<string, string>);
|
|
||||||
console.table(cookies);
|
|
||||||
}
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
export type DebouncedFunction<T extends (...args: any[]) => any> = ((
|
|
||||||
...args: Parameters<T>
|
|
||||||
) => void) & {
|
|
||||||
cancel: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const debounce = <T extends (...args: any[]) => any>(
|
|
||||||
fn: T,
|
|
||||||
wait = 300,
|
|
||||||
immediate = true
|
|
||||||
): DebouncedFunction<T> => {
|
|
||||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
|
|
||||||
const debounced = ((...args: Parameters<T>) => {
|
|
||||||
const shouldCallNow = immediate && timer === null;
|
|
||||||
|
|
||||||
if (timer) {
|
|
||||||
clearTimeout(timer);
|
|
||||||
}
|
|
||||||
|
|
||||||
timer = setTimeout(() => {
|
|
||||||
timer = null;
|
|
||||||
|
|
||||||
if (!immediate) {
|
|
||||||
void fn(...args);
|
|
||||||
}
|
|
||||||
}, wait);
|
|
||||||
|
|
||||||
if (shouldCallNow) {
|
|
||||||
void fn(...args);
|
|
||||||
}
|
|
||||||
}) as DebouncedFunction<T>;
|
|
||||||
|
|
||||||
debounced.cancel = () => {
|
|
||||||
if (!timer) return;
|
|
||||||
|
|
||||||
clearTimeout(timer);
|
|
||||||
timer = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
return debounced;
|
|
||||||
};
|
|
||||||
@ -1,174 +0,0 @@
|
|||||||
import axios from 'axios'
|
|
||||||
import { ElMessage } from 'element-plus'
|
|
||||||
|
|
||||||
// 获取环境变量,添加默认值
|
|
||||||
const getDefaultApiBaseURL = () => {
|
|
||||||
const envUrl = import.meta.env.VITE_API_BASE_URL
|
|
||||||
|
|
||||||
// 开发环境如果没有配置,使用本地地址
|
|
||||||
if (import.meta.env.DEV && !envUrl) {
|
|
||||||
console.warn('⚠️ 未配置 VITE_API_BASE_URL,使用默认本地地址')
|
|
||||||
return 'http://localhost:5173' // Vite 默认端口
|
|
||||||
}
|
|
||||||
|
|
||||||
return envUrl || 'https://token.opencomputing.cn'
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultApiBaseURL = getDefaultApiBaseURL()
|
|
||||||
|
|
||||||
// 调试信息(只在开发环境显示)
|
|
||||||
if (import.meta.env.DEV) {
|
|
||||||
console.group('🔧 API 配置信息')
|
|
||||||
console.log('环境模式:', import.meta.env.MODE)
|
|
||||||
console.log('VITE_API_BASE_URL:', import.meta.env.VITE_API_BASE_URL)
|
|
||||||
console.log('最终使用的 API 地址:', defaultApiBaseURL)
|
|
||||||
console.log('是否开发环境:', import.meta.env.DEV)
|
|
||||||
console.groupEnd()
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiBaseURLStorageKey = 'yuanjing_api_base_url'
|
|
||||||
|
|
||||||
export const setApiBaseURL = (url: string) => {
|
|
||||||
localStorage.setItem(apiBaseURLStorageKey, url)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const clearApiBaseURL = () => {
|
|
||||||
localStorage.removeItem(apiBaseURLStorageKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getApiBaseURL = () => {
|
|
||||||
if (import.meta.env.DEV) {
|
|
||||||
return defaultApiBaseURL
|
|
||||||
}
|
|
||||||
|
|
||||||
return localStorage.getItem(apiBaseURLStorageKey) || defaultApiBaseURL
|
|
||||||
}
|
|
||||||
|
|
||||||
const shouldIncludeCredentials = (url?: string, baseURL?: string) => {
|
|
||||||
if (!url) return true
|
|
||||||
|
|
||||||
try {
|
|
||||||
const apiBaseURL = getApiBaseURL()
|
|
||||||
const apiOrigin = new URL(apiBaseURL).origin
|
|
||||||
const requestOrigin = new URL(url, baseURL || apiBaseURL).origin
|
|
||||||
return requestOrigin === apiOrigin
|
|
||||||
} catch {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 请求拦截器
|
|
||||||
axios.interceptors.request.use(
|
|
||||||
function (config) {
|
|
||||||
const apiBaseURL = getApiBaseURL()
|
|
||||||
config.baseURL = apiBaseURL
|
|
||||||
config.withCredentials = shouldIncludeCredentials(config.url, config.baseURL)
|
|
||||||
return config
|
|
||||||
},
|
|
||||||
function (error) {
|
|
||||||
// 对请求错误做些什么
|
|
||||||
return Promise.reject(error)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// 响应拦截器
|
|
||||||
axios.interceptors.response.use(
|
|
||||||
function (response) {
|
|
||||||
// 放行的状态列表
|
|
||||||
const allowedStatuses = [
|
|
||||||
'ok',
|
|
||||||
'CREATED',
|
|
||||||
'PENDING',
|
|
||||||
'RUNNING',
|
|
||||||
'SUCCEEDED',
|
|
||||||
'FAILED',
|
|
||||||
'created',
|
|
||||||
'processing',
|
|
||||||
'queueing',
|
|
||||||
'running',
|
|
||||||
'succeeded',
|
|
||||||
'completed',
|
|
||||||
'failed',
|
|
||||||
'PENDING'
|
|
||||||
];
|
|
||||||
|
|
||||||
// 检查响应数据中的状态
|
|
||||||
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'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('文件上传失败:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,114 +0,0 @@
|
|||||||
// 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,
|
|
||||||
title: 'title' in data ? data.title : data.tag || data.brief || '未命名作品',
|
|
||||||
description: data.detail || ('brief' in data ? data.brief : '') || '',
|
|
||||||
createdAt: data.created_at || '',
|
|
||||||
contentType: item.type,
|
|
||||||
displayTag: 'tag' in data ? data.tag : data.type,
|
|
||||||
collectCount: data.react_info?.collect_count ?? 0,
|
|
||||||
recreateCount: 'recreate_count' in data ? data.recreate_count : undefined,
|
|
||||||
creatorId: data.creator?.id ?? '',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 转换 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,
|
|
||||||
title: element.name || '未命名作品',
|
|
||||||
description: share.introduce || element.recaption?.description || '',
|
|
||||||
createdAt: share.created_at || element.created_at || '',
|
|
||||||
contentType: 'share_elements',
|
|
||||||
displayTag: share.category_display?.[0] || element.type || '素材',
|
|
||||||
collectCount: share.react_info?.collect_count ?? 0,
|
|
||||||
creatorId: element.creator?.id ?? '',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,364 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="apikey-page">
|
|
||||||
<LoginPrompt
|
|
||||||
v-if="!isLoggedIn"
|
|
||||||
class="login-fullscreen"
|
|
||||||
title="请先登录"
|
|
||||||
description="登录后可创建、复制、查看和删除自己的访问令牌。"
|
|
||||||
full-height
|
|
||||||
/>
|
|
||||||
|
|
||||||
<el-card v-else class="apikey-card">
|
|
||||||
<div class="page-header">
|
|
||||||
<div class="header-main">
|
|
||||||
<div class="title-row">
|
|
||||||
<i class="iconfont icon-apikey"></i>
|
|
||||||
<span>API Key 管理</span>
|
|
||||||
</div>
|
|
||||||
<p class="header-desc">创建和管理访问令牌,用于在外部系统中安全调用你的能力接口。</p>
|
|
||||||
</div>
|
|
||||||
<div class="header-actions">
|
|
||||||
<el-button class="refresh-btn" :loading="loading" @click="loadKeys">刷新</el-button>
|
|
||||||
<el-button type="primary" @click="openCreateDialog">创建新令牌</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="guide-card">
|
|
||||||
<div class="guide-section">
|
|
||||||
<div class="guide-title">API 密钥安全指南:</div>
|
|
||||||
<ul class="guide-list">
|
|
||||||
<li>请不要与他人共享您的 API Keys,暴露 API Key 可能会破坏您的帐户安全和访问的服务。</li>
|
|
||||||
<li>避免将 API Key 暴露在浏览器或其他客户端代码中。</li>
|
|
||||||
<li>为了保护您的帐户安全,一旦 API Keys 被发现泄露,平台发现后可能会将其禁用。</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stats-grid">
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-label">令牌总数</div>
|
|
||||||
<div class="stat-value">{{ stats.total }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-label">启用中</div>
|
|
||||||
<div class="stat-value success">{{ stats.active }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="table-wrapper">
|
|
||||||
<el-table :data="keys" class="apikey-table" stripe v-loading="loading">
|
|
||||||
<el-table-column prop="name" label="令牌名称" min-width="150" />
|
|
||||||
<!-- <el-table-column prop="note" label="备注" min-width="150" show-overflow-tooltip /> -->
|
|
||||||
<el-table-column label="id" min-width="220">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<code class="key-mask">{{ maskKey(row.key || '') }}</code>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" width="180" fixed="right">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-button link type="primary" size="small" @click="openViewDialog(row)">查看</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<!-- 创建 API Key 对话框 -->
|
|
||||||
<el-dialog v-model="createDialogVisible" title="创建 API Key" width="560px" destroy-on-close
|
|
||||||
class="create-key-dialog">
|
|
||||||
<el-form ref="formRef" :model="createForm" :rules="formRules" label-position="top">
|
|
||||||
<el-form-item label="令牌名称" prop="appname">
|
|
||||||
<el-input v-model="createForm.appname" placeholder="例如:前端开发环境 / 自动化脚本" maxlength="30" show-word-limit />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="备注">
|
|
||||||
<el-input v-model="createForm.description" type="textarea" :rows="3" maxlength="100" show-word-limit
|
|
||||||
placeholder="填写使用场景,方便后续区分" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<div class="dialog-footer">
|
|
||||||
<el-button @click="createDialogVisible = false">取消</el-button>
|
|
||||||
<el-button type="primary" :loading="createLoading" @click="createKey">确认创建</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<!-- 查看完整 Key 对话框 -->
|
|
||||||
<el-dialog v-model="viewDialogVisible" title="API Key 详情" width="560px" class="view-key-dialog" @close="handleViewDialogClose">
|
|
||||||
<div v-loading="detailLoading" class="dialog-content-wrapper">
|
|
||||||
<div class="view-key-content">
|
|
||||||
<div class="view-key-name">
|
|
||||||
<span class="label">令牌名称:</span>
|
|
||||||
<span class="value">{{ viewingKey?.name || '未命名' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="view-key-value">
|
|
||||||
<span class="label">完整 Key:</span>
|
|
||||||
<div class="key-box">
|
|
||||||
<code>{{ viewingKey?.key }}</code>
|
|
||||||
<el-button link type="primary" size="small" @click="copyViewingKey">
|
|
||||||
复制
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="view-key-tip">
|
|
||||||
<i class="el-icon-info"></i>
|
|
||||||
请妥善保管该密钥 ,切勿泄露。
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang='ts'>
|
|
||||||
import { computed, reactive, ref, watch, onMounted } from 'vue';
|
|
||||||
import { ElMessage } from 'element-plus';
|
|
||||||
import type { FormInstance, FormRules } from 'element-plus';
|
|
||||||
import { useLoginStore } from '@/store/LoginStore';
|
|
||||||
import { applyApiKey, getApiKeyList, getApiKeyDetail } from '@/apis/apiKey';
|
|
||||||
import type { AddParams, AddResult, DetailResult, ListApikeys } from '@/types/apiKey';
|
|
||||||
import LoginPrompt from '@/components/LoginPrompt/LoginPrompt.vue';
|
|
||||||
|
|
||||||
const loginStore = useLoginStore();
|
|
||||||
const isLoggedIn = computed(() => loginStore.isLoggedIn);
|
|
||||||
|
|
||||||
// 数据状态
|
|
||||||
const keys = ref<ListApikeys[]>([]);
|
|
||||||
const loading = ref(false);
|
|
||||||
|
|
||||||
// 统计数据
|
|
||||||
const stats = computed(() => {
|
|
||||||
const total = keys.value.length;
|
|
||||||
const active = keys.value.length; // 接口未返回状态字段,默认所有令牌为启用状态
|
|
||||||
return { total, active };
|
|
||||||
});
|
|
||||||
|
|
||||||
// 创建对话框相关
|
|
||||||
const createDialogVisible = ref(false);
|
|
||||||
const createLoading = ref(false);
|
|
||||||
const formRef = ref<FormInstance>();
|
|
||||||
const createForm = reactive<AddParams>({
|
|
||||||
appname: '',
|
|
||||||
description: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const formRules: FormRules = {
|
|
||||||
appname: [
|
|
||||||
{ required: true, message: '请输入令牌名称', trigger: 'blur' },
|
|
||||||
{ max: 30, message: '名称不能超过30个字符', trigger: 'blur' }
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
// 查看对话框相关
|
|
||||||
const viewDialogVisible = ref(false);
|
|
||||||
const viewingKey = ref<any>(null);
|
|
||||||
const detailLoading = ref(false);
|
|
||||||
|
|
||||||
// 工具函数:掩码处理 API Key
|
|
||||||
const maskKey = (key: string) => {
|
|
||||||
if (!key || key.length <= 8) return '****';
|
|
||||||
const start = key.slice(0, 6);
|
|
||||||
const end = key.slice(-4);
|
|
||||||
return `${start}****${end}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const copyToClipboard = async (text: string, successMsg = '复制成功') => {
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(text);
|
|
||||||
ElMessage.success(successMsg);
|
|
||||||
} catch (err) {
|
|
||||||
ElMessage.error('复制失败,请手动复制');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 加载列表(适配接口返回结构)
|
|
||||||
const loadKeys = async () => {
|
|
||||||
if (!isLoggedIn.value) return;
|
|
||||||
loading.value = true;
|
|
||||||
try {
|
|
||||||
const res = await getApiKeyList<any>();
|
|
||||||
// 适配实际返回结构:{ status: 'ok', data: { apikeys: [...] } }
|
|
||||||
const apikeys = res.data?.apikeys || res.apikeys || [];
|
|
||||||
keys.value = apikeys.map((item: any) => ({
|
|
||||||
id: item.id,
|
|
||||||
name: item.name || '未命名',
|
|
||||||
note: item.description || '暂无备注', // 后端可能用 description 表示备注
|
|
||||||
key: item.apikeyid || '', // 列表中的 key 可能是掩码或空
|
|
||||||
status: 'active',
|
|
||||||
createdAt: new Date().toISOString()
|
|
||||||
}));
|
|
||||||
} catch (error: any) {
|
|
||||||
ElMessage.error(error?.message || '获取 API Key 列表失败');
|
|
||||||
keys.value = [];
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 打开创建对话框
|
|
||||||
const openCreateDialog = () => {
|
|
||||||
createForm.appname = '';
|
|
||||||
createForm.description = '';
|
|
||||||
formRef.value?.clearValidate();
|
|
||||||
createDialogVisible.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 创建 API Key
|
|
||||||
const createKey = async () => {
|
|
||||||
if (!formRef.value) return;
|
|
||||||
await formRef.value.validate();
|
|
||||||
createLoading.value = true;
|
|
||||||
try {
|
|
||||||
const res = await applyApiKey<AddResult, AddParams>({
|
|
||||||
appname: createForm.appname,
|
|
||||||
description: createForm.description,
|
|
||||||
});
|
|
||||||
if (res.status === 'ok') {
|
|
||||||
createDialogVisible.value = false;
|
|
||||||
await loadKeys();
|
|
||||||
ElMessage.success('创建成功,点击查看可复制完整 Key');
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
ElMessage.error(error?.message || '创建失败');
|
|
||||||
} finally {
|
|
||||||
createLoading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 查看完整 Key(调用详情接口)
|
|
||||||
const openViewDialog = async (row: ListApikeys) => {
|
|
||||||
if (!row.id) {
|
|
||||||
ElMessage.error('令牌 ID 无效');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
detailLoading.value = true;
|
|
||||||
viewDialogVisible.value = false; // 先关闭旧对话框,避免残留数据
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await getApiKeyDetail<DetailResult, { id: string }>({ id: row.id });
|
|
||||||
|
|
||||||
if (res.status === 'ok' && res.data) {
|
|
||||||
// 根据实际接口返回字段构建 viewingKey(字段名 apikey 为完整密钥)
|
|
||||||
viewingKey.value = {
|
|
||||||
...row,
|
|
||||||
key: res.data.apikey || '',
|
|
||||||
name: res.data.name || row.name || '未命名',
|
|
||||||
note: res.data.description || '暂无备注',
|
|
||||||
};
|
|
||||||
viewDialogVisible.value = true;
|
|
||||||
} else {
|
|
||||||
ElMessage.error('获取 API Key 详情失败');
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('获取详情异常:', error);
|
|
||||||
ElMessage.error(error?.message || '网络错误,请稍后重试');
|
|
||||||
} finally {
|
|
||||||
detailLoading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 查看对话框内复制完整 Key
|
|
||||||
const copyViewingKey = () => {
|
|
||||||
if (viewingKey.value?.key) {
|
|
||||||
copyToClipboard(viewingKey.value.key, '完整 API Key 已复制');
|
|
||||||
} else {
|
|
||||||
ElMessage.warning('无可复制的 Key');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 关闭查看对话框时重置数据
|
|
||||||
const handleViewDialogClose = () => {
|
|
||||||
viewDialogVisible.value = false;
|
|
||||||
viewingKey.value = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 监听登录状态变化
|
|
||||||
watch(
|
|
||||||
() => loginStore.isLoggedIn,
|
|
||||||
(loggedIn) => {
|
|
||||||
if (loggedIn) {
|
|
||||||
loadKeys();
|
|
||||||
} else {
|
|
||||||
keys.value = [];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (loginStore.isLoggedIn) {
|
|
||||||
loadKeys();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<style scoped lang='less'>
|
|
||||||
@import url('@/assets/less/apiKey/apikey.less');
|
|
||||||
|
|
||||||
.login-fullscreen {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style lang='less'>
|
|
||||||
.apikey-table.el-table {
|
|
||||||
--el-table-bg-color: #1e2024;
|
|
||||||
--el-table-tr-bg-color: #1e2024;
|
|
||||||
--el-table-row-hover-bg-color: #25282e;
|
|
||||||
--el-table-current-row-bg-color: #25282e;
|
|
||||||
--el-table-header-bg-color: #1e2024;
|
|
||||||
--el-table-border-color: #2a2d32;
|
|
||||||
--el-table-text-color: rgba(255, 255, 255, 0.88);
|
|
||||||
--el-table-header-text-color: rgba(255, 255, 255, 0.75);
|
|
||||||
--el-table-fixed-box-shadow: none;
|
|
||||||
background: #1e2024;
|
|
||||||
}
|
|
||||||
|
|
||||||
.apikey-table.el-table::before,
|
|
||||||
.apikey-table .el-table__inner-wrapper::before,
|
|
||||||
.apikey-table .el-table__fixed-right-patch {
|
|
||||||
background-color: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.apikey-table .el-table__header-wrapper,
|
|
||||||
.apikey-table .el-table__body-wrapper,
|
|
||||||
.apikey-table .el-table__inner-wrapper,
|
|
||||||
.apikey-table .el-table__fixed,
|
|
||||||
.apikey-table .el-table__fixed-right,
|
|
||||||
.apikey-table td.el-table__cell,
|
|
||||||
.apikey-table th.el-table__cell {
|
|
||||||
background: #1e2024 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.apikey-table .el-table__row--striped td.el-table__cell {
|
|
||||||
background: #1a1c20 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.apikey-table .el-table__row:hover td.el-table__cell {
|
|
||||||
background: #25282e !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-select-popup .el-select-dropdown__item.selected,
|
|
||||||
.custom-select-popup .el-select-dropdown__item:hover {
|
|
||||||
background-color: #3a3d46 !important;
|
|
||||||
}
|
|
||||||
.el-input__count-inner{
|
|
||||||
background-color: #25282e !important;
|
|
||||||
}
|
|
||||||
.el-input__count{
|
|
||||||
background-color: #25282e !important;
|
|
||||||
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,822 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="bill-container">
|
|
||||||
<LoginPrompt
|
|
||||||
v-if="!isLoggedIn"
|
|
||||||
class="bill-login-prompt"
|
|
||||||
title="请先登录"
|
|
||||||
description="登录后可查看模型调用和消费记录。"
|
|
||||||
full-height
|
|
||||||
/>
|
|
||||||
|
|
||||||
<el-card v-else class="bill-card">
|
|
||||||
<!-- 头部筛选区 -->
|
|
||||||
<div class="bill-header">
|
|
||||||
<div class="header-title">
|
|
||||||
<i class="iconfont icon-xiaofeijilu"></i>
|
|
||||||
<span>消费记录</span>
|
|
||||||
</div>
|
|
||||||
<div class="header-filters">
|
|
||||||
<el-select
|
|
||||||
v-model="statusFilter"
|
|
||||||
placeholder="订单状态"
|
|
||||||
size="default"
|
|
||||||
@change="handleFilterChange"
|
|
||||||
popper-class="custom-select-popup"
|
|
||||||
class="status-select"
|
|
||||||
>
|
|
||||||
<el-option label="全部状态" value="" />
|
|
||||||
<el-option label="成功" value="SUCCEEDED" />
|
|
||||||
<el-option label="失败" value="FAILED" />
|
|
||||||
<el-option label="运行中" value="RUNNING" />
|
|
||||||
<el-option label="处理中" value="PROCESSING" />
|
|
||||||
<el-option label="已创建" value="CREATED" />
|
|
||||||
<el-option label="排队中" value="QUEUING" />
|
|
||||||
<el-option label="未知错误" value="UNKNOWN" />
|
|
||||||
</el-select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 内容区 -->
|
|
||||||
<div class="bill-content">
|
|
||||||
<!-- 加载中 -->
|
|
||||||
<div v-if="isLoading" class="loading-container">
|
|
||||||
<div class="loading-spinner">
|
|
||||||
<div class="spinner-circle"></div>
|
|
||||||
</div>
|
|
||||||
<div class="loading-text">加载中...</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 空数据 -->
|
|
||||||
<div v-else-if="!orderList.length" class="empty-container">
|
|
||||||
<img src="../../assets/images/preparation.png" alt="">
|
|
||||||
<div class="empty-text">暂无消费记录</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 订单列表 -->
|
|
||||||
<div v-else class="order-list">
|
|
||||||
<div class="order-item" v-for="order in orderList" :key="order.row_num_">
|
|
||||||
<!-- 订单头部 -->
|
|
||||||
<div class="order-header">
|
|
||||||
<div class="order-model">
|
|
||||||
<span class="label">模型:</span>
|
|
||||||
<span class="value">{{ order.model }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="order-status">
|
|
||||||
<span :class="['status-badge', getStatusClass(order.status)]">
|
|
||||||
{{ getStatusText(order.status) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 订单内容 -->
|
|
||||||
<div class="order-body">
|
|
||||||
<div class="order-info">
|
|
||||||
<div class="info-row">
|
|
||||||
<span class="info-label">使用时间:</span>
|
|
||||||
<span class="info-value">{{ order.use_time }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-row">
|
|
||||||
<span class="info-label">模型ID:</span>
|
|
||||||
<span class="info-value">{{ order.llmid }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-row">
|
|
||||||
<span class="info-label">模型代码:</span>
|
|
||||||
<span class="info-value">{{ order.model }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 金额信息 -->
|
|
||||||
<div class="order-amount">
|
|
||||||
<div class="amount-label">消费金额</div>
|
|
||||||
<div class="amount-value">
|
|
||||||
<span class="currency">¥</span>
|
|
||||||
<span class="number">{{ order.amount !== null && order.amount !== undefined ? order.amount.toFixed(2) : '-' }}</span>
|
|
||||||
<span class="unit" v-if="order.amount !== null && order.amount !== undefined">元</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 订单底部 -->
|
|
||||||
<div class="order-footer">
|
|
||||||
<div class="footer-info">
|
|
||||||
<span class="info-item">使用日期:{{ order.use_date }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 分页 -->
|
|
||||||
<div v-if="orderList.length" class="pagination-container">
|
|
||||||
<el-pagination
|
|
||||||
v-model:current-page="currentPage"
|
|
||||||
v-model:page-size="pageSize"
|
|
||||||
:page-sizes="[5, 10, 20, 30,50]"
|
|
||||||
:total="total"
|
|
||||||
layout="total, sizes, prev, pager, next, jumper"
|
|
||||||
popper-class="bill-pagination-popper"
|
|
||||||
@size-change="handleSizeChange"
|
|
||||||
@current-change="handleCurrentChange"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, onMounted, computed, watch } from 'vue';
|
|
||||||
import { ElMessage } from 'element-plus';
|
|
||||||
import { consumptionRecord } from '@/apis/home';
|
|
||||||
import { useLoginStore } from '@/store/LoginStore';
|
|
||||||
import LoginPrompt from '@/components/LoginPrompt/LoginPrompt.vue';
|
|
||||||
|
|
||||||
// ==================== 实例和状态 ====================
|
|
||||||
const loginStore = useLoginStore();
|
|
||||||
|
|
||||||
// ==================== 响应式数据 ====================
|
|
||||||
/** 全部订单列表(原始数据) */
|
|
||||||
const allOrderList = ref<any[]>([]);
|
|
||||||
|
|
||||||
/** 当前页显示的订单列表(分页后) */
|
|
||||||
const orderList = ref<any[]>([]);
|
|
||||||
|
|
||||||
/** 加载状态 */
|
|
||||||
const isLoading = ref(false);
|
|
||||||
|
|
||||||
/** 当前页码 */
|
|
||||||
const currentPage = ref(1);
|
|
||||||
|
|
||||||
/** 每页条数 */
|
|
||||||
const pageSize = ref(5);
|
|
||||||
|
|
||||||
/** 总条数 */
|
|
||||||
const total = ref(0);
|
|
||||||
|
|
||||||
/** 状态筛选 */
|
|
||||||
const statusFilter = ref('');
|
|
||||||
|
|
||||||
|
|
||||||
// ==================== 计算属性 ====================
|
|
||||||
/** 登录状态 */
|
|
||||||
const isLoggedIn = computed(() => loginStore.isLoggedIn);
|
|
||||||
|
|
||||||
const resetOrderState = () => {
|
|
||||||
allOrderList.value = [];
|
|
||||||
orderList.value = [];
|
|
||||||
total.value = 0;
|
|
||||||
currentPage.value = 1;
|
|
||||||
statusFilter.value = '';
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新当前页显示的数据(前端分页)
|
|
||||||
*/
|
|
||||||
const updatePaginatedList = () => {
|
|
||||||
// 先根据状态筛选
|
|
||||||
let filteredList = allOrderList.value;
|
|
||||||
|
|
||||||
if (statusFilter.value) {
|
|
||||||
filteredList = allOrderList.value.filter(order => {
|
|
||||||
const status = (order.status || '').toUpperCase();
|
|
||||||
return status === statusFilter.value.toUpperCase();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新总数为筛选后的数量
|
|
||||||
total.value = filteredList.length;
|
|
||||||
|
|
||||||
// 如果当前页码超出总页数,重置为第一页
|
|
||||||
const maxPage = Math.ceil(total.value / pageSize.value) || 1;
|
|
||||||
if (currentPage.value > maxPage) {
|
|
||||||
currentPage.value = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 分页
|
|
||||||
const startIndex = (currentPage.value - 1) * pageSize.value;
|
|
||||||
const endIndex = startIndex + pageSize.value;
|
|
||||||
orderList.value = filteredList.slice(startIndex, endIndex);
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
// ==================== 事件处理 ====================
|
|
||||||
/**
|
|
||||||
* 获取状态显示文本
|
|
||||||
*/
|
|
||||||
const getStatusText = (status: string): string => {
|
|
||||||
const statusMap: Record<string, string> = {
|
|
||||||
'SUCCEEDED': '成功',
|
|
||||||
'SUCCESS': '成功',
|
|
||||||
'succeeded': '成功',
|
|
||||||
'success': '成功',
|
|
||||||
'FAILED': '失败',
|
|
||||||
'failed': '失败',
|
|
||||||
'ERROR': '失败',
|
|
||||||
'error': '失败',
|
|
||||||
'RUNNING': '运行中',
|
|
||||||
'running': '运行中',
|
|
||||||
'PROCESSING': '处理中',
|
|
||||||
'processing': '处理中',
|
|
||||||
'CREATED': '已创建',
|
|
||||||
'created': '已创建',
|
|
||||||
'PENDING': '排队中',
|
|
||||||
'pending': '排队中',
|
|
||||||
'QUEUING': '排队中',
|
|
||||||
'queueing': '排队中',
|
|
||||||
'UNKNOWN': '未知错误',
|
|
||||||
'unknown': '未知错误'
|
|
||||||
};
|
|
||||||
return statusMap[status] || '未知错误';
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取状态样式类名
|
|
||||||
*/
|
|
||||||
const getStatusClass = (status: string): string => {
|
|
||||||
const classMap: Record<string, string> = {
|
|
||||||
'SUCCEEDED': 'status-succeeded',
|
|
||||||
'SUCCESS': 'status-succeeded',
|
|
||||||
'succeeded': 'status-succeeded',
|
|
||||||
'success': 'status-succeeded',
|
|
||||||
'FAILED': 'status-failed',
|
|
||||||
'failed': 'status-failed',
|
|
||||||
'ERROR': 'status-failed',
|
|
||||||
'error': 'status-failed',
|
|
||||||
'RUNNING': 'status-running',
|
|
||||||
'running': 'status-running',
|
|
||||||
'PROCESSING': 'status-processing',
|
|
||||||
'processing': 'status-processing',
|
|
||||||
'CREATED': 'status-created',
|
|
||||||
'created': 'status-created',
|
|
||||||
'PENDING': 'status-queueing',
|
|
||||||
'pending': 'status-queueing',
|
|
||||||
'QUEUING': 'status-queueing',
|
|
||||||
'queueing': 'status-queueing',
|
|
||||||
'UNKNOWN': 'status-unknown',
|
|
||||||
'unknown': 'status-unknown'
|
|
||||||
};
|
|
||||||
return classMap[status] || 'status-unknown';
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 每页条数变化
|
|
||||||
*/
|
|
||||||
const handleSizeChange = (size: number) => {
|
|
||||||
pageSize.value = size;
|
|
||||||
currentPage.value = 1; // 切换每页条数通常重置为第一页
|
|
||||||
updatePaginatedList(); // 前端分页,更新显示数据
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 当前页码变化
|
|
||||||
*/
|
|
||||||
const handleCurrentChange = (page: number) => {
|
|
||||||
currentPage.value = page;
|
|
||||||
updatePaginatedList(); // 前端分页,更新显示数据
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 筛选条件变化
|
|
||||||
*/
|
|
||||||
const handleFilterChange = () => {
|
|
||||||
currentPage.value = 1; // 筛选变化时重置为第一页
|
|
||||||
updatePaginatedList(); // 重新筛选和分页
|
|
||||||
};
|
|
||||||
|
|
||||||
// ==================== 数据加载 ====================
|
|
||||||
/**
|
|
||||||
* 加载订单列表
|
|
||||||
*/
|
|
||||||
const loadOrderList = async () => {
|
|
||||||
if (!isLoggedIn.value) {
|
|
||||||
resetOrderState();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoading.value = true;
|
|
||||||
try {
|
|
||||||
// 不传递分页参数,获取所有数据
|
|
||||||
const res = await consumptionRecord<any, any>({});
|
|
||||||
|
|
||||||
if (res.status === 'ok' && res.data) {
|
|
||||||
const rawData = res.data.rows || [];
|
|
||||||
allOrderList.value = rawData; // 存储所有数据
|
|
||||||
total.value = rawData.length || 0; // 使用实际数据长度作为总数
|
|
||||||
|
|
||||||
// 更新当前页显示的数据
|
|
||||||
updatePaginatedList();
|
|
||||||
} else {
|
|
||||||
resetOrderState();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('=== 获取消费记录失败 ===');
|
|
||||||
console.error('错误详情:', error);
|
|
||||||
ElMessage.error('获取消费记录失败');
|
|
||||||
resetOrderState();
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ==================== 生命周期 ====================
|
|
||||||
onMounted(() => {
|
|
||||||
if (isLoggedIn.value) {
|
|
||||||
loadOrderList();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => loginStore.isLoggedIn,
|
|
||||||
(loggedIn) => {
|
|
||||||
if (loggedIn) {
|
|
||||||
loadOrderList();
|
|
||||||
} else {
|
|
||||||
resetOrderState();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="less">
|
|
||||||
|
|
||||||
:deep(.el-select__wrapper){
|
|
||||||
background-color: #1e2024!important;
|
|
||||||
border: none!important;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.bill-container {
|
|
||||||
height: 100%;
|
|
||||||
padding-bottom: 40px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
// background-color: #1a1c20; // 主背景色
|
|
||||||
}
|
|
||||||
|
|
||||||
.bill-card {
|
|
||||||
height: 100%;
|
|
||||||
background: #1e2024; // 卡片背景稍亮
|
|
||||||
border: 1px solid #2a2d32;
|
|
||||||
border-radius: 16px;
|
|
||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
|
|
||||||
:deep(.el-card__body) {
|
|
||||||
padding: 28px 32px;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.bill-login-prompt {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 头部
|
|
||||||
.bill-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 28px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
.header-title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
font-size: 22px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #fff;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
|
|
||||||
.iconfont {
|
|
||||||
font-size: 26px;
|
|
||||||
color: #00c4b3; // 主色调整为深青
|
|
||||||
filter: drop-shadow(0 0 8px rgba(0, 196, 179, 0.5));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-filters {
|
|
||||||
.status-select {
|
|
||||||
width: 160px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-input__wrapper) {
|
|
||||||
background-color: #25282e;
|
|
||||||
border: 1px solid #3a3e45;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: none;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: #00c4b3;
|
|
||||||
box-shadow: 0 0 8px rgba(0, 196, 179, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-focus {
|
|
||||||
border-color: #00c4b3;
|
|
||||||
box-shadow: 0 0 12px rgba(0, 196, 179, 0.25);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-input__inner) {
|
|
||||||
color: #e0e0e0;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-input__suffix) {
|
|
||||||
color: #00c4b3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// 内容区
|
|
||||||
.bill-content {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载中
|
|
||||||
.loading-container {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-spinner {
|
|
||||||
width: 52px;
|
|
||||||
height: 52px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spinner-circle {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border: 4px solid rgba(0, 196, 179, 0.1);
|
|
||||||
border-top-color: #00c4b3;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 0.8s linear infinite;
|
|
||||||
box-shadow: 0 0 12px rgba(0, 196, 179, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
0% { transform: rotate(0deg); }
|
|
||||||
100% { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-text {
|
|
||||||
font-size: 15px;
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 空数据
|
|
||||||
.empty-container {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 20px;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 220px;
|
|
||||||
height: 220px;
|
|
||||||
object-fit: contain;
|
|
||||||
opacity: 0.5;
|
|
||||||
filter: grayscale(30%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-text {
|
|
||||||
font-size: 17px;
|
|
||||||
color: rgba(255, 255, 255, 0.6);
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 订单列表
|
|
||||||
.order-list {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding-right: 12px;
|
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
width: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-track {
|
|
||||||
background: #14161a;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
background: #3a3e45;
|
|
||||||
border-radius: 4px;
|
|
||||||
transition: all 0.3s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: #00c4b3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.order-item {
|
|
||||||
background: #1e2024;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 24px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
border: 1px solid #2a2d32;
|
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 订单头部
|
|
||||||
.order-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
padding-bottom: 16px;
|
|
||||||
border-bottom: 1px solid #2a2d32;
|
|
||||||
|
|
||||||
.order-model {
|
|
||||||
font-size: 15px;
|
|
||||||
color: rgba(255, 255, 255, 0.9);
|
|
||||||
|
|
||||||
.label {
|
|
||||||
color: rgba(255, 255, 255, 0.5);
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.value {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge {
|
|
||||||
padding: 6px 14px;
|
|
||||||
border-radius: 20px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
|
|
||||||
&.status-succeeded {
|
|
||||||
background-color: rgba(0, 196, 179, 0.12);
|
|
||||||
color: #7fddd3;
|
|
||||||
border: 1px solid rgba(0, 196, 179, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.status-processing {
|
|
||||||
background-color: rgba(33, 150, 243, 0.12);
|
|
||||||
color: #90caf9;
|
|
||||||
border: 1px solid rgba(33, 150, 243, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.status-running {
|
|
||||||
background-color: rgba(156, 39, 176, 0.12);
|
|
||||||
color: #ce93d8;
|
|
||||||
border: 1px solid rgba(156, 39, 176, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.status-created {
|
|
||||||
background-color: rgba(158, 158, 158, 0.12);
|
|
||||||
color: #e0e0e0;
|
|
||||||
border: 1px solid rgba(158, 158, 158, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.status-failed {
|
|
||||||
background-color: rgba(244, 67, 54, 0.12);
|
|
||||||
color: #ef9a9a;
|
|
||||||
border: 1px solid rgba(244, 67, 54, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.status-queueing {
|
|
||||||
background-color: rgba(255, 193, 7, 0.12);
|
|
||||||
color: #ffe082;
|
|
||||||
border: 1px solid rgba(255, 193, 7, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.status-unknown {
|
|
||||||
background-color: rgba(255, 87, 34, 0.12);
|
|
||||||
color: #ffab91;
|
|
||||||
border: 1px solid rgba(255, 87, 34, 0.3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 订单内容
|
|
||||||
.order-body {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
gap: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.order-info {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-row {
|
|
||||||
display: flex;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
font-size: 14px;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-label {
|
|
||||||
color: rgba(255, 255, 255, 0.55);
|
|
||||||
width: 80px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-value {
|
|
||||||
color: rgba(255, 255, 255, 0.9);
|
|
||||||
flex: 1;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.order-amount {
|
|
||||||
text-align: right;
|
|
||||||
flex-shrink: 0;
|
|
||||||
min-width: 140px;
|
|
||||||
|
|
||||||
.amount-label {
|
|
||||||
font-size: 13px;
|
|
||||||
color: rgba(255, 255, 255, 0.6);
|
|
||||||
margin-bottom: 6px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.amount-value {
|
|
||||||
font-size: 28px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #00c4b3;
|
|
||||||
text-shadow: 0 0 12px rgba(0, 196, 179, 0.4);
|
|
||||||
|
|
||||||
.currency {
|
|
||||||
font-size: 18px;
|
|
||||||
margin-right: 4px;
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.number {
|
|
||||||
font-family: 'DIN Alternate', 'Courier New', monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.unit {
|
|
||||||
margin-left: 4px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 订单底部
|
|
||||||
.order-footer {
|
|
||||||
padding-top: 16px;
|
|
||||||
border-top: 1px solid #2a2d32;
|
|
||||||
|
|
||||||
.footer-info {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 13px;
|
|
||||||
color: rgba(255, 255, 255, 0.5);
|
|
||||||
|
|
||||||
.info-item {
|
|
||||||
margin-right: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.separator {
|
|
||||||
margin: 0 12px;
|
|
||||||
color: rgba(255, 255, 255, 0.3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 分页
|
|
||||||
.pagination-container {
|
|
||||||
margin-top: 28px;
|
|
||||||
padding-top: 20px;
|
|
||||||
border-top: 1px solid #2a2d32;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
:deep(.el-pagination) {
|
|
||||||
--el-pagination-bg-color: transparent;
|
|
||||||
--el-pagination-text-color: rgba(235, 244, 255, 0.9);
|
|
||||||
--el-pagination-button-disabled-bg-color: transparent;
|
|
||||||
--el-pagination-button-bg-color: transparent;
|
|
||||||
--el-pagination-hover-color: #94d8ff;
|
|
||||||
color: rgba(235, 244, 255, 0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-pagination__total),
|
|
||||||
:deep(.el-pagination__jump) {
|
|
||||||
color: rgba(235, 244, 255, 0.86);
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-pager li),
|
|
||||||
:deep(.btn-prev),
|
|
||||||
:deep(.btn-next) {
|
|
||||||
color: rgba(235, 244, 255, 0.74);
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-pager li.is-active) {
|
|
||||||
color: #94d8ff;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-select__wrapper),
|
|
||||||
:deep(.el-input__wrapper) {
|
|
||||||
border-radius: 10px;
|
|
||||||
background: rgba(15, 24, 36, 0.82);
|
|
||||||
box-shadow: 0 0 0 1px rgba(148, 216, 255, 0.16) inset;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-select__selected-item),
|
|
||||||
:deep(.el-input__inner) {
|
|
||||||
color: rgba(235, 244, 255, 0.92);
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-select__caret) {
|
|
||||||
color: rgba(235, 244, 255, 0.58);
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-select__wrapper:hover),
|
|
||||||
:deep(.el-input__wrapper:hover) {
|
|
||||||
box-shadow: 0 0 0 1px rgba(148, 216, 255, 0.34) inset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bill-pagination-popper {
|
|
||||||
border: 1px solid rgba(148, 216, 255, 0.18) !important;
|
|
||||||
border-radius: 10px !important;
|
|
||||||
background: rgba(15, 24, 36, 0.96) !important;
|
|
||||||
box-shadow: 0 14px 34px rgba(0, 0, 0, 0.32) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bill-pagination-popper .el-select-dropdown__list {
|
|
||||||
padding: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bill-pagination-popper .el-select-dropdown__item {
|
|
||||||
border-radius: 8px;
|
|
||||||
color: rgba(235, 244, 255, 0.78);
|
|
||||||
}
|
|
||||||
|
|
||||||
.bill-pagination-popper .el-select-dropdown__item.is-selected {
|
|
||||||
color: #94d8ff;
|
|
||||||
background: rgba(148, 216, 255, 0.12);
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bill-pagination-popper .el-select-dropdown__item.hover,
|
|
||||||
.bill-pagination-popper .el-select-dropdown__item:hover {
|
|
||||||
color: #fff;
|
|
||||||
background: rgba(148, 216, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.bill-pagination-popper .el-popper__arrow::before {
|
|
||||||
border-color: rgba(148, 216, 255, 0.18) !important;
|
|
||||||
background: rgba(15, 24, 36, 0.96) !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,307 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="sync-page">
|
|
||||||
<div class="sync-card">
|
|
||||||
<div class="sync-icon" :class="status">
|
|
||||||
<el-icon v-if="status === 'loading'" class="is-loading">
|
|
||||||
<Loading />
|
|
||||||
</el-icon>
|
|
||||||
<el-icon v-else-if="status === 'success'">
|
|
||||||
<CircleCheckFilled />
|
|
||||||
</el-icon>
|
|
||||||
<el-icon v-else>
|
|
||||||
<CircleCloseFilled />
|
|
||||||
</el-icon>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2>{{ title }}</h2>
|
|
||||||
<p>{{ message }}</p>
|
|
||||||
|
|
||||||
<div v-if="status === 'loading'" class="sync-progress">
|
|
||||||
<span></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-button v-if="status === 'error'" type="primary" @click="router.replace('/login')">
|
|
||||||
返回登录
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script setup lang='ts'>
|
|
||||||
import { computed, ref, watch } from 'vue';
|
|
||||||
import { ElMessage } from 'element-plus';
|
|
||||||
import { CircleCheckFilled, CircleCloseFilled, Loading } from '@element-plus/icons-vue';
|
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
|
||||||
import { useLoginStore } from '@/store/LoginStore';
|
|
||||||
import { setApiBaseURL } from '@/utlis/request';
|
|
||||||
import type { LoginUser } from '@/types/login';
|
|
||||||
|
|
||||||
interface SyncUserResponse {
|
|
||||||
status?: string;
|
|
||||||
message?: string;
|
|
||||||
cookie?: string | string[] | Record<string, string>;
|
|
||||||
cookies?: string | string[] | Record<string, string>;
|
|
||||||
user?: Partial<LoginUser>;
|
|
||||||
data?: {
|
|
||||||
user?: Partial<LoginUser>;
|
|
||||||
cookie?: string | string[] | Record<string, string>;
|
|
||||||
cookies?: string | string[] | Record<string, string>;
|
|
||||||
} | Partial<LoginUser>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const route = useRoute();
|
|
||||||
const router = useRouter();
|
|
||||||
const loginStore = useLoginStore();
|
|
||||||
const cookieApiBaseURL = import.meta.env.VITE_API_BASE_URL;
|
|
||||||
const status = ref<'loading' | 'success' | 'error'>('loading');
|
|
||||||
const message = ref('同步用户信息中...');
|
|
||||||
const syncing = ref(false);
|
|
||||||
const title = computed(() => {
|
|
||||||
if (status.value === 'success') return '同步成功';
|
|
||||||
if (status.value === 'error') return '同步失败';
|
|
||||||
return '正在同步登录';
|
|
||||||
});
|
|
||||||
|
|
||||||
const goLoginLater = () => {
|
|
||||||
window.setTimeout(() => {
|
|
||||||
router.replace('/login');
|
|
||||||
}, 1500);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getQueryValue = (key: string) => {
|
|
||||||
const routeValue = route.query[key];
|
|
||||||
if (typeof routeValue === 'string' && routeValue) return routeValue;
|
|
||||||
const searchParams = new URLSearchParams(window.location.search);
|
|
||||||
const searchValue = searchParams.get(key);
|
|
||||||
if (searchValue) return searchValue;
|
|
||||||
|
|
||||||
const hashQuery = window.location.hash.split('?')[1] || '';
|
|
||||||
return new URLSearchParams(hashQuery).get(key);
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseResponse = (text: string): SyncUserResponse | string | null => {
|
|
||||||
if (!text) return null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
return JSON.parse(text) as SyncUserResponse;
|
|
||||||
} catch {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const setCookieValue = (cookie: string) => {
|
|
||||||
if (!cookie.includes('=')) return;
|
|
||||||
document.cookie = cookie;
|
|
||||||
};
|
|
||||||
|
|
||||||
const persistCookies = (cookies?: string | string[] | Record<string, string>) => {
|
|
||||||
if (!cookies) return;
|
|
||||||
|
|
||||||
if (typeof cookies === 'string') {
|
|
||||||
setCookieValue(cookies);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(cookies)) {
|
|
||||||
cookies.forEach(setCookieValue);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.entries(cookies).forEach(([name, value]) => {
|
|
||||||
document.cookie = `${name}=${encodeURIComponent(value)}; path=/`;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const isUserData = (data: SyncUserResponse['data']): data is Partial<LoginUser> => {
|
|
||||||
return Boolean(data && ('id' in data || 'username' in data || 'mobile' in data || 'name' in data));
|
|
||||||
};
|
|
||||||
|
|
||||||
const getSyncedUser = (result: SyncUserResponse | string | null) => {
|
|
||||||
if (!result || typeof result === 'string') return undefined;
|
|
||||||
if (result.user) return result.user;
|
|
||||||
if (result.data && 'user' in result.data) return result.data.user;
|
|
||||||
if (isUserData(result.data)) return result.data;
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const persistResponseCookies = (result: SyncUserResponse | string | null) => {
|
|
||||||
if (!result) return;
|
|
||||||
|
|
||||||
if (typeof result === 'string') {
|
|
||||||
persistCookies(result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
persistCookies(result.cookie);
|
|
||||||
persistCookies(result.cookies);
|
|
||||||
|
|
||||||
if (result.data && 'cookie' in result.data) persistCookies(result.data.cookie);
|
|
||||||
if (result.data && 'cookies' in result.data) persistCookies(result.data.cookies);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getErrorMessage = (result: SyncUserResponse | string | null) => {
|
|
||||||
if (!result || typeof result === 'string') return '同步用户失败';
|
|
||||||
return result.message || '同步用户失败';
|
|
||||||
};
|
|
||||||
|
|
||||||
const getSyncUrl = (deerer: string) => {
|
|
||||||
const url = new URL('/dapi/deerer_user.dspy', cookieApiBaseURL);
|
|
||||||
url.searchParams.set('deerer', deerer);
|
|
||||||
return url.toString();
|
|
||||||
};
|
|
||||||
|
|
||||||
const syncUser = async () => {
|
|
||||||
if (syncing.value) return;
|
|
||||||
|
|
||||||
const deerer = getQueryValue('deerer');
|
|
||||||
|
|
||||||
if (!deerer) {
|
|
||||||
status.value = 'error';
|
|
||||||
message.value = '缺少 deerer 参数';
|
|
||||||
ElMessage.error(message.value);
|
|
||||||
goLoginLater();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
syncing.value = true;
|
|
||||||
status.value = 'loading';
|
|
||||||
message.value = '同步用户信息中...';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch(
|
|
||||||
getSyncUrl(deerer),
|
|
||||||
{
|
|
||||||
credentials: 'include',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = parseResponse(await res.text());
|
|
||||||
persistResponseCookies(result);
|
|
||||||
|
|
||||||
const responseStatus = typeof result === 'string' ? undefined : result?.status;
|
|
||||||
const syncSuccess = res.ok && (!responseStatus || ['ok', 'success'].includes(responseStatus));
|
|
||||||
|
|
||||||
if (syncSuccess) {
|
|
||||||
status.value = 'success';
|
|
||||||
message.value = '同步登录成功,正在进入系统...';
|
|
||||||
setApiBaseURL(cookieApiBaseURL);
|
|
||||||
loginStore.syncCookieLogin(getSyncedUser(result));
|
|
||||||
ElMessage.success('同步登录成功');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
status.value = 'error';
|
|
||||||
message.value = getErrorMessage(result);
|
|
||||||
ElMessage.error(message.value);
|
|
||||||
goLoginLater();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('同步用户失败:', error);
|
|
||||||
status.value = 'error';
|
|
||||||
message.value = '同步用户失败,请稍后重试';
|
|
||||||
ElMessage.error(message.value);
|
|
||||||
goLoginLater();
|
|
||||||
} finally {
|
|
||||||
syncing.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => route.fullPath,
|
|
||||||
() => {
|
|
||||||
syncUser();
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
<style scoped lang='less'>
|
|
||||||
.sync-page {
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 24px;
|
|
||||||
background:
|
|
||||||
radial-gradient(circle at 20% 20%, rgba(64, 158, 255, 0.18), transparent 28%),
|
|
||||||
radial-gradient(circle at 80% 10%, rgba(103, 194, 58, 0.14), transparent 26%),
|
|
||||||
linear-gradient(135deg, #eef5ff 0%, #f8fbff 48%, #eef7f3 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sync-card {
|
|
||||||
width: 420px;
|
|
||||||
max-width: 100%;
|
|
||||||
padding: 44px 36px;
|
|
||||||
text-align: center;
|
|
||||||
background: rgba(255, 255, 255, 0.86);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.9);
|
|
||||||
border-radius: 24px;
|
|
||||||
box-shadow: 0 20px 60px rgba(30, 64, 110, 0.12);
|
|
||||||
backdrop-filter: blur(16px);
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
margin: 20px 0 10px;
|
|
||||||
color: #1f2d3d;
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
min-height: 24px;
|
|
||||||
margin: 0 0 28px;
|
|
||||||
color: #6b778c;
|
|
||||||
font-size: 15px;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.sync-icon {
|
|
||||||
width: 76px;
|
|
||||||
height: 76px;
|
|
||||||
margin: 0 auto;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: 50%;
|
|
||||||
font-size: 38px;
|
|
||||||
|
|
||||||
&.loading {
|
|
||||||
color: #409eff;
|
|
||||||
background: rgba(64, 158, 255, 0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.success {
|
|
||||||
color: #67c23a;
|
|
||||||
background: rgba(103, 194, 58, 0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.error {
|
|
||||||
color: #f56c6c;
|
|
||||||
background: rgba(245, 108, 108, 0.12);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.sync-progress {
|
|
||||||
height: 6px;
|
|
||||||
overflow: hidden;
|
|
||||||
background: #e8f1ff;
|
|
||||||
border-radius: 999px;
|
|
||||||
|
|
||||||
span {
|
|
||||||
display: block;
|
|
||||||
width: 45%;
|
|
||||||
height: 100%;
|
|
||||||
border-radius: inherit;
|
|
||||||
background: linear-gradient(90deg, #79bbff, #409eff);
|
|
||||||
animation: progress 1.2s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes progress {
|
|
||||||
0% {
|
|
||||||
transform: translateX(-110%);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
transform: translateX(230%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
@ -1,784 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="history-page">
|
|
||||||
<LoginPrompt
|
|
||||||
v-if="!isLoggedIn"
|
|
||||||
class="history-login-prompt"
|
|
||||||
title="请先登录"
|
|
||||||
description="登录后可查看和管理你生成过的图片、视频作品。"
|
|
||||||
full-height
|
|
||||||
/>
|
|
||||||
|
|
||||||
<template v-else>
|
|
||||||
<div class="page-head">
|
|
||||||
<div>
|
|
||||||
<div class="page-title">历史创作</div>
|
|
||||||
<div class="page-desc">查看和管理你生成过的图片、视频作品</div>
|
|
||||||
</div>
|
|
||||||
<button type="button" class="refresh-btn" :disabled="loading" @click="loadHistory">
|
|
||||||
{{ loading ? '刷新中...' : '刷新' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="toolbar">
|
|
||||||
<div class="toolbar-left">
|
|
||||||
<button
|
|
||||||
v-for="item in categoryOptions"
|
|
||||||
:key="item.value"
|
|
||||||
type="button"
|
|
||||||
class="category-chip"
|
|
||||||
:class="{ active: activeCategory === item.value }"
|
|
||||||
@click="activeCategory = item.value; handleCategoryChange()"
|
|
||||||
>
|
|
||||||
{{ item.label }}
|
|
||||||
</button>
|
|
||||||
<button type="button" class="tool-btn" @click="toggleSort">
|
|
||||||
日期 <i class="iconfont icon-jiantou_shangxiaqiehuan_o"></i>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="tool-btn reset-btn" @click="resetFilters">重置</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="toolbar-right">
|
|
||||||
<div class="search-box">
|
|
||||||
<span class="search-icon"><i class="iconfont icon-sousuo"></i></span>
|
|
||||||
<input v-model="keyword" type="text" placeholder="名字 / 提示词 / ID" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="library-meta">
|
|
||||||
<div class="folder-name">所有作品</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="content-area" v-loading="loading">
|
|
||||||
<div v-if="!loading && !filteredWorks.length" class="empty-container">
|
|
||||||
<img src="../assets/images/preparation.png" alt="" />
|
|
||||||
<div>暂无历史创作</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="works-grid">
|
|
||||||
<div
|
|
||||||
v-for="work in filteredWorks"
|
|
||||||
:key="work.id"
|
|
||||||
class="work-card"
|
|
||||||
@click="selectWork(work)"
|
|
||||||
>
|
|
||||||
<div class="thumb-wrap">
|
|
||||||
<img
|
|
||||||
v-if="work.mediaType === 'image' && work.mediaUrl"
|
|
||||||
:src="work.mediaUrl"
|
|
||||||
alt="作品预览"
|
|
||||||
class="thumb-media"
|
|
||||||
/>
|
|
||||||
<video
|
|
||||||
v-else-if="work.mediaType === 'video' && work.mediaUrl"
|
|
||||||
:src="work.mediaUrl"
|
|
||||||
:poster="work.coverUrl"
|
|
||||||
class="thumb-media"
|
|
||||||
muted
|
|
||||||
loop
|
|
||||||
playsinline
|
|
||||||
preload="metadata"
|
|
||||||
@mouseenter="playVideo"
|
|
||||||
@mouseleave="pauseVideo"
|
|
||||||
/>
|
|
||||||
<div v-else class="thumb-empty">
|
|
||||||
{{ work.status === 'SUCCEEDED' ? '暂无预览' : getStatusText(work.status) }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="media-badge">{{ getCategoryLabel(work.llmcatelogid) }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="work-info">
|
|
||||||
<div class="work-name" :title="work.prompt || work.id">
|
|
||||||
{{ work.prompt || work.id }}
|
|
||||||
</div>
|
|
||||||
<div class="work-size">{{ work.sizeText }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="work-sub-info">
|
|
||||||
<span class="model-name">{{ work.model || getCategoryLabel(work.llmcatelogid) }}</span>
|
|
||||||
<span>{{ work.useTime || '-' }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="pagination-wrap">
|
|
||||||
<el-pagination
|
|
||||||
v-model:current-page="page"
|
|
||||||
v-model:page-size="pageSize"
|
|
||||||
:page-sizes="[10, 20, 30, 50]"
|
|
||||||
:total="total"
|
|
||||||
layout="total, sizes, prev, pager, next, jumper"
|
|
||||||
popper-class="history-pagination-popper"
|
|
||||||
@size-change="loadHistory"
|
|
||||||
@current-change="loadHistory"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { getHistoryRecord } from '@/apis/Model';
|
|
||||||
import LoginPrompt from '@/components/LoginPrompt/LoginPrompt.vue';
|
|
||||||
import { useLoginStore } from '@/store/LoginStore';
|
|
||||||
import type { HistoryRecordParams } from '@/types/model';
|
|
||||||
import { ElMessage } from 'element-plus';
|
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
|
|
||||||
type CategoryValue = 't2v' | 'i2v' | 'r2v' | 't2i' | 'i2i' | 'r2i';
|
|
||||||
type MediaType = 'image' | 'video';
|
|
||||||
|
|
||||||
interface HistoryRecordResponse {
|
|
||||||
success?: boolean;
|
|
||||||
status?: string;
|
|
||||||
rows?: RawHistoryRecord[];
|
|
||||||
total?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RawHistoryRecord {
|
|
||||||
id?: string;
|
|
||||||
taskid?: string;
|
|
||||||
status?: string;
|
|
||||||
use_time?: string;
|
|
||||||
use_date?: string;
|
|
||||||
usages?: unknown;
|
|
||||||
io_content?: {
|
|
||||||
input?: Record<string, unknown>;
|
|
||||||
output?: Array<Record<string, unknown>>;
|
|
||||||
} | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WorkItem {
|
|
||||||
id: string;
|
|
||||||
taskid: string;
|
|
||||||
status: string;
|
|
||||||
prompt: string;
|
|
||||||
model: string;
|
|
||||||
resolution: string;
|
|
||||||
duration: string;
|
|
||||||
useTime: string;
|
|
||||||
llmcatelogid: CategoryValue;
|
|
||||||
mediaType: MediaType;
|
|
||||||
mediaUrl: string;
|
|
||||||
coverUrl: string;
|
|
||||||
sizeText: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const categoryOptions: Array<{ label: string; value: CategoryValue }> = [
|
|
||||||
{ label: '文生视频', value: 't2v' },
|
|
||||||
{ label: '图生视频', value: 'i2v' },
|
|
||||||
{ label: '参考生视频', value: 'r2v' },
|
|
||||||
{ label: '文生图', value: 't2i' },
|
|
||||||
{ label: '图生图', value: 'i2i' },
|
|
||||||
{ label: '参考生图', value: 'r2i' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const activeCategory = ref<CategoryValue>('t2v');
|
|
||||||
const loginStore = useLoginStore();
|
|
||||||
const router = useRouter();
|
|
||||||
const keyword = ref('');
|
|
||||||
const works = ref<WorkItem[]>([]);
|
|
||||||
const loading = ref(false);
|
|
||||||
const page = ref(1);
|
|
||||||
const pageSize = ref(10);
|
|
||||||
const total = ref(0);
|
|
||||||
const sortDesc = ref(true);
|
|
||||||
|
|
||||||
const isLoggedIn = computed(() => loginStore.isLoggedIn);
|
|
||||||
|
|
||||||
const filteredWorks = computed(() => {
|
|
||||||
const search = keyword.value.trim().toLowerCase();
|
|
||||||
const list = [...works.value].sort((a, b) => (
|
|
||||||
sortDesc.value
|
|
||||||
? b.useTime.localeCompare(a.useTime)
|
|
||||||
: a.useTime.localeCompare(b.useTime)
|
|
||||||
));
|
|
||||||
|
|
||||||
if (!search) return list;
|
|
||||||
|
|
||||||
return list.filter((work) => [
|
|
||||||
work.id,
|
|
||||||
work.taskid,
|
|
||||||
work.prompt,
|
|
||||||
work.model,
|
|
||||||
work.resolution,
|
|
||||||
].some((value) => value.toLowerCase().includes(search)));
|
|
||||||
});
|
|
||||||
|
|
||||||
const readString = (source: Record<string, unknown> | undefined, keys: string[]) => {
|
|
||||||
if (!source) return '';
|
|
||||||
|
|
||||||
for (const key of keys) {
|
|
||||||
const value = source[key];
|
|
||||||
if (typeof value === 'string' && value) return value;
|
|
||||||
if (typeof value === 'number') return String(value);
|
|
||||||
if (Array.isArray(value) && typeof value[0] === 'string') return value[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseUsage = (usage: unknown) => {
|
|
||||||
if (!usage) return {};
|
|
||||||
if (typeof usage === 'object') return usage as Record<string, unknown>;
|
|
||||||
if (typeof usage !== 'string') return {};
|
|
||||||
|
|
||||||
try {
|
|
||||||
return JSON.parse(usage.replace(/'/g, '"')) as Record<string, unknown>;
|
|
||||||
} catch {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getFinalOutput = (output?: Array<Record<string, unknown>>) => {
|
|
||||||
if (!Array.isArray(output) || !output.length) return {};
|
|
||||||
return output.find((item) => readString(item, ['video', 'video_url', 'image', 'image_url', 'url']))
|
|
||||||
|| output[output.length - 1]
|
|
||||||
|| {};
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizeStatus = (status?: string) => {
|
|
||||||
const upperStatus = String(status || '').toUpperCase();
|
|
||||||
const statusMap: Record<string, string> = {
|
|
||||||
CREATED: 'CREATED',
|
|
||||||
CREATEDD: 'CREATED',
|
|
||||||
PROCESSING: 'RUNNING',
|
|
||||||
RUNNING: 'RUNNING',
|
|
||||||
PENDING: 'PENDING',
|
|
||||||
QUEUEING: 'PENDING',
|
|
||||||
SUCCEEDED: 'SUCCEEDED',
|
|
||||||
COMPLETED: 'SUCCEEDED',
|
|
||||||
SUCCESS: 'SUCCEEDED',
|
|
||||||
FAILED: 'FAILED',
|
|
||||||
};
|
|
||||||
return statusMap[upperStatus] || upperStatus || 'UNKNOWN';
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizeCategory = (value: string): CategoryValue => {
|
|
||||||
return categoryOptions.some((item) => item.value === value)
|
|
||||||
? value as CategoryValue
|
|
||||||
: activeCategory.value;
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizeWork = (record: RawHistoryRecord): WorkItem => {
|
|
||||||
const usage = parseUsage(record.usages);
|
|
||||||
const input = record.io_content?.input || {};
|
|
||||||
const output = getFinalOutput(record.io_content?.output);
|
|
||||||
const action = normalizeCategory(readString(usage, ['action']));
|
|
||||||
const videoUrl = readString(output, ['video', 'video_url']);
|
|
||||||
const imageUrl = readString(output, ['image', 'image_url', 'url', 'output_image', 'output_url', 'images']);
|
|
||||||
const mediaType: MediaType = videoUrl || action.endsWith('v') ? 'video' : 'image';
|
|
||||||
const resolution = readString(input, ['resolution', 'size', 'ratio'])
|
|
||||||
|| readString(usage, ['resolution', 'video_ratio'])
|
|
||||||
|| '';
|
|
||||||
const duration = readString(input, ['duration'])
|
|
||||||
|| readString(usage, ['duration', 'video_duration'])
|
|
||||||
|| '';
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: record.id || record.taskid || '',
|
|
||||||
taskid: record.taskid || '',
|
|
||||||
status: normalizeStatus(record.status || readString(output, ['status'])),
|
|
||||||
prompt: readString(input, ['prompt']),
|
|
||||||
model: readString(input, ['model']) || readString(usage, ['model']),
|
|
||||||
resolution,
|
|
||||||
duration,
|
|
||||||
useTime: record.use_time || record.use_date || '',
|
|
||||||
llmcatelogid: action,
|
|
||||||
mediaType,
|
|
||||||
mediaUrl: videoUrl || imageUrl,
|
|
||||||
coverUrl: imageUrl,
|
|
||||||
sizeText: [resolution, duration ? `${duration}s` : ''].filter(Boolean).join(' / ') || getCategoryLabel(action),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadHistory = async () => {
|
|
||||||
if (!isLoggedIn.value) {
|
|
||||||
works.value = [];
|
|
||||||
total.value = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
loading.value = true;
|
|
||||||
try {
|
|
||||||
const params: HistoryRecordParams = {
|
|
||||||
page: page.value,
|
|
||||||
pageSize: pageSize.value,
|
|
||||||
llmcatelogid: activeCategory.value,
|
|
||||||
};
|
|
||||||
const res = await getHistoryRecord<HistoryRecordResponse, HistoryRecordParams>(params);
|
|
||||||
const rows = Array.isArray(res.rows) ? res.rows : [];
|
|
||||||
|
|
||||||
works.value = rows.map(normalizeWork);
|
|
||||||
total.value = Number(res.total || rows.length);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取历史创作失败:', error);
|
|
||||||
ElMessage.error('获取历史创作失败');
|
|
||||||
works.value = [];
|
|
||||||
total.value = 0;
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCategoryChange = () => {
|
|
||||||
page.value = 1;
|
|
||||||
loadHistory();
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectWork = (work: WorkItem) => {
|
|
||||||
sessionStorage.setItem('history_creation_detail', JSON.stringify(work));
|
|
||||||
router.push({
|
|
||||||
name: 'history-creation-detail',
|
|
||||||
params: { id: work.id || work.taskid },
|
|
||||||
query: { llmcatelogid: work.llmcatelogid },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleSort = () => {
|
|
||||||
sortDesc.value = !sortDesc.value;
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetFilters = () => {
|
|
||||||
keyword.value = '';
|
|
||||||
activeCategory.value = 't2v';
|
|
||||||
sortDesc.value = true;
|
|
||||||
page.value = 1;
|
|
||||||
loadHistory();
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCategoryLabel = (value: CategoryValue) => {
|
|
||||||
return categoryOptions.find((item) => item.value === value)?.label || '创作记录';
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusText = (status: string) => {
|
|
||||||
const statusMap: Record<string, string> = {
|
|
||||||
CREATED: '已创建',
|
|
||||||
PENDING: '排队中',
|
|
||||||
RUNNING: '运行中',
|
|
||||||
SUCCEEDED: '成功',
|
|
||||||
FAILED: '失败',
|
|
||||||
UNKNOWN: '未知',
|
|
||||||
};
|
|
||||||
return statusMap[normalizeStatus(status)] || status;
|
|
||||||
};
|
|
||||||
|
|
||||||
const playVideo = (event: MouseEvent) => {
|
|
||||||
const video = event.currentTarget as HTMLVideoElement;
|
|
||||||
video.play().catch(() => undefined);
|
|
||||||
};
|
|
||||||
|
|
||||||
const pauseVideo = (event: MouseEvent) => {
|
|
||||||
const video = event.currentTarget as HTMLVideoElement;
|
|
||||||
video.pause();
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (isLoggedIn.value) {
|
|
||||||
loadHistory();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(isLoggedIn, (value) => {
|
|
||||||
if (value) {
|
|
||||||
loadHistory();
|
|
||||||
} else {
|
|
||||||
works.value = [];
|
|
||||||
total.value = 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="less">
|
|
||||||
.history-page {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
color: #fff;
|
|
||||||
padding: 18px 20px 14px;
|
|
||||||
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-head {
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 16px;
|
|
||||||
margin-bottom: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-title {
|
|
||||||
color: #ffffff;
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-desc {
|
|
||||||
margin-top: 6px;
|
|
||||||
color: rgba(220, 232, 255, 0.5);
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.refresh-btn {
|
|
||||||
height: 34px;
|
|
||||||
padding: 0 16px;
|
|
||||||
border: 1px solid rgba(142, 164, 198, 0.18);
|
|
||||||
border-radius: 10px;
|
|
||||||
color: rgba(255, 255, 255, 0.82);
|
|
||||||
background: rgba(255, 255, 255, 0.06);
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: #fff;
|
|
||||||
border-color: rgba(148, 216, 255, 0.42);
|
|
||||||
background: rgba(148, 216, 255, 0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar {
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 14px;
|
|
||||||
margin: 14px 0 12px;
|
|
||||||
padding: 12px;
|
|
||||||
border: 1px solid rgba(142, 164, 198, 0.1);
|
|
||||||
border-radius: 14px;
|
|
||||||
background: rgba(14, 24, 36, 0.76);
|
|
||||||
box-shadow: 0 18px 44px rgba(0, 0, 0, 0.18);
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar-left,
|
|
||||||
.toolbar-right {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool-btn {
|
|
||||||
height: 32px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 0 12px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 9px;
|
|
||||||
color: rgba(220, 232, 255, 0.78);
|
|
||||||
background: rgba(255, 255, 255, 0.07);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.18s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: #fff;
|
|
||||||
background: rgba(255, 255, 255, 0.12);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-chip {
|
|
||||||
height: 32px;
|
|
||||||
padding: 0 12px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
border-radius: 999px;
|
|
||||||
color: rgba(220, 232, 255, 0.68);
|
|
||||||
background: rgba(255, 255, 255, 0.06);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.18s ease;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
color: #07101a;
|
|
||||||
background: linear-gradient(270deg, #94d8ff 11.11%, #e0ddff);
|
|
||||||
box-shadow: 0 8px 20px rgba(148, 216, 255, 0.18);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover:not(.active) {
|
|
||||||
color: #fff;
|
|
||||||
border-color: rgba(148, 216, 255, 0.26);
|
|
||||||
background: rgba(148, 216, 255, 0.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-box {
|
|
||||||
width: 220px;
|
|
||||||
height: 34px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 0 12px;
|
|
||||||
border: 1px solid rgba(142, 164, 198, 0.1);
|
|
||||||
border-radius: 10px;
|
|
||||||
background: rgba(4, 10, 18, 0.58);
|
|
||||||
color: rgba(255, 255, 255, 0.42);
|
|
||||||
|
|
||||||
input {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
color: #fff;
|
|
||||||
background: transparent;
|
|
||||||
font-size: 13px;
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: rgba(255, 255, 255, 0.38);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.library-meta {
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 16px;
|
|
||||||
padding: 2px 4px 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.folder-name {
|
|
||||||
color: #fff;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-area {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
padding: 0 4px 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-container {
|
|
||||||
height: 100%;
|
|
||||||
min-height: 360px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: rgba(255, 255, 255, 0.62);
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 180px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.works-grid {
|
|
||||||
height: 100%;
|
|
||||||
min-height: 0;
|
|
||||||
overflow-y: auto;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
||||||
align-content: start;
|
|
||||||
gap: 18px;
|
|
||||||
padding-right: 4px;
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: rgba(148, 216, 255, 0.28) transparent;
|
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
border-radius: 999px;
|
|
||||||
background: rgba(148, 216, 255, 0.28);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.work-card {
|
|
||||||
min-width: 0;
|
|
||||||
padding: 8px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
border-radius: 14px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: transform 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease, background 0.18s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
transform: translateY(-3px);
|
|
||||||
border-color: rgba(148, 216, 255, 0.24);
|
|
||||||
background: rgba(23, 37, 54, 0.72);
|
|
||||||
box-shadow: 0 18px 34px rgba(0, 0, 0, 0.28);
|
|
||||||
|
|
||||||
.thumb-media {
|
|
||||||
transform: scale(1.04);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.thumb-wrap {
|
|
||||||
position: relative;
|
|
||||||
height: 260px;
|
|
||||||
overflow: hidden;
|
|
||||||
border-radius: 10px;
|
|
||||||
background:
|
|
||||||
linear-gradient(135deg, rgba(148, 216, 255, 0.08), transparent),
|
|
||||||
#162231;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thumb-media {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
display: block;
|
|
||||||
transition: transform 0.25s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thumb-empty {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: rgba(255, 255, 255, 0.42);
|
|
||||||
background: linear-gradient(135deg, rgba(148, 216, 255, 0.08), rgba(224, 221, 255, 0.05));
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-badge {
|
|
||||||
position: absolute;
|
|
||||||
left: 8px;
|
|
||||||
top: 8px;
|
|
||||||
height: 24px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0 8px;
|
|
||||||
border-radius: 999px;
|
|
||||||
color: #dce8ff;
|
|
||||||
background: rgba(0, 0, 0, 0.46);
|
|
||||||
backdrop-filter: blur(8px);
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.work-info {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 10px 2px 0;
|
|
||||||
color: rgba(255, 255, 255, 0.86);
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.work-sub-info {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 4px 2px 0;
|
|
||||||
color: rgba(255, 255, 255, 0.48);
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.model-name {
|
|
||||||
min-width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.work-name {
|
|
||||||
min-width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.work-size {
|
|
||||||
flex-shrink: 0;
|
|
||||||
color: rgba(255, 255, 255, 0.52);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination-wrap {
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
padding: 12px 4px 0;
|
|
||||||
border-top: 1px solid rgba(142, 164, 198, 0.08);
|
|
||||||
|
|
||||||
:deep(.el-pagination) {
|
|
||||||
--el-pagination-bg-color: transparent;
|
|
||||||
--el-pagination-text-color: rgba(235, 244, 255, 0.9);
|
|
||||||
--el-pagination-button-disabled-bg-color: transparent;
|
|
||||||
--el-pagination-button-bg-color: transparent;
|
|
||||||
--el-pagination-hover-color: #94d8ff;
|
|
||||||
color: rgba(235, 244, 255, 0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-pagination__total),
|
|
||||||
:deep(.el-pagination__jump) {
|
|
||||||
color: rgba(235, 244, 255, 0.86);
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-pager li),
|
|
||||||
:deep(.btn-prev),
|
|
||||||
:deep(.btn-next) {
|
|
||||||
color: rgba(235, 244, 255, 0.74);
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-pager li.is-active) {
|
|
||||||
color: #94d8ff;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-select__wrapper),
|
|
||||||
:deep(.el-input__wrapper) {
|
|
||||||
border-radius: 10px;
|
|
||||||
background: rgba(15, 24, 36, 0.82);
|
|
||||||
box-shadow: 0 0 0 1px rgba(148, 216, 255, 0.16) inset;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-select__selected-item),
|
|
||||||
:deep(.el-input__inner) {
|
|
||||||
color: rgba(235, 244, 255, 0.92);
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-select__caret) {
|
|
||||||
color: rgba(235, 244, 255, 0.58);
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-select__wrapper:hover),
|
|
||||||
:deep(.el-input__wrapper:hover) {
|
|
||||||
box-shadow: 0 0 0 1px rgba(148, 216, 255, 0.34) inset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.history-pagination-popper) {
|
|
||||||
border: 1px solid rgba(148, 216, 255, 0.18) !important;
|
|
||||||
border-radius: 10px !important;
|
|
||||||
background: rgba(15, 24, 36, 0.96) !important;
|
|
||||||
box-shadow: 0 14px 34px rgba(0, 0, 0, 0.32) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.history-pagination-popper .el-select-dropdown__list) {
|
|
||||||
padding: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.history-pagination-popper .el-select-dropdown__item) {
|
|
||||||
border-radius: 8px;
|
|
||||||
color: rgba(235, 244, 255, 0.78);
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.history-pagination-popper .el-select-dropdown__item.is-selected) {
|
|
||||||
color: #94d8ff;
|
|
||||||
background: rgba(148, 216, 255, 0.12);
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.history-pagination-popper .el-select-dropdown__item.hover),
|
|
||||||
:global(.history-pagination-popper .el-select-dropdown__item:hover) {
|
|
||||||
color: #fff;
|
|
||||||
background: rgba(148, 216, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.history-pagination-popper .el-popper__arrow::before) {
|
|
||||||
border-color: rgba(148, 216, 255, 0.18) !important;
|
|
||||||
background: rgba(15, 24, 36, 0.96) !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,491 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="history-detail-page">
|
|
||||||
<LoginPrompt
|
|
||||||
v-if="!isLoggedIn"
|
|
||||||
class="history-login-prompt"
|
|
||||||
title="请先登录"
|
|
||||||
description="登录后可查看和下载历史创作作品。"
|
|
||||||
full-height
|
|
||||||
/>
|
|
||||||
|
|
||||||
<template v-else-if="work">
|
|
||||||
<button type="button" class="back-btn" @click="goBack"> <i class="iconfont icon-zuo"></i> 返回</button>
|
|
||||||
|
|
||||||
<div class="detail-layout">
|
|
||||||
<section class="preview-panel">
|
|
||||||
<img
|
|
||||||
v-if="work.mediaType === 'image' && work.mediaUrl"
|
|
||||||
:src="work.mediaUrl"
|
|
||||||
alt="作品预览"
|
|
||||||
/>
|
|
||||||
<video
|
|
||||||
v-else-if="work.mediaType === 'video' && work.mediaUrl"
|
|
||||||
:src="work.mediaUrl"
|
|
||||||
:poster="work.coverUrl"
|
|
||||||
controls
|
|
||||||
playsinline
|
|
||||||
/>
|
|
||||||
<div v-else class="empty-preview">暂无预览</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<aside class="info-panel">
|
|
||||||
<div class="info-time">{{ work.useTime || '-' }}</div>
|
|
||||||
<!-- <div class="author">
|
|
||||||
<div class="author-avatar">元</div>
|
|
||||||
<div>
|
|
||||||
<div class="author-name">元境官方</div>
|
|
||||||
<div class="author-type">{{ getCategoryLabel(work.llmcatelogid) }}</div>
|
|
||||||
</div>
|
|
||||||
</div> -->
|
|
||||||
|
|
||||||
<div class="info-title">{{ work.prompt || work.id }}</div>
|
|
||||||
|
|
||||||
<div class="info-list">
|
|
||||||
<div class="info-line">
|
|
||||||
<span>模型</span>
|
|
||||||
<strong>{{ work.model || '-' }}</strong>
|
|
||||||
</div>
|
|
||||||
<div class="info-line">
|
|
||||||
<span>规格</span>
|
|
||||||
<strong>{{ work.sizeText }}</strong>
|
|
||||||
</div>
|
|
||||||
<div class="info-line">
|
|
||||||
<span>状态</span>
|
|
||||||
<strong>{{ getStatusText(work.status) }}</strong>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-stats">
|
|
||||||
<div class="stat-card">
|
|
||||||
<span>类型</span>
|
|
||||||
<strong>{{ work.mediaType === 'video' ? '视频' : '图片' }}</strong>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card">
|
|
||||||
<span>时长</span>
|
|
||||||
<strong>{{ work.duration || '-' }}</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="download-btn"
|
|
||||||
:disabled="!work.mediaUrl"
|
|
||||||
@click="downloadMedia"
|
|
||||||
>
|
|
||||||
<i class="iconfont icon-xiazai"></i>
|
|
||||||
下载作品
|
|
||||||
</button>
|
|
||||||
</aside>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div v-else class="empty-detail">
|
|
||||||
<div class="empty-title">未找到作品详情</div>
|
|
||||||
<button type="button" class="back-btn" @click="goBack">返回历史创作</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import LoginPrompt from '@/components/LoginPrompt/LoginPrompt.vue';
|
|
||||||
import { useLoginStore } from '@/store/LoginStore';
|
|
||||||
import { ElMessage } from 'element-plus';
|
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
|
||||||
|
|
||||||
type CategoryValue = 't2v' | 'i2v' | 'r2v' | 't2i' | 'i2i' | 'r2i';
|
|
||||||
type MediaType = 'image' | 'video';
|
|
||||||
|
|
||||||
interface WorkItem {
|
|
||||||
id: string;
|
|
||||||
taskid: string;
|
|
||||||
status: string;
|
|
||||||
prompt: string;
|
|
||||||
model: string;
|
|
||||||
resolution: string;
|
|
||||||
duration: string;
|
|
||||||
useTime: string;
|
|
||||||
llmcatelogid: CategoryValue;
|
|
||||||
mediaType: MediaType;
|
|
||||||
mediaUrl: string;
|
|
||||||
coverUrl: string;
|
|
||||||
sizeText: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const categoryOptions: Array<{ label: string; value: CategoryValue }> = [
|
|
||||||
{ label: '文生视频', value: 't2v' },
|
|
||||||
{ label: '图生视频', value: 'i2v' },
|
|
||||||
{ label: '参考生视频', value: 'r2v' },
|
|
||||||
{ label: '文生图', value: 't2i' },
|
|
||||||
{ label: '图生图', value: 'i2i' },
|
|
||||||
{ label: '参考生图', value: 'r2i' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const detailStorageKey = 'history_creation_detail';
|
|
||||||
const route = useRoute();
|
|
||||||
const router = useRouter();
|
|
||||||
const loginStore = useLoginStore();
|
|
||||||
const work = ref<WorkItem | null>(null);
|
|
||||||
const isLoggedIn = computed(() => loginStore.isLoggedIn);
|
|
||||||
|
|
||||||
const goBack = () => {
|
|
||||||
router.push('/history-creation');
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCategoryLabel = (value: CategoryValue) => {
|
|
||||||
return categoryOptions.find((item) => item.value === value)?.label || '创作记录';
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizeStatus = (status?: string) => {
|
|
||||||
const upperStatus = String(status || '').toUpperCase();
|
|
||||||
const statusMap: Record<string, string> = {
|
|
||||||
CREATED: 'CREATED',
|
|
||||||
CREATEDD: 'CREATED',
|
|
||||||
PROCESSING: 'RUNNING',
|
|
||||||
RUNNING: 'RUNNING',
|
|
||||||
PENDING: 'PENDING',
|
|
||||||
QUEUEING: 'PENDING',
|
|
||||||
SUCCEEDED: 'SUCCEEDED',
|
|
||||||
COMPLETED: 'SUCCEEDED',
|
|
||||||
SUCCESS: 'SUCCEEDED',
|
|
||||||
FAILED: 'FAILED',
|
|
||||||
};
|
|
||||||
return statusMap[upperStatus] || upperStatus || 'UNKNOWN';
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusText = (status: string) => {
|
|
||||||
const statusMap: Record<string, string> = {
|
|
||||||
CREATED: '已创建',
|
|
||||||
PENDING: '排队中',
|
|
||||||
RUNNING: '运行中',
|
|
||||||
SUCCEEDED: '成功',
|
|
||||||
FAILED: '失败',
|
|
||||||
UNKNOWN: '未知',
|
|
||||||
};
|
|
||||||
return statusMap[normalizeStatus(status)] || status;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getMediaExtension = (target: WorkItem, mimeType = '') => {
|
|
||||||
if (mimeType.includes('jpeg')) return 'jpg';
|
|
||||||
if (mimeType.includes('png')) return 'png';
|
|
||||||
if (mimeType.includes('webp')) return 'webp';
|
|
||||||
if (mimeType.includes('gif')) return 'gif';
|
|
||||||
if (mimeType.includes('mp4')) return 'mp4';
|
|
||||||
|
|
||||||
const decodedUrl = decodeURIComponent(target.mediaUrl);
|
|
||||||
const extension = decodedUrl.match(/\.(png|jpe?g|webp|gif|mp4)(?:\?|&|$)/i)?.[1];
|
|
||||||
if (extension) return extension.toLowerCase().replace('jpeg', 'jpg');
|
|
||||||
|
|
||||||
return target.mediaType === 'image' ? 'png' : 'mp4';
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDownloadFileName = (target: WorkItem, mimeType = '') => {
|
|
||||||
const safeId = (target.id || target.taskid || 'result').replace(/[\\/:*?"<>|]/g, '_');
|
|
||||||
return `${getCategoryLabel(target.llmcatelogid)}_${safeId}.${getMediaExtension(target, mimeType)}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const downloadBlob = (target: WorkItem, blob: Blob) => {
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = url;
|
|
||||||
link.download = getDownloadFileName(target, blob.type);
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
document.body.removeChild(link);
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
};
|
|
||||||
|
|
||||||
const triggerDirectDownload = (target: WorkItem) => {
|
|
||||||
const frameName = 'history-download-frame';
|
|
||||||
let iframe = document.querySelector<HTMLIFrameElement>(`iframe[name="${frameName}"]`);
|
|
||||||
if (!iframe) {
|
|
||||||
iframe = document.createElement('iframe');
|
|
||||||
iframe.name = frameName;
|
|
||||||
iframe.style.display = 'none';
|
|
||||||
document.body.appendChild(iframe);
|
|
||||||
}
|
|
||||||
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = target.mediaUrl;
|
|
||||||
link.download = getDownloadFileName(target);
|
|
||||||
link.target = frameName;
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
document.body.removeChild(link);
|
|
||||||
};
|
|
||||||
|
|
||||||
const downloadMedia = async () => {
|
|
||||||
if (!work.value?.mediaUrl) {
|
|
||||||
ElMessage.warning('暂无可下载作品');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(work.value.mediaUrl, {
|
|
||||||
credentials: 'omit',
|
|
||||||
cache: 'no-store',
|
|
||||||
});
|
|
||||||
if (!response.ok) throw new Error(`下载请求失败:${response.status}`);
|
|
||||||
|
|
||||||
const blob = await response.blob();
|
|
||||||
downloadBlob(work.value, blob);
|
|
||||||
} catch {
|
|
||||||
triggerDirectDownload(work.value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadDetail = () => {
|
|
||||||
const raw = sessionStorage.getItem(detailStorageKey);
|
|
||||||
if (!raw) {
|
|
||||||
work.value = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const cachedWork = JSON.parse(raw) as WorkItem;
|
|
||||||
const routeId = String(route.params.id || '');
|
|
||||||
work.value = !routeId || cachedWork.id === routeId || cachedWork.taskid === routeId
|
|
||||||
? cachedWork
|
|
||||||
: null;
|
|
||||||
} catch {
|
|
||||||
work.value = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(loadDetail);
|
|
||||||
|
|
||||||
watch(isLoggedIn, (value) => {
|
|
||||||
if (!value) {
|
|
||||||
work.value = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="less">
|
|
||||||
.history-detail-page {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 9999;
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
min-height: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 18px 24px 20px;
|
|
||||||
color: #fff;
|
|
||||||
overflow: hidden;
|
|
||||||
background:
|
|
||||||
radial-gradient(circle at 20% 10%, rgba(16, 79, 120, 0.24), transparent 32%),
|
|
||||||
linear-gradient(180deg, #070a12 0%, #080b13 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-login-prompt {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.back-btn {
|
|
||||||
width: fit-content;
|
|
||||||
height: 34px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0 14px;
|
|
||||||
margin-bottom: 14px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 999px;
|
|
||||||
color: rgba(255, 255, 255, 0.82);
|
|
||||||
background: rgba(255, 255, 255, 0.08);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
i{
|
|
||||||
font-size: 16px;
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
&:hover {
|
|
||||||
color: #fff;
|
|
||||||
background: rgba(255, 255, 255, 0.13);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-layout {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(0, 1fr) 320px;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-panel {
|
|
||||||
min-width: 0;
|
|
||||||
min-height: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
overflow: hidden;
|
|
||||||
border: 1px solid rgba(142, 164, 198, 0.08);
|
|
||||||
border-radius: 16px;
|
|
||||||
background: #000;
|
|
||||||
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.28);
|
|
||||||
|
|
||||||
img,
|
|
||||||
video {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: contain;
|
|
||||||
background: #000;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-panel {
|
|
||||||
min-height: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 18px;
|
|
||||||
border: 1px solid rgba(142, 164, 198, 0.1);
|
|
||||||
border-radius: 16px;
|
|
||||||
background: linear-gradient(180deg, rgba(24, 30, 43, 0.96), rgba(13, 18, 28, 0.96));
|
|
||||||
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.22);
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-time {
|
|
||||||
color: rgba(255, 255, 255, 0.45);
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.author {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
margin-top: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.author-avatar {
|
|
||||||
width: 34px;
|
|
||||||
height: 34px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: 50%;
|
|
||||||
color: #94d8ff;
|
|
||||||
background: rgba(148, 216, 255, 0.12);
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.author-name {
|
|
||||||
color: #fff;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.author-type {
|
|
||||||
margin-top: 2px;
|
|
||||||
color: rgba(255, 255, 255, 0.48);
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-title {
|
|
||||||
margin-top: 18px;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 800;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-list {
|
|
||||||
margin-top: 16px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-line {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 10px;
|
|
||||||
color: rgba(255, 255, 255, 0.58);
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.6;
|
|
||||||
|
|
||||||
span {
|
|
||||||
flex-shrink: 0;
|
|
||||||
color: rgba(148, 216, 255, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
strong {
|
|
||||||
min-width: 0;
|
|
||||||
color: rgba(255, 255, 255, 0.78);
|
|
||||||
font-weight: 500;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-stats {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
margin-top: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-card {
|
|
||||||
min-width: 74px;
|
|
||||||
padding: 12px;
|
|
||||||
border-radius: 10px;
|
|
||||||
background: rgba(255, 255, 255, 0.06);
|
|
||||||
|
|
||||||
span {
|
|
||||||
display: block;
|
|
||||||
color: rgba(255, 255, 255, 0.46);
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
strong {
|
|
||||||
display: block;
|
|
||||||
margin-top: 8px;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-btn {
|
|
||||||
height: 42px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 8px;
|
|
||||||
margin-top: auto;
|
|
||||||
border: none;
|
|
||||||
border-radius: 12px;
|
|
||||||
color: #07101a;
|
|
||||||
background: linear-gradient(270deg, #94d8ff 11.11%, #e0ddff);
|
|
||||||
font-weight: 700;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
opacity: 0.55;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-preview,
|
|
||||||
.empty-detail {
|
|
||||||
color: rgba(255, 255, 255, 0.58);
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-detail {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-title {
|
|
||||||
margin-bottom: 14px;
|
|
||||||
color: rgba(255, 255, 255, 0.86);
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,309 +0,0 @@
|
|||||||
<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>
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
<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>
|
|
||||||
@ -1,493 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="app">
|
|
||||||
<div class="left-panel">
|
|
||||||
<ImgLeft
|
|
||||||
@generation-start="handleGenerationStart"
|
|
||||||
@image-generated="handleImageGenerated"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="right-panel">
|
|
||||||
<el-card class="image-result-card">
|
|
||||||
<div v-if="isLoggedIn" class="result-header">
|
|
||||||
<div>
|
|
||||||
<div class="result-title">生成结果</div>
|
|
||||||
<div class="result-desc">图片生成成功后会直接展示在这里</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
v-if="resultImages.length"
|
|
||||||
type="button"
|
|
||||||
class="clear-btn"
|
|
||||||
@click="clearResults"
|
|
||||||
>
|
|
||||||
清空
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<LoginPrompt
|
|
||||||
v-if="!isLoggedIn"
|
|
||||||
class="image-login-prompt"
|
|
||||||
title="请先登录"
|
|
||||||
description="登录后可创作图片,并在右侧查看生成结果。"
|
|
||||||
full-height
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div v-else-if="isGenerating" class="generating-result">
|
|
||||||
<div class="generating-card">
|
|
||||||
<div class="generating-spinner"></div>
|
|
||||||
<div class="generating-title">图片生成中</div>
|
|
||||||
<div class="generating-desc">正在根据提示词生成图片,请稍候...</div>
|
|
||||||
<div class="generating-progress">
|
|
||||||
<span></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else-if="!resultImages.length" class="empty-result">
|
|
||||||
<img src="../../assets/images/preparation.png" alt="" />
|
|
||||||
<div class="empty-title">暂无生成结果</div>
|
|
||||||
<div class="empty-desc">点击左侧创作后,生成图片会展示在这里。</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="result-content">
|
|
||||||
<div class="usage-bar">
|
|
||||||
<span v-if="latestUsage.size">尺寸 {{ latestUsage.size }}</span>
|
|
||||||
<span v-if="latestUsage.image_count">数量 {{ latestUsage.image_count }}</span>
|
|
||||||
<span v-if="latestUsage.total_tokens">Tokens {{ latestUsage.total_tokens }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="image-grid">
|
|
||||||
<div v-for="item in resultImages" :key="item.id" class="image-item">
|
|
||||||
<div class="image-preview-box">
|
|
||||||
<img :src="item.url" alt="生成图片" />
|
|
||||||
</div>
|
|
||||||
<button type="button" class="download-mask" @click="downloadImage(item)">
|
|
||||||
<i class="iconfont icon-xiazai"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script setup lang='ts'>
|
|
||||||
import ImgLeft from '@/components/img/ImgLeft.vue';
|
|
||||||
import LoginPrompt from '@/components/LoginPrompt/LoginPrompt.vue';
|
|
||||||
import { useLoginStore } from '@/store/LoginStore';
|
|
||||||
import type { TextToImageResponse } from '@/types/model';
|
|
||||||
import { computed, ref, watch } from 'vue';
|
|
||||||
|
|
||||||
interface ResultImage {
|
|
||||||
id: string;
|
|
||||||
url: string;
|
|
||||||
createdAt: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const resultImages = ref<ResultImage[]>([]);
|
|
||||||
const loginStore = useLoginStore();
|
|
||||||
const latestResult = ref<TextToImageResponse | null>(null);
|
|
||||||
const isGenerating = ref(false);
|
|
||||||
|
|
||||||
const isLoggedIn = computed(() => loginStore.isLoggedIn);
|
|
||||||
const latestUsage = computed(() => latestResult.value?.usage || latestResult.value?.data?.usage || {});
|
|
||||||
|
|
||||||
const handleGenerationStart = () => {
|
|
||||||
isGenerating.value = true;
|
|
||||||
latestResult.value = null;
|
|
||||||
resultImages.value = [];
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleImageGenerated = (result: TextToImageResponse) => {
|
|
||||||
const images = result.image || result.data?.image || [];
|
|
||||||
latestResult.value = result;
|
|
||||||
isGenerating.value = false;
|
|
||||||
resultImages.value = images.map((url, index) => ({
|
|
||||||
id: `${Date.now()}-${index}-${url}`,
|
|
||||||
url,
|
|
||||||
createdAt: Date.now(),
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearResults = () => {
|
|
||||||
isGenerating.value = false;
|
|
||||||
latestResult.value = null;
|
|
||||||
resultImages.value = [];
|
|
||||||
};
|
|
||||||
|
|
||||||
const downloadImage = async (item: ResultImage) => {
|
|
||||||
const fileName = `generated-image-${item.createdAt}.png`;
|
|
||||||
try {
|
|
||||||
const response = await fetch(item.url);
|
|
||||||
if (!response.ok) throw new Error('download failed');
|
|
||||||
const blob = await response.blob();
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = url;
|
|
||||||
link.download = fileName;
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
document.body.removeChild(link);
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
} catch {
|
|
||||||
const frameName = 'image-result-download-frame';
|
|
||||||
let iframe = document.querySelector<HTMLIFrameElement>(`iframe[name="${frameName}"]`);
|
|
||||||
if (!iframe) {
|
|
||||||
iframe = document.createElement('iframe');
|
|
||||||
iframe.name = frameName;
|
|
||||||
iframe.style.display = 'none';
|
|
||||||
document.body.appendChild(iframe);
|
|
||||||
}
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = item.url;
|
|
||||||
link.download = fileName;
|
|
||||||
link.target = frameName;
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
document.body.removeChild(link);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
watch(isLoggedIn, (value) => {
|
|
||||||
if (!value) {
|
|
||||||
clearResults();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<style scoped lang="less">
|
|
||||||
:deep(.el-card__body){
|
|
||||||
padding: 0 !important;
|
|
||||||
}
|
|
||||||
/* 禁止页面滚动,并占满全屏 */
|
|
||||||
html, body {
|
|
||||||
height: 100%;
|
|
||||||
margin: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app {
|
|
||||||
display: flex;
|
|
||||||
height: calc(100vh - 80px);
|
|
||||||
width: 100%;
|
|
||||||
gap: 12px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.left-panel {
|
|
||||||
width: 35%; /* 与 leftBox 宽度一致 */
|
|
||||||
height: 100%;
|
|
||||||
min-height: 0;
|
|
||||||
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%;
|
|
||||||
min-width: 0;
|
|
||||||
// min-height: 0;
|
|
||||||
height: calc(100vh - 100px);
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-result-card {
|
|
||||||
flex: 1;
|
|
||||||
height: 100%;
|
|
||||||
border: 1px solid #1a1c20;
|
|
||||||
border-radius: 16px;
|
|
||||||
background: #1a1c20;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
:deep(.el-card__body) {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 22px 22px 24px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-header {
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 16px;
|
|
||||||
margin-bottom: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-title {
|
|
||||||
color: #fff;
|
|
||||||
font-size: 22px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-desc {
|
|
||||||
margin-top: 6px;
|
|
||||||
color: rgba(255, 255, 255, 0.5);
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clear-btn {
|
|
||||||
height: 34px;
|
|
||||||
padding: 0 16px;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
border-radius: 10px;
|
|
||||||
color: rgba(255, 255, 255, 0.78);
|
|
||||||
background: rgba(255, 255, 255, 0.06);
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: #fff;
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-login-prompt {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-result {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: rgba(255, 255, 255, 0.56);
|
|
||||||
border: 1px dashed rgba(255, 255, 255, 0.1);
|
|
||||||
border-radius: 16px;
|
|
||||||
background: rgba(255, 255, 255, 0.025);
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 160px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
opacity: 0.86;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-title {
|
|
||||||
color: rgba(255, 255, 255, 0.82);
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-desc {
|
|
||||||
margin-top: 8px;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.generating-result {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.generating-card {
|
|
||||||
width: min(420px, 80%);
|
|
||||||
min-height: 260px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 32px;
|
|
||||||
border: 1px solid rgba(148, 216, 255, 0.12);
|
|
||||||
border-radius: 16px;
|
|
||||||
background: rgba(255, 255, 255, 0.035);
|
|
||||||
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.18);
|
|
||||||
}
|
|
||||||
|
|
||||||
.generating-spinner {
|
|
||||||
width: 44px;
|
|
||||||
height: 44px;
|
|
||||||
border: 4px solid rgba(255, 255, 255, 0.16);
|
|
||||||
border-top-color: #94d8ff;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: result-rotate 0.9s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.generating-title {
|
|
||||||
margin-top: 18px;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.generating-desc {
|
|
||||||
margin-top: 8px;
|
|
||||||
color: rgba(255, 255, 255, 0.54);
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.generating-progress {
|
|
||||||
width: 240px;
|
|
||||||
height: 6px;
|
|
||||||
overflow: hidden;
|
|
||||||
margin-top: 22px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: rgba(255, 255, 255, 0.12);
|
|
||||||
|
|
||||||
span {
|
|
||||||
display: block;
|
|
||||||
width: 42%;
|
|
||||||
height: 100%;
|
|
||||||
border-radius: inherit;
|
|
||||||
background: linear-gradient(90deg, #82f0ff, #e0ddff);
|
|
||||||
animation: result-progress 1.2s ease-in-out infinite alternate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-content {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.usage-bar {
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 18px;
|
|
||||||
color: rgba(255, 255, 255, 0.66);
|
|
||||||
font-size: 13px;
|
|
||||||
|
|
||||||
span {
|
|
||||||
height: 28px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0 10px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: rgba(255, 255, 255, 0.06);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-grid {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
overflow-y: auto;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(260px, 360px));
|
|
||||||
align-content: start;
|
|
||||||
justify-content: start;
|
|
||||||
gap: 18px;
|
|
||||||
padding: 2px 6px 16px 0;
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: rgba(148, 216, 255, 0.24) transparent;
|
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
border-radius: 999px;
|
|
||||||
background: rgba(148, 216, 255, 0.24);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-item {
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
||||||
border-radius: 16px;
|
|
||||||
background: rgba(255, 255, 255, 0.035);
|
|
||||||
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.22);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: rgba(148, 216, 255, 0.28);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover .download-mask {
|
|
||||||
opacity: 1;
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-preview-box {
|
|
||||||
min-height: 260px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 10px;
|
|
||||||
background:
|
|
||||||
linear-gradient(45deg, rgba(255, 255, 255, 0.035) 25%, transparent 25%),
|
|
||||||
linear-gradient(-45deg, rgba(255, 255, 255, 0.035) 25%, transparent 25%),
|
|
||||||
linear-gradient(45deg, transparent 75%, rgba(255, 255, 255, 0.035) 75%),
|
|
||||||
linear-gradient(-45deg, transparent 75%, rgba(255, 255, 255, 0.035) 75%);
|
|
||||||
background-color: #1a1f2a;
|
|
||||||
background-position: 0 0, 0 8px, 8px -8px, -8px 0;
|
|
||||||
background-size: 16px 16px;
|
|
||||||
|
|
||||||
img {
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 520px;
|
|
||||||
display: block;
|
|
||||||
border-radius: 10px;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-mask {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border: none;
|
|
||||||
color: #fff;
|
|
||||||
background: rgba(0, 0, 0, 0.38);
|
|
||||||
backdrop-filter: blur(2px);
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: opacity 0.18s ease;
|
|
||||||
|
|
||||||
.iconfont {
|
|
||||||
font-size: 34px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes result-rotate {
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes result-progress {
|
|
||||||
from {
|
|
||||||
transform: translateX(-80%);
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
transform: translateX(170%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
@ -1,625 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="text-generate-page">
|
|
||||||
<header class="page-header">
|
|
||||||
<el-select v-model="selectedModel" class="model-select" popper-class="text-model-popper">
|
|
||||||
<template #prefix>
|
|
||||||
<span class="model-icon">✦</span>
|
|
||||||
</template>
|
|
||||||
<el-option
|
|
||||||
v-for="model in modelOptions"
|
|
||||||
:key="model.value"
|
|
||||||
:label="model.label"
|
|
||||||
:value="model.value"
|
|
||||||
/>
|
|
||||||
|
|
||||||
</el-select>
|
|
||||||
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main class="welcome-panel">
|
|
||||||
<div class="aurora aurora-left"></div>
|
|
||||||
<div class="aurora aurora-right"></div>
|
|
||||||
|
|
||||||
<section class="welcome-content">
|
|
||||||
<LoginPrompt
|
|
||||||
v-if="!isLoggedIn"
|
|
||||||
class="text-login-prompt"
|
|
||||||
title="请先登录"
|
|
||||||
description="登录后可使用文本对话生成能力,并查看生成结果。"
|
|
||||||
full-height
|
|
||||||
/>
|
|
||||||
|
|
||||||
<template v-else-if="!messages.length && !isGenerating">
|
|
||||||
<div class="chat-bubble-icon">
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h1>开启智能对话</h1>
|
|
||||||
<p>
|
|
||||||
与 {{ selectedModel || '模型' }} 开始一场流畅的对话。支持多轮上下文理解,随时调整参数探索不同的输出效果。
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="prompt-examples">
|
|
||||||
<button
|
|
||||||
v-for="example in promptExamples"
|
|
||||||
:key="example"
|
|
||||||
type="button"
|
|
||||||
@click="useExample(example)"
|
|
||||||
>
|
|
||||||
{{ example }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="feature-list">
|
|
||||||
<span><i class="blue"></i>用心理解输出</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div v-else ref="messageListRef" class="message-list">
|
|
||||||
<article
|
|
||||||
v-for="message in messages"
|
|
||||||
:key="message.id"
|
|
||||||
:class="['message-card', message.role]"
|
|
||||||
>
|
|
||||||
<div class="message-title">
|
|
||||||
<span v-if="message.role === 'assistant'" class="model-icon">✦</span>
|
|
||||||
<span>{{ message.role === 'assistant' ? message.model : '' }}</span>
|
|
||||||
<!-- <span v-if="message.id === currentAssistantMessageId && isGenerating" class="typing">生成中...</span> -->
|
|
||||||
</div>
|
|
||||||
<div class="message-content">{{ message.content || '正在思考...' }}</div>
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<footer class="composer-bar">
|
|
||||||
<div class="composer">
|
|
||||||
<el-input
|
|
||||||
v-model="prompt"
|
|
||||||
class="composer-input"
|
|
||||||
type="textarea"
|
|
||||||
:autosize="{ minRows: 10, maxRows: 5 }"
|
|
||||||
:disabled="!isLoggedIn"
|
|
||||||
:placeholder="isLoggedIn ? '在这里输入内容,探索模型的无限可能...' : '请先登录后使用文本生成'"
|
|
||||||
@keydown="handleComposerKeydown"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
class="send-btn"
|
|
||||||
type="button"
|
|
||||||
:disabled="isGenerating || !isLoggedIn"
|
|
||||||
@click="textToText"
|
|
||||||
>
|
|
||||||
<span v-if="isGenerating" class="sending-text">...</span>
|
|
||||||
<i v-else class="iconfont icon-a-17Afasong"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { AllModel, textToTextStream } from '@/apis/Model';
|
|
||||||
import LoginPrompt from '@/components/LoginPrompt/LoginPrompt.vue';
|
|
||||||
import { useLoginStore } from '@/store/LoginStore';
|
|
||||||
import type { AllModelResponse, TextToTextParams } from '@/types/model';
|
|
||||||
import { ElMessage } from 'element-plus';
|
|
||||||
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
|
||||||
|
|
||||||
interface ChatMessage {
|
|
||||||
id: string;
|
|
||||||
role: 'user' | 'assistant';
|
|
||||||
content: string;
|
|
||||||
model?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const prompt = ref('');
|
|
||||||
const loginStore = useLoginStore();
|
|
||||||
const selectedModel = ref('qwen3.5-plus');
|
|
||||||
const modelOptions = ref([
|
|
||||||
{ label: 'qwen3.5-plus', value: 'qwen3.5-plus' },
|
|
||||||
{ label: 'qwen-plus', value: 'qwen-plus' },
|
|
||||||
{ label: 'qwen-max', value: 'qwen-max' },
|
|
||||||
]);
|
|
||||||
const messages = ref<ChatMessage[]>([]);
|
|
||||||
const messageListRef = ref<HTMLElement | null>(null);
|
|
||||||
const currentAssistantMessageId = ref('');
|
|
||||||
const isGenerating = ref(false);
|
|
||||||
const isLoggedIn = computed(() => loginStore.isLoggedIn);
|
|
||||||
const promptExamples = [
|
|
||||||
'解析什么是人工智能',
|
|
||||||
'给我讲个有趣的冷知识',
|
|
||||||
'帮我写一份打卡计划',
|
|
||||||
];
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 获取模型
|
|
||||||
const getModel = async () => {
|
|
||||||
interface ModelParams {
|
|
||||||
llmcatelogid: string;
|
|
||||||
}
|
|
||||||
const Props: ModelParams = {
|
|
||||||
llmcatelogid:'t2t'
|
|
||||||
}
|
|
||||||
const res = await AllModel<AllModelResponse, ModelParams>(Props);
|
|
||||||
if(res.status === 'ok' && res.data?.length) {
|
|
||||||
modelOptions.value = res.data.map((item) => ({
|
|
||||||
label: item.model || item.name,
|
|
||||||
value: item.model,
|
|
||||||
}));
|
|
||||||
selectedModel.value = modelOptions.value[0]?.value || selectedModel.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
getModel();
|
|
||||||
})
|
|
||||||
|
|
||||||
const scrollMessagesToBottom = async () => {
|
|
||||||
await nextTick();
|
|
||||||
if (messageListRef.value) {
|
|
||||||
messageListRef.value.scrollTop = messageListRef.value.scrollHeight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 文生文
|
|
||||||
const textToText = async () => {
|
|
||||||
if (isGenerating.value) return;
|
|
||||||
if (!isLoggedIn.value) {
|
|
||||||
ElMessage.warning('请先登录后使用文本生成');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = prompt.value.trim();
|
|
||||||
if (!content) {
|
|
||||||
ElMessage.warning('请输入内容');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!selectedModel.value) {
|
|
||||||
ElMessage.warning('请选择模型');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const params: TextToTextParams = {
|
|
||||||
model: selectedModel.value,
|
|
||||||
catelogid: 't2t',
|
|
||||||
prompt: content,
|
|
||||||
sys_prompt: '',
|
|
||||||
stream: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const userMessage: ChatMessage = {
|
|
||||||
id: `user-${Date.now()}`,
|
|
||||||
role: 'user',
|
|
||||||
content,
|
|
||||||
};
|
|
||||||
const assistantMessage: ChatMessage = {
|
|
||||||
id: `assistant-${Date.now()}`,
|
|
||||||
role: 'assistant',
|
|
||||||
content: '',
|
|
||||||
model: selectedModel.value,
|
|
||||||
};
|
|
||||||
|
|
||||||
messages.value.push(userMessage, assistantMessage);
|
|
||||||
currentAssistantMessageId.value = assistantMessage.id;
|
|
||||||
prompt.value = '';
|
|
||||||
isGenerating.value = true;
|
|
||||||
scrollMessagesToBottom();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await textToTextStream(params, (chunk) => {
|
|
||||||
const assistantIndex = messages.value.findIndex(message => message.id === assistantMessage.id);
|
|
||||||
if (assistantIndex !== -1) {
|
|
||||||
messages.value[assistantIndex].content += chunk;
|
|
||||||
scrollMessagesToBottom();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('文生文失败:', error);
|
|
||||||
const assistantIndex = messages.value.findIndex(message => message.id === assistantMessage.id);
|
|
||||||
if (assistantIndex !== -1 && !messages.value[assistantIndex].content) {
|
|
||||||
messages.value[assistantIndex].content = '生成失败,请稍后重试。';
|
|
||||||
}
|
|
||||||
ElMessage.error(error instanceof Error ? error.message : '生成失败,请重试');
|
|
||||||
} finally {
|
|
||||||
isGenerating.value = false;
|
|
||||||
currentAssistantMessageId.value = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const useExample = (example: string) => {
|
|
||||||
prompt.value = example;
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleComposerKeydown = (event: KeyboardEvent) => {
|
|
||||||
if (event.key !== 'Enter' || event.shiftKey || event.ctrlKey || event.altKey || event.metaKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
textToText();
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(isLoggedIn, (value) => {
|
|
||||||
if (!value) {
|
|
||||||
messages.value = [];
|
|
||||||
isGenerating.value = false;
|
|
||||||
currentAssistantMessageId.value = '';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="less">
|
|
||||||
.text-generate-page {
|
|
||||||
height: calc(100vh - 80px);
|
|
||||||
min-height: 0;
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
|
||||||
color: #fff;
|
|
||||||
background:
|
|
||||||
radial-gradient(circle at 12% 12%, rgba(44, 255, 248, 0.16), transparent 28%),
|
|
||||||
radial-gradient(circle at 88% 78%, rgba(44, 104, 255, 0.2), transparent 34%),
|
|
||||||
linear-gradient(135deg, #0f0f19 0%, #141621 54%, #10131f 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header {
|
|
||||||
flex-shrink: 0;
|
|
||||||
height: 56px;
|
|
||||||
padding: 0 10px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
|
||||||
background: rgba(15, 15, 25, 0.82);
|
|
||||||
backdrop-filter: blur(16px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.model-select {
|
|
||||||
width: 16%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.model-icon {
|
|
||||||
color: #00ffcc;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.params-empty {
|
|
||||||
height: 32px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0 12px;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
||||||
border-radius: 16px;
|
|
||||||
color: rgba(255, 255, 255, 0.46);
|
|
||||||
background: rgba(255, 255, 255, 0.04);
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.welcome-panel {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: stretch;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 20px 4px 150px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aurora {
|
|
||||||
position: absolute;
|
|
||||||
width: 340px;
|
|
||||||
height: 340px;
|
|
||||||
border-radius: 50%;
|
|
||||||
filter: blur(58px);
|
|
||||||
opacity: 0.42;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aurora-left {
|
|
||||||
left: 40px;
|
|
||||||
top: 12px;
|
|
||||||
background: rgba(0, 255, 204, 0.24);
|
|
||||||
}
|
|
||||||
|
|
||||||
.aurora-right {
|
|
||||||
right: 24px;
|
|
||||||
bottom: 64px;
|
|
||||||
background: rgba(44, 104, 255, 0.28);
|
|
||||||
}
|
|
||||||
|
|
||||||
.welcome-content {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
width: min(1120px, 100%);
|
|
||||||
min-height: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
margin: 22px 0 10px;
|
|
||||||
font-size: 28px;
|
|
||||||
line-height: 1.35;
|
|
||||||
font-weight: 700;
|
|
||||||
background: linear-gradient(90deg, #00ffcc, #94d8ff 56%, #e0ddff);
|
|
||||||
background-clip: text;
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
max-width: 560px;
|
|
||||||
margin: 0;
|
|
||||||
color: rgba(255, 255, 255, 0.62);
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-login-prompt {
|
|
||||||
width: 100%;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-bubble-icon {
|
|
||||||
width: 58px;
|
|
||||||
height: 42px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 5px;
|
|
||||||
border-radius: 14px;
|
|
||||||
background: linear-gradient(135deg, #2cfff8, #2c68ff);
|
|
||||||
box-shadow: 0 14px 30px rgba(44, 104, 255, 0.36);
|
|
||||||
|
|
||||||
span {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: rgba(255, 255, 255, 0.86);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt-examples {
|
|
||||||
margin-top: 28px;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 12px;
|
|
||||||
|
|
||||||
button {
|
|
||||||
height: 34px;
|
|
||||||
padding: 0 18px;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
||||||
border-radius: 10px;
|
|
||||||
background: rgba(255, 255, 255, 0.06);
|
|
||||||
color: rgba(255, 255, 255, 0.76);
|
|
||||||
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.12);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: rgba(0, 255, 204, 0.38);
|
|
||||||
color: #00ffcc;
|
|
||||||
background: rgba(0, 255, 204, 0.08);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-list {
|
|
||||||
margin-top: 28px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 28px;
|
|
||||||
color: rgba(255, 255, 255, 0.52);
|
|
||||||
font-size: 12px;
|
|
||||||
|
|
||||||
span {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
width: 7px;
|
|
||||||
height: 7px;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.green {
|
|
||||||
background: #35d399;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blue {
|
|
||||||
background: #5b8cff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.purple {
|
|
||||||
background: #a66cff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-list {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 18px;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 8px 6px 72px;
|
|
||||||
scrollbar-width: none;
|
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-card {
|
|
||||||
width: fit-content;
|
|
||||||
max-width: min(860px, 90%);
|
|
||||||
padding: 16px 18px;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
border-radius: 16px;
|
|
||||||
box-shadow: 0 14px 38px rgba(0, 0, 0, 0.24);
|
|
||||||
backdrop-filter: blur(18px);
|
|
||||||
text-align: left;
|
|
||||||
|
|
||||||
&.assistant {
|
|
||||||
align-self: flex-start;
|
|
||||||
background: rgba(20, 22, 33, 0.86);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.user {
|
|
||||||
align-self: flex-end;
|
|
||||||
max-width: min(520px, 78%);
|
|
||||||
border-color: rgba(0, 255, 204, 0.18);
|
|
||||||
background: linear-gradient(135deg, rgba(0, 255, 204, 0.18), rgba(44, 104, 255, 0.14));
|
|
||||||
|
|
||||||
.message-title {
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.typing {
|
|
||||||
margin-left: auto;
|
|
||||||
color: rgba(255, 255, 255, 0.48);
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-content {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-word;
|
|
||||||
color: rgba(255, 255, 255, 0.82);
|
|
||||||
font-size: 15px;
|
|
||||||
line-height: 1.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.composer-bar {
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
right: auto;
|
|
||||||
bottom: 10px;
|
|
||||||
width: min(1120px, calc(100% - 8px));
|
|
||||||
transform: translateX(-50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.composer {
|
|
||||||
min-height: 80px;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 10px 10px 10px 14px;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
border-radius: 14px;
|
|
||||||
background: rgba(20, 22, 33, 0.92);
|
|
||||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.36);
|
|
||||||
backdrop-filter: blur(18px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.composer-input {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.send-btn {
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
border: none;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: linear-gradient(270deg, #94d8ff 11.11%, #e0ddff);
|
|
||||||
color: #0f0f19;
|
|
||||||
font-size: 18px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
opacity: 0.55;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.sending-text {
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.composer-input .el-textarea__inner) {
|
|
||||||
border: none;
|
|
||||||
box-shadow: none;
|
|
||||||
resize: none;
|
|
||||||
color: #fff;
|
|
||||||
background: transparent;
|
|
||||||
min-height: 40px !important;
|
|
||||||
max-height: 120px;
|
|
||||||
padding: 8px 0 6px;
|
|
||||||
line-height: 24px;
|
|
||||||
overflow-y: auto;
|
|
||||||
scrollbar-width: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.composer-input .el-textarea__inner::-webkit-scrollbar) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.composer-input .el-textarea__inner::placeholder) {
|
|
||||||
color: rgba(255, 255, 255, 0.42);
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.model-select .el-select__wrapper) {
|
|
||||||
min-height: 34px;
|
|
||||||
border-radius: 17px;
|
|
||||||
background: rgba(255, 255, 255, 0.06);
|
|
||||||
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.08) inset;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.model-select .el-select__selected-item) {
|
|
||||||
color: #fff;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.model-select .el-select__caret) {
|
|
||||||
color: rgba(255, 255, 255, 0.62);
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.text-model-popper) {
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1) !important;
|
|
||||||
background: #141621 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.text-model-popper .el-select-dropdown__item) {
|
|
||||||
color: rgba(255, 255, 255, 0.72) !important;
|
|
||||||
background: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.text-model-popper .el-select-dropdown__item.is-selected),
|
|
||||||
:global(.text-model-popper .el-select-dropdown__item.selected),
|
|
||||||
:global(.text-model-popper .el-select-dropdown__item.is-hovering),
|
|
||||||
:global(.text-model-popper .el-select-dropdown__item.hover),
|
|
||||||
:global(.text-model-popper .el-select-dropdown__item:hover) {
|
|
||||||
color: #00ffcc !important;
|
|
||||||
background: rgba(0, 255, 204, 0.08) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.text-model-popper .el-select-dropdown__list) {
|
|
||||||
background: #141621 !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,105 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="index" ref="indexContainer">
|
|
||||||
<div class="top"><Top /></div>
|
|
||||||
<div class="AI"><AI /></div>
|
|
||||||
|
|
||||||
<!-- Tab 栏,后续通过添加 sticky 类实现吸顶 -->
|
|
||||||
<div class="sticky-tabs" :class="{ 'is-sticky': isTabSticky }">
|
|
||||||
<StencilTabs :activeTab="activeTab" @tab-change="handleTabChange" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 瀑布流内容 -->
|
|
||||||
<div class="stencil">
|
|
||||||
<StencilWaterfall
|
|
||||||
ref="waterfallRef"
|
|
||||||
:activeTab="activeTab"
|
|
||||||
:gap="16"
|
|
||||||
@like="handleLike"
|
|
||||||
@make-same="handleMakeSame"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, onMounted, onUnmounted } from 'vue'
|
|
||||||
import Top from '@/components/index/top.vue'
|
|
||||||
import AI from '@/components/index/AIMode.vue'
|
|
||||||
import StencilTabs from '@/components/index/StencilTabs.vue'
|
|
||||||
import StencilWaterfall from '@/components/index/StencilWaterfall.vue'
|
|
||||||
// import type { StencilItem } from '@/types/home'
|
|
||||||
|
|
||||||
const activeTab = ref('mixed') // 默认推荐
|
|
||||||
|
|
||||||
const handleTabChange = (tabType: string) => {
|
|
||||||
activeTab.value = tabType
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleLike = () => {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleMakeSame = () => {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== 吸顶逻辑(示例,可根据实际滚动容器调整)=====
|
|
||||||
const indexContainer = ref<HTMLElement>()
|
|
||||||
const isTabSticky = ref(false)
|
|
||||||
const tabsOffsetTop = ref(0)
|
|
||||||
|
|
||||||
const handleScroll = () => {
|
|
||||||
if (!indexContainer.value) return
|
|
||||||
const scrollTop = indexContainer.value.scrollTop
|
|
||||||
// 当滚动超过 Tab 栏原始位置时,添加 sticky 类
|
|
||||||
isTabSticky.value = scrollTop >= tabsOffsetTop.value
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
const container = indexContainer.value
|
|
||||||
if (container) {
|
|
||||||
container.addEventListener('scroll', handleScroll)
|
|
||||||
// 获取 Tab 栏距离滚动容器顶部的初始偏移量
|
|
||||||
const tabsEl = container.querySelector('.sticky-tabs') as HTMLElement
|
|
||||||
if (tabsEl) {
|
|
||||||
tabsOffsetTop.value = tabsEl.offsetTop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
indexContainer.value?.removeEventListener('scroll', handleScroll)
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
.index {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
overflow-y: auto;
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
}
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
background-color: rgba(255, 255, 255, 0.3);
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
&::-webkit-scrollbar-track {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.sticky-tabs {
|
|
||||||
position: relative;
|
|
||||||
z-index: 10;
|
|
||||||
background-color: #0f0f0f; /* 与页面背景一致,吸顶时遮盖内容 */
|
|
||||||
transition: box-shadow 0.2s;
|
|
||||||
|
|
||||||
&.is-sticky {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,262 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="login">
|
|
||||||
<!-- 背景视频 -->
|
|
||||||
<div class="background-video-stack">
|
|
||||||
<video
|
|
||||||
class="background-video"
|
|
||||||
autoplay
|
|
||||||
muted
|
|
||||||
loop
|
|
||||||
playsinline
|
|
||||||
crossorigin="anonymous"
|
|
||||||
src="../../assets/images/loginBg.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"
|
|
||||||
@submit.prevent="submitForm"
|
|
||||||
>
|
|
||||||
<!-- 手机号 -->
|
|
||||||
<el-form-item prop="cellphone">
|
|
||||||
<el-input
|
|
||||||
v-model="form.cellphone"
|
|
||||||
placeholder="请输入手机号"
|
|
||||||
:prefix-icon="Iphone"
|
|
||||||
size="large"
|
|
||||||
@keyup.enter="submitForm"
|
|
||||||
/>
|
|
||||||
</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"
|
|
||||||
@keyup.enter="submitForm"
|
|
||||||
/>
|
|
||||||
<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 } from '@/apis/login';
|
|
||||||
import type { CodeResult, LoginParams, CaptchaParams } from '@/types/login';
|
|
||||||
import { useLoginStore } from '@/store/LoginStore';
|
|
||||||
import { debounce } from '@/utlis/debounce';
|
|
||||||
|
|
||||||
// 登录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 requestCaptcha = 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 handleGetCaptcha = debounce(requestCaptcha, 500);
|
|
||||||
|
|
||||||
// 提交表单(登录)
|
|
||||||
const doSubmitForm = async () => {
|
|
||||||
if (!formRef.value) return;
|
|
||||||
if (loading.value) return;
|
|
||||||
|
|
||||||
await formRef.value.validate(async (valid) => {
|
|
||||||
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 {
|
|
||||||
ElMessage.error('请正确填写表单');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const submitForm = debounce(doSubmitForm, 500);
|
|
||||||
|
|
||||||
// 选择账号
|
|
||||||
const handleSelectAccount = 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 selectAccount = debounce(handleSelectAccount, 500);
|
|
||||||
|
|
||||||
// 弹窗关闭时的回调(如果用户直接关闭,清空账号列表,避免下次残留)
|
|
||||||
const handleDialogClose = () => {
|
|
||||||
loginStore.accountList = [];
|
|
||||||
};
|
|
||||||
|
|
||||||
// 组件卸载时清除定时器
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (timer) clearInterval(timer);
|
|
||||||
handleGetCaptcha.cancel();
|
|
||||||
submitForm.cancel();
|
|
||||||
selectAccount.cancel();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="less">
|
|
||||||
@import url('../../assets/less/Login/Login.less');
|
|
||||||
</style>
|
|
||||||
@ -1,529 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="material-detail">
|
|
||||||
<div v-if="currentItem" class="detail-layout">
|
|
||||||
<section class="player-panel">
|
|
||||||
<button class="back-btn" type="button" @click="goBack">
|
|
||||||
<i class="iconfont icon-zuo"></i>
|
|
||||||
返回
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="video-card">
|
|
||||||
<video
|
|
||||||
:key="currentItem.id"
|
|
||||||
:src="currentItem.src"
|
|
||||||
:poster="currentItem.poster"
|
|
||||||
class="main-video"
|
|
||||||
controls
|
|
||||||
autoplay
|
|
||||||
playsinline
|
|
||||||
></video>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<aside class="side-panel">
|
|
||||||
<div class="info-card">
|
|
||||||
<div class="meta-row">
|
|
||||||
<!-- <span class="type-tag">{{ currentItem.displayTag || getTypeLabel(currentItem.contentType) }}</span> -->
|
|
||||||
<span class="publish-time">{{ formatDate(currentItem.createdAt) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="creator">
|
|
||||||
<img
|
|
||||||
v-if="currentItem.avatar"
|
|
||||||
:src="currentItem.avatar"
|
|
||||||
:alt="currentItem.userName"
|
|
||||||
class="creator-avatar"
|
|
||||||
/>
|
|
||||||
<div class="creator-info">
|
|
||||||
<div class="creator-name">{{ currentItem.userName || '匿名创作者' }}</div>
|
|
||||||
<div class="creator-subtitle">{{ getTypeLabel(currentItem.contentType) }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h1 class="title">{{ currentItem.title || '未命名作品' }}</h1>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<p class="description">
|
|
||||||
{{ currentItem.description || '暂无作品描述' }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="stats">
|
|
||||||
<div class="stat-item">
|
|
||||||
<span class="stat-label">点赞</span>
|
|
||||||
<span class="stat-value">{{ currentItem.likes }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="stat-item">
|
|
||||||
<span class="stat-label">收藏</span>
|
|
||||||
<span class="stat-value">{{ currentItem.collectCount ?? 0 }}</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="currentItem.recreateCount !== undefined" class="stat-item">
|
|
||||||
<span class="stat-label">复用</span>
|
|
||||||
<span class="stat-value">{{ currentItem.recreateCount }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="relatedItems.length" class="recommend-card">
|
|
||||||
<div class="recommend-title">相关内容</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
v-for="item in relatedItems"
|
|
||||||
:key="item.id"
|
|
||||||
type="button"
|
|
||||||
class="recommend-item"
|
|
||||||
:class="{ active: item.id === selectedId }"
|
|
||||||
@click="selectItem(item)"
|
|
||||||
>
|
|
||||||
<img :src="item.poster" :alt="item.title" class="recommend-cover" />
|
|
||||||
<div class="recommend-info">
|
|
||||||
<div class="recommend-name">{{ item.title || item.userName }}</div>
|
|
||||||
<div class="recommend-author">{{ item.userName || '匿名创作者' }}</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="empty-state">
|
|
||||||
<div class="empty-title">暂无详情数据</div>
|
|
||||||
<div class="empty-desc">请从素材瀑布流列表中点击任意卡片进入详情页。</div>
|
|
||||||
<button class="back-home-btn" type="button" @click="goBack">返回列表</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, ref, watch } from 'vue'
|
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
|
||||||
import type { StencilItem } from '@/types/home'
|
|
||||||
|
|
||||||
interface DetailCache {
|
|
||||||
selectedId?: string
|
|
||||||
items?: StencilItem[]
|
|
||||||
activeTab?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const DETAIL_STORAGE_KEY = 'material-square-detail'
|
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const items = ref<StencilItem[]>([])
|
|
||||||
const selectedId = ref('')
|
|
||||||
|
|
||||||
const readCache = (): DetailCache | null => {
|
|
||||||
const raw = sessionStorage.getItem(DETAIL_STORAGE_KEY)
|
|
||||||
if (!raw) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return JSON.parse(raw) as DetailCache
|
|
||||||
} catch (error) {
|
|
||||||
console.error('读取详情缓存失败', error)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const writeCache = () => {
|
|
||||||
sessionStorage.setItem(
|
|
||||||
DETAIL_STORAGE_KEY,
|
|
||||||
JSON.stringify({
|
|
||||||
selectedId: selectedId.value,
|
|
||||||
items: items.value,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const syncFromCache = () => {
|
|
||||||
const cache = readCache()
|
|
||||||
const routeId = typeof route.query.id === 'string' ? route.query.id : ''
|
|
||||||
|
|
||||||
items.value = cache?.items ?? []
|
|
||||||
selectedId.value = routeId || cache?.selectedId || items.value[0]?.id || ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentItem = computed(() => {
|
|
||||||
return items.value.find((item) => item.id === selectedId.value) ?? null
|
|
||||||
})
|
|
||||||
|
|
||||||
const relatedItems = computed(() => {
|
|
||||||
return items.value.filter((item) => item.id !== selectedId.value).slice(0, 12)
|
|
||||||
})
|
|
||||||
|
|
||||||
const getTypeLabel = (type: string) => {
|
|
||||||
const typeMap: Record<string, string> = {
|
|
||||||
short_film: '短片',
|
|
||||||
tutorial: '教程',
|
|
||||||
media_asset: '素材',
|
|
||||||
share_elements: '参考素材',
|
|
||||||
}
|
|
||||||
|
|
||||||
return typeMap[type] || '作品'
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatDate = (value: string) => {
|
|
||||||
if (!value) {
|
|
||||||
return '未知时间'
|
|
||||||
}
|
|
||||||
|
|
||||||
const date = new Date(value)
|
|
||||||
if (Number.isNaN(date.getTime())) {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Intl.DateTimeFormat('zh-CN', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: '2-digit',
|
|
||||||
day: '2-digit',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
}).format(date)
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectItem = (item: StencilItem) => {
|
|
||||||
selectedId.value = item.id
|
|
||||||
writeCache()
|
|
||||||
|
|
||||||
router.replace({
|
|
||||||
path: '/material-square/detail',
|
|
||||||
query: {
|
|
||||||
...route.query,
|
|
||||||
id: item.id,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const goBack = () => {
|
|
||||||
if (window.history.length > 1) {
|
|
||||||
router.back()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
router.push('/index')
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => route.query.id,
|
|
||||||
() => {
|
|
||||||
syncFromCache()
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="less">
|
|
||||||
.material-detail {
|
|
||||||
width: 100%;
|
|
||||||
height: 100vh;
|
|
||||||
padding: 24px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
background: #0f0f19;
|
|
||||||
color: #fff;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-layout {
|
|
||||||
display: flex;
|
|
||||||
gap: 24px;
|
|
||||||
height: calc(100vh - 48px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-panel {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.back-btn,
|
|
||||||
.back-home-btn {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
justify-content: center;
|
|
||||||
.iconfont{
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
height: 40px;
|
|
||||||
padding: 0 18px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
||||||
border-radius: 999px;
|
|
||||||
background: rgba(255, 255, 255, 0.06);
|
|
||||||
color: #fff;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-card,
|
|
||||||
.info-card,
|
|
||||||
.recommend-card {
|
|
||||||
background: linear-gradient(180deg, rgba(26, 28, 35, 0.98), rgba(13, 14, 19, 0.98));
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
||||||
border-radius: 24px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-card {
|
|
||||||
height: calc(100% - 56px);
|
|
||||||
min-height: 640px;
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-video {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: block;
|
|
||||||
border-radius: 18px;
|
|
||||||
background: #000;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.side-panel {
|
|
||||||
width: 500px;
|
|
||||||
display: flex;
|
|
||||||
gap: 16px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-card {
|
|
||||||
flex: 1;
|
|
||||||
padding: 20px;
|
|
||||||
height: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.type-tag {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
height: 28px;
|
|
||||||
padding: 0 12px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: rgba(71, 117, 255, 0.22);
|
|
||||||
color: #b9ccff;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.publish-time {
|
|
||||||
color: rgba(255, 255, 255, 0.5);
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
margin: 0 0 16px;
|
|
||||||
font-size: 20px;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.creator {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
margin-bottom: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.creator-avatar {
|
|
||||||
width: 44px;
|
|
||||||
height: 44px;
|
|
||||||
border-radius: 50%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.creator-name {
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.creator-subtitle {
|
|
||||||
margin-top: 4px;
|
|
||||||
color: rgba(255, 255, 255, 0.6);
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
|
||||||
margin: 0;
|
|
||||||
color: rgba(255, 255, 255, 0.82);
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.75;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
||||||
gap: 10px;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-item {
|
|
||||||
padding: 12px;
|
|
||||||
border-radius: 14px;
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
color: rgba(255, 255, 255, 0.56);
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-value {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recommend-card {
|
|
||||||
width: 120px;
|
|
||||||
height: 100%;
|
|
||||||
padding: 12px 10px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
overflow-y: auto;
|
|
||||||
position: relative;
|
|
||||||
right: -4px;
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: rgba(122, 144, 214, 0.18) transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recommend-card::-webkit-scrollbar {
|
|
||||||
width: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recommend-card::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
border-radius: 999px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recommend-card::-webkit-scrollbar-thumb {
|
|
||||||
background: rgba(132, 158, 255, 0.14);
|
|
||||||
border-radius: 999px;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.recommend-card:hover::-webkit-scrollbar {
|
|
||||||
margin-left: 8px;
|
|
||||||
width: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recommend-card:hover::-webkit-scrollbar-track {
|
|
||||||
background: rgba(255, 255, 255, 0.03);
|
|
||||||
}
|
|
||||||
|
|
||||||
.recommend-card:hover::-webkit-scrollbar-thumb {
|
|
||||||
background: rgba(132, 158, 255, 0.38);
|
|
||||||
}
|
|
||||||
|
|
||||||
.recommend-card::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: rgba(154, 178, 255, 0.58);
|
|
||||||
}
|
|
||||||
|
|
||||||
.recommend-title {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
color: rgba(255, 255, 255, 0.68);
|
|
||||||
font-size: 12px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recommend-item {
|
|
||||||
width: 100%;
|
|
||||||
height: 50px;
|
|
||||||
padding: 0;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
border: 0;
|
|
||||||
background: transparent;
|
|
||||||
color: #fff;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recommend-item.active .recommend-cover,
|
|
||||||
.recommend-item:hover .recommend-cover {
|
|
||||||
border-color: rgba(130, 161, 255, 0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
.recommend-cover {
|
|
||||||
width: 100%;
|
|
||||||
height: 80px;
|
|
||||||
// aspect-ratio: 9 / 16;
|
|
||||||
display: block;
|
|
||||||
border: 2px solid transparent;
|
|
||||||
border-radius: 14px;
|
|
||||||
object-fit: cover;
|
|
||||||
background: #111;
|
|
||||||
img{
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.recommend-info {
|
|
||||||
margin-top: 6px;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recommend-name,
|
|
||||||
.recommend-author {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recommend-name {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recommend-author {
|
|
||||||
margin-top: 2px;
|
|
||||||
color: rgba(255, 255, 255, 0.55);
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state {
|
|
||||||
height: calc(100vh - 48px);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-title {
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-desc {
|
|
||||||
color: rgba(255, 255, 255, 0.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1280px) {
|
|
||||||
.detail-layout {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.side-panel {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recommend-card {
|
|
||||||
width: 100%;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recommend-item {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,91 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="app">
|
|
||||||
<div class="left-panel">
|
|
||||||
<VideoLeft ref="videoLeftRef" @task-created="handleTaskCreated" />
|
|
||||||
</div>
|
|
||||||
<div class="right-panel">
|
|
||||||
<RightResult ref="rightResultRef" :llmcatelogid="currentLlmcatelogid" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script setup lang='ts'>
|
|
||||||
import { computed, ref } from 'vue';
|
|
||||||
import { useRoute } from 'vue-router';
|
|
||||||
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 route = useRoute();
|
|
||||||
|
|
||||||
const currentLlmcatelogid = computed(() => {
|
|
||||||
if (route.path.includes('/videoGenerate/image')) return 'i2v';
|
|
||||||
if (route.path.includes('/videoGenerate/text')) return 't2v';
|
|
||||||
return 'r2v';
|
|
||||||
});
|
|
||||||
|
|
||||||
// 处理任务创建事件,刷新任务列表
|
|
||||||
const handleTaskCreated = () => {
|
|
||||||
if (rightResultRef.value && typeof rightResultRef.value.refreshTaskList === 'function') {
|
|
||||||
rightResultRef.value.refreshTaskList();
|
|
||||||
} 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
7
src/vite-env.d.ts
vendored
@ -1,7 +0,0 @@
|
|||||||
/// <reference types="vite/client" />
|
|
||||||
|
|
||||||
declare module '*.vue' {
|
|
||||||
import type { DefineComponent } from 'vue'
|
|
||||||
const component: DefineComponent<{}, {}, any>
|
|
||||||
export default component
|
|
||||||
}
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"files": [],
|
|
||||||
"references": [
|
|
||||||
{ "path": "./tsconfig.app.json" },
|
|
||||||
{ "path": "./tsconfig.node.json" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"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"]
|
|
||||||
}
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
import { defineConfig, loadEnv } from 'vite'
|
|
||||||
import vue from '@vitejs/plugin-vue'
|
|
||||||
import { fileURLToPath, URL } from 'node:url'
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// https://vite.dev/config/
|
|
||||||
export default defineConfig(({ mode }) => {
|
|
||||||
const env = loadEnv(mode, process.cwd(), '')
|
|
||||||
const apiBaseURL = env.VITE_API_BASE_URL || 'https://token.opencomputing.cn'
|
|
||||||
|
|
||||||
return {
|
|
||||||
plugins: [vue(),],
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
server: {
|
|
||||||
host: 'localhost',
|
|
||||||
port: 5173,
|
|
||||||
// 配置代理解决跨域问题
|
|
||||||
proxy: {
|
|
||||||
'/api': {
|
|
||||||
target: apiBaseURL,
|
|
||||||
changeOrigin: true,
|
|
||||||
rewrite: (path) => path.replace(/^\/api/, ''),
|
|
||||||
},
|
|
||||||
'/token':{
|
|
||||||
target: apiBaseURL,
|
|
||||||
changeOrigin: true,
|
|
||||||
rewrite: (path) => path.replace(/^\/token/, ''),
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// optimizeDeps: {
|
|
||||||
// exclude: ['@techui/scifi']
|
|
||||||
// },
|
|
||||||
}
|
|
||||||
})
|
|
||||||
1
yuanjing
1
yuanjing
@ -1 +0,0 @@
|
|||||||
Subproject commit ef717c6c46a0c261e885443d6878e34f046adfbe
|
|
||||||
Loading…
x
Reference in New Issue
Block a user