This commit is contained in:
hrx 2026-04-08 18:08:16 +08:00
parent c513814c0a
commit 64af489397
15 changed files with 1640 additions and 462 deletions

View File

@ -1,7 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted } from 'vue'; import { onMounted } from 'vue';
import { useLoginStore } from './store/LoginStore'; import { useLoginStore } from './store/LoginStore';
import { ElConfigProvider } from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
const loginStore = useLoginStore(); const loginStore = useLoginStore();
onMounted(() => { onMounted(() => {
@ -11,10 +12,12 @@ onMounted(() => {
</script> </script>
<template> <template>
<el-config-provider :locale="zhCn">
<div> <div>
<router-view> <router-view>
</router-view> </router-view>
</div> </div>
</el-config-provider>
</template> </template>
<style lang="less"> <style lang="less">

View File

@ -1,12 +1,16 @@
import { get, post } from '../utlis/request' import { get, post } from '../utlis/request'
// 获取视频列表 // 获取视频列表
export const getVideos = <T,D>(data:D):Promise<T> =>get('http://180.76.111.84:9028/v1/feed/list',data) export const 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('/api/unipay/user_recharge.dspy', data) export const recharge = <T, D>(data: D): Promise<T> => post('/api/unipay/user_recharge.dspy', data)
// 开户 // 开户
export const openAccount = <T>(): Promise<T> => post('/api/accounting/open_customer_accounts.dspy') export const openAccount = <T>(): Promise<T> => post('/api/accounting/open_customer_accounts.dspy')
// 余额 // 余额
export const balance = <T>(): Promise<T> => get('/api/accounting/mybalance.dspy') export const balance = <T>(): Promise<T> => get('/api/accounting/mybalance.dspy')
// 充值记录
export const consumptionRecord = <T,D>(data:D):Promise<T> => get('/api/platformbiz/query_orders.dspy',data)

View File

@ -54,6 +54,12 @@
<div class="content unicode" style="display: block;"> <div class="content unicode" style="display: block;">
<ul class="icon_lists dib-box"> <ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont">&#xe654;</span>
<div class="name">消费记录</div>
<div class="code-name">&amp;#xe654;</div>
</li>
<li class="dib"> <li class="dib">
<span class="icon iconfont">&#xe615;</span> <span class="icon iconfont">&#xe615;</span>
<div class="name">余额</div> <div class="name">余额</div>
@ -174,9 +180,9 @@
<pre><code class="language-css" <pre><code class="language-css"
>@font-face { >@font-face {
font-family: 'iconfont'; font-family: 'iconfont';
src: url('iconfont.woff2?t=1774432601927') format('woff2'), src: url('iconfont.woff2?t=1775638325739') format('woff2'),
url('iconfont.woff?t=1774432601927') format('woff'), url('iconfont.woff?t=1775638325739') format('woff'),
url('iconfont.ttf?t=1774432601927') format('truetype'); url('iconfont.ttf?t=1775638325739') format('truetype');
} }
</code></pre> </code></pre>
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3> <h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@ -202,6 +208,15 @@
<div class="content font-class"> <div class="content font-class">
<ul class="icon_lists dib-box"> <ul class="icon_lists dib-box">
<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"> <li class="dib">
<span class="icon iconfont icon-yue"></span> <span class="icon iconfont icon-yue"></span>
<div class="name"> <div class="name">
@ -382,6 +397,14 @@
<div class="content symbol"> <div class="content symbol">
<ul class="icon_lists dib-box"> <ul class="icon_lists dib-box">
<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"> <li class="dib">
<svg class="icon svg-icon" aria-hidden="true"> <svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-yue"></use> <use xlink:href="#icon-yue"></use>

View File

@ -1,8 +1,8 @@
@font-face { @font-face {
font-family: "iconfont"; /* Project id 5136943 */ font-family: "iconfont"; /* Project id 5136943 */
src: url('iconfont.woff2?t=1774432601927') format('woff2'), src: url('iconfont.woff2?t=1775638325739') format('woff2'),
url('iconfont.woff?t=1774432601927') format('woff'), url('iconfont.woff?t=1775638325739') format('woff'),
url('iconfont.ttf?t=1774432601927') format('truetype'); url('iconfont.ttf?t=1775638325739') format('truetype');
} }
.iconfont { .iconfont {
@ -13,6 +13,10 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.icon-xiaofeijilu:before {
content: "\e654";
}
.icon-yue:before { .icon-yue:before {
content: "\e615"; content: "\e615";
} }

File diff suppressed because one or more lines are too long

View File

@ -5,6 +5,13 @@
"css_prefix_text": "icon-", "css_prefix_text": "icon-",
"description": "", "description": "",
"glyphs": [ "glyphs": [
{
"icon_id": "1020316",
"name": "消费记录",
"font_class": "xiaofeijilu",
"unicode": "e654",
"unicode_decimal": 58964
},
{ {
"icon_id": "2681721", "icon_id": "2681721",
"name": "余额", "name": "余额",

Binary file not shown.

View File

@ -18,6 +18,12 @@
<span class="menu-text">首页</span> <span class="menu-text">首页</span>
</div> </div>
</el-menu-item> </el-menu-item>
<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> </el-menu>
</div> </div>
@ -96,6 +102,9 @@ const handleSelect = (index: string) => {
case 'index': case 'index':
router.push('/index'); router.push('/index');
break; break;
case 'bill':
router.push('/bill');
break;
case 'discover': case 'discover':
router.push('/discover'); router.push('/discover');
break; break;
@ -127,6 +136,8 @@ const updateActiveIndex = () => {
const path = route.path const path = route.path
if (path.includes('/index')) { if (path.includes('/index')) {
activeIndex.value = 'index'; activeIndex.value = 'index';
} else if (path.includes('/bill')) {
activeIndex.value = 'bill';
} else if (path.includes('/discover')) { } else if (path.includes('/discover')) {
activeIndex.value = 'discover'; activeIndex.value = 'discover';
} else if (path.includes('/profile')) { } else if (path.includes('/profile')) {

View File

@ -10,47 +10,46 @@
{{ field.label }} {{ field.label }}
<span v-if="field.required" class="required-star">*</span> <span v-if="field.required" class="required-star">*</span>
</div> </div>
<el-upload action="#" list-type="picture-card" :auto-upload="false" <!-- 已上传图片时显示预览 -->
:on-change="(file: UploadFile) => handleFileChange(file, field.name)" <div v-if="uploadedFiles[field.name]" class="image-preview-container">
:on-remove="() => handleRemove(field.name)" :file-list="getFileList(field.name)" :limit="1" <img :src="uploadedFiles[field.name]" :alt="field.label" class="preview-image" />
:on-exceed="() => ElMessage.warning('每个字段只能上传一张图片')" <div class="preview-actions">
:disabled="generating" class="upload"> <span class="preview-action" @click="handlePictureCardPreview({ url: uploadedFiles[field.name] } as UploadFile)">
<el-icon> <el-icon><ZoomIn /></el-icon>
<Plus />
</el-icon>
<template #file="{ file }">
<div>
<img class="el-upload-list__item-thumbnail" :src="file.url" alt="" />
<span class="el-upload-list__item-actions">
<span class="el-upload-list__item-preview"
@click="handlePictureCardPreview(file)">
<el-icon>
<ZoomIn />
</el-icon>
</span>
<span v-if="!disabled" class="el-upload-list__item-delete"
@click="handleDownload(field.name)">
<el-icon>
<Download />
</el-icon>
</span>
<span v-if="!disabled" class="el-upload-list__item-delete"
@click="handleRemove(field.name)">
<el-icon>
<Delete />
</el-icon>
</span> </span>
<span class="preview-action" @click="handleRemove(field.name)">
<el-icon><Delete /></el-icon>
</span> </span>
</div> </div>
</template> </div>
<!-- 未上传时显示上传框(已有图片时禁用上传) -->
<el-upload
v-else
action="#"
list-type="picture-card"
:auto-upload="false"
:on-change="(file: UploadFile) => handleFileChange(file, field.name)"
:limit="1"
:on-exceed="() => ElMessage.warning('每个字段只能上传一张图片')"
:disabled="generating || !!uploadedFiles[field.name]"
class="upload"
>
<el-icon><Plus /></el-icon>
</el-upload> </el-upload>
</div> </div>
</div> </div>
<!-- 文本输入框(绑定 prompt 字段) --> <!-- 文本输入框(绑定 prompt 字段) -->
<div v-if="hasPromptField" class="input"> <div v-if="hasPromptField" class="input">
<el-input type="textarea" :rows="6" maxlength="400" :placeholder="promptPlaceholder" <el-input
v-model="prompt" show-word-limit :disabled="generating" /> type="textarea"
:rows="6"
maxlength="400"
:placeholder="promptPlaceholder"
v-model="prompt"
show-word-limit
:disabled="generating"
/>
</div> </div>
</div> </div>
</el-card> </el-card>
@ -67,9 +66,19 @@
<div class="select-item"> <div class="select-item">
<div class="select-title">模型</div> <div class="select-title">模型</div>
<div class="select-content"> <div class="select-content">
<el-select v-model="currentModelId" style="width: 100%" popper-class="custom-select-popup" <el-select
@change="handleModelChange" :disabled="generating"> v-model="currentModelId"
<el-option v-for="model in models" :key="model.id" :label="model.name" :value="model.id" /> style="width: 100%"
popper-class="custom-select-popup"
@change="handleModelChange"
:disabled="generating"
>
<el-option
v-for="model in models"
:key="model.id"
:label="model.name"
:value="model.id"
/>
</el-select> </el-select>
</div> </div>
</div> </div>
@ -83,32 +92,54 @@
</div> </div>
<div class="select-content"> <div class="select-content">
<!-- 下拉选择类型(code) --> <!-- 下拉选择类型(code) -->
<el-select v-if="field.uitype === 'code'" v-model="configData[field.name]" <el-select
style="width: 100%" popper-class="custom-select-popup" v-if="field.uitype === 'code'"
:placeholder="`请选择${field.label}`" :disabled="generating"> v-model="configData[field.name]"
<el-option v-for="option in field.data" :key="option.value" style="width: 100%"
:label="option.text || option.value" :value="option.value" /> popper-class="custom-select-popup"
:placeholder="`请选择${field.label}`"
:disabled="generating"
>
<el-option
v-for="option in field.data"
:key="option.value"
:label="option.text || option.value"
:value="option.value"
/>
</el-select> </el-select>
<!-- 数字输入类型(int) --> <!-- 数字输入类型(int) -->
<el-input-number v-else-if="field.uitype === 'int'" v-model="configData[field.name]" <el-input-number
:min="1" :max="field.max" :step="1" controls-position="right" style="width: 100%" v-else-if="field.uitype === 'int'"
:disabled="generating" /> v-model="configData[field.name]"
<el-input-number v-else-if="field.uitype === 'int'" v-model="configData[field.name]" :min="1"
:min="1" :max="field.max || 15" :step="1" controls-position="right" style="width: 100%" :max="field.max ?? 100"
:disabled="generating" /> :step="1"
controls-position="right"
style="width: 100%"
:disabled="generating"
/>
<!-- 文本输入(text 类型,但非 prompt) --> <!-- 文本输入(text 类型,但非 prompt) -->
<el-input v-else-if="field.uitype === 'text'" v-model="configData[field.name]" <el-input
v-else-if="field.uitype === 'text'"
v-model="configData[field.name]"
:placeholder="`请输入${field.label}`" :placeholder="`请输入${field.label}`"
:type="field.name === 'negative_prompt' ? 'textarea' : 'text'" :type="field.name === 'negative_prompt' ? 'textarea' : 'text'"
:rows="field.name === 'negative_prompt' ? 3 : 1" clearable :disabled="generating" /> :rows="field.name === 'negative_prompt' ? 3 : 1"
clearable
:disabled="generating"
/>
<!-- 音频上传 --> <!-- 音频上传 -->
<div v-else-if="field.uitype === 'audio'" class="audio-wrapper"> <div v-else-if="field.uitype === 'audio'" class="audio-wrapper">
<el-input v-model="configData[field.name]" placeholder="请输入音频文件URL或点击上传" clearable <el-input
:disabled="generating" /> v-model="configData[field.name]"
<el-button size="small" @click="simulateUpload" :disabled="generating" placeholder="请输入音频文件URL或点击上传"
class="upload-btn"> clearable
:disabled="generating"
/>
<el-button size="small" @click="simulateUpload" :disabled="generating" class="upload-btn">
上传 上传
</el-button> </el-button>
</div> </div>
@ -123,7 +154,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'; import { ref, computed, watch, onMounted } from 'vue';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { Delete, Download, Plus, ZoomIn } from '@element-plus/icons-vue'; import { Delete, Plus, ZoomIn } from '@element-plus/icons-vue';
import type { UploadFile } from 'element-plus'; import type { UploadFile } from 'element-plus';
import { imagetovideo, AllModel, queryModelPrice } from '../../apis/Model'; import { imagetovideo, AllModel, queryModelPrice } from '../../apis/Model';
import type { import type {
@ -135,9 +166,6 @@ import type {
ModelParams, ModelParams,
} from '../../types/model'; } from '../../types/model';
// ==================== 环境变量 ====================
const IMG_BASE_URL = import.meta.env.VITE_IMG_BASE_URL || '';
// ==================== 事件定义 ==================== // ==================== 事件定义 ====================
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'price-update', price: number): void; (e: 'price-update', price: number): void;
@ -163,7 +191,6 @@ const price = ref(0);
const dialogImageUrl = ref(''); const dialogImageUrl = ref('');
const dialogVisible = ref(false); const dialogVisible = ref(false);
const disabled = ref(false);
// ==================== 计算属性 ==================== // ==================== 计算属性 ====================
const imageFields = computed(() => { const imageFields = computed(() => {
@ -188,8 +215,7 @@ const visibleFields = computed(() => {
const promptPlaceholder = computed(() => { const promptPlaceholder = computed(() => {
if (!currentModel.value) return '请输入提示词'; if (!currentModel.value) return '请输入提示词';
const promptField = currentModel.value.input_fields.find(f => f.name === 'prompt'); const promptField = currentModel.value.input_fields.find(f => f.name === 'prompt');
if (promptField?.required) return '请输入提示词(必填)'; return promptField?.required ? '请输入提示词(必填)' : '请输入提示词';
return '请输入提示词';
}); });
// ==================== 表单初始化 ==================== // ==================== 表单初始化 ====================
@ -334,6 +360,7 @@ const handleModelChange = (modelId: string) => {
fetchPrice(); fetchPrice();
} }
}; };
// ==================== 监听器 ==================== // ==================== 监听器 ====================
watch( watch(
() => [configData.value, currentModelId.value], () => [configData.value, currentModelId.value],
@ -359,36 +386,33 @@ watch(
); );
// ==================== 上传处理 ==================== // ==================== 上传处理 ====================
const getFileList = (fieldName: string) => {
const url = uploadedFiles.value[fieldName];
if (url) {
return [{ name: fieldName, url }];
}
return [];
};
const handleFileChange = (file: UploadFile, fieldName: string) => { const handleFileChange = (file: UploadFile, fieldName: string) => {
// 检查是否已存在该字段的文件 // 检查是否已存在该字段的文件(防御性判断)
if (uploadedFiles.value[fieldName]) { if (uploadedFiles.value[fieldName]) {
ElMessage.warning('每个字段只能上传一张图片,请先删除已有图片再上传'); ElMessage.warning('每个字段只能上传一张图片,请先删除已有图片再上传');
return; return;
} }
const url = URL.createObjectURL(file.raw as File); const rawFile = file.raw as File;
const fileName = file.name; const fileName = file.name;
uploadedFiles.value[fieldName] = url; const reader = new FileReader();
reader.onload = (e) => {
const base64 = e.target?.result as string;
uploadedFiles.value[fieldName] = base64;
uploadedFileNames.value[fieldName] = fileName; uploadedFileNames.value[fieldName] = fileName;
console.log('=== 上传图片 ==='); console.log('=== 上传图片 ===');
console.log('字段名:', fieldName); console.log('字段名:', fieldName);
console.log('文件名:', fileName); console.log('文件名:', fileName);
console.log('预览 URL:', url); console.log('Base64长度:', base64.length);
console.log('提交 URL:', `${IMG_BASE_URL}/${fileName}`); console.log('Base64前100字符:', base64.substring(0, 100));
console.log('当前所有上传文件:', uploadedFileNames.value); console.log('当前所有上传文件:', Object.keys(uploadedFileNames.value));
ElMessage.success('上传成功'); ElMessage.success('上传成功');
}; };
reader.readAsDataURL(rawFile);
};
const handleRemove = (fieldName: string) => { const handleRemove = (fieldName: string) => {
uploadedFiles.value[fieldName] = ''; uploadedFiles.value[fieldName] = '';
@ -406,16 +430,6 @@ const handlePictureCardPreview = (file: UploadFile) => {
dialogVisible.value = true; dialogVisible.value = true;
}; };
const handleDownload = (fieldName: string) => {
const url = uploadedFiles.value[fieldName];
if (url) {
const a = document.createElement('a');
a.href = url;
a.download = fieldName + '.png';
a.click();
}
};
const simulateUpload = () => { const simulateUpload = () => {
ElMessage.info('此处可对接实际音频上传服务'); ElMessage.info('此处可对接实际音频上传服务');
configData.value.audio_file = 'https://example.com/demo_audio.mp3'; configData.value.audio_file = 'https://example.com/demo_audio.mp3';
@ -432,13 +446,11 @@ const buildSubmitParams = (): Record<string, unknown> => {
baseParams.prompt = prompt.value; baseParams.prompt = prompt.value;
} }
// 添加上传的文件 // 添加上传的文件(使用base64格式)
Object.keys(uploadedFileNames.value).forEach(key => { Object.keys(uploadedFiles.value).forEach(key => {
if (uploadedFileNames.value[key]) { if (uploadedFiles.value[key]) {
// 使用环境变量中的图片前缀 + 文件名 baseParams[key] = uploadedFiles.value[key];
baseParams[key] = `${IMG_BASE_URL}/${uploadedFileNames.value[key]}`; console.log('提交字段:', key, 'Base64长度:', uploadedFiles.value[key].length);
console.log('123',baseParams[key]);
} }
}); });
@ -446,7 +458,6 @@ const buildSubmitParams = (): Record<string, unknown> => {
Object.keys(configData.value).forEach(key => { Object.keys(configData.value).forEach(key => {
if (configData.value[key] !== undefined) { if (configData.value[key] !== undefined) {
const value = configData.value[key]; const value = configData.value[key];
// 确保 audio 字段是布尔值
if (key === 'audio_file') { if (key === 'audio_file') {
baseParams.audio = Boolean(value); baseParams.audio = Boolean(value);
} else { } else {
@ -457,8 +468,7 @@ const buildSubmitParams = (): Record<string, unknown> => {
console.log('=== 构建提交参数 ==='); console.log('=== 构建提交参数 ===');
console.log('完整参数:', JSON.stringify(baseParams, null, 2)); console.log('完整参数:', JSON.stringify(baseParams, null, 2));
console.log('上传的图片字段:', uploadedFileNames.value); console.log('上传的图片字段:', Object.keys(uploadedFiles.value));
console.log('图片基础 URL:', IMG_BASE_URL);
return baseParams; return baseParams;
}; };
@ -556,6 +566,7 @@ const resetForm = () => {
emit('price-update', 0); emit('price-update', 0);
emit('prompt-change', true); emit('prompt-change', true);
}; };
// ==================== 暴露给父组件 ==================== // ==================== 暴露给父组件 ====================
defineExpose({ defineExpose({
generate, generate,
@ -570,9 +581,87 @@ onMounted(() => {
<style scoped lang="less"> <style scoped lang="less">
@import url('../../assets/less/Video/text/text.less'); @import url('../../assets/less/Video/text/text.less');
// 全屏 + 滚动条样式
.reference-to-video {
height: 100vh; // 占满整个视口高度
display: flex;
flex-direction: column;
overflow-y: auto; // 内容超出时显示垂直滚动条
// padding: 16px;
box-sizing: border-box;
background-color: #0f1114; // 与设计背景一致
.card {
margin-bottom: 16px;
flex-shrink: 0; // 防止卡片被压缩
}
.btmcard {
margin-bottom: 0;
}
}
// 图片预览容器样式(保持不变)
.image-preview-container {
position: relative;
width: 148px;
height: 148px;
border-radius: 8px;
overflow: hidden;
border: 1px solid #35383F;
background-color: #1a1c20;
.preview-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.preview-actions {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
opacity: 0;
transition: opacity 0.3s;
&:hover {
opacity: 1;
}
.preview-action {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.2);
color: #fff;
cursor: pointer;
transition: background-color 0.3s;
&:hover {
background-color: rgba(255, 255, 255, 0.4);
}
.el-icon {
font-size: 18px;
}
}
}
}
</style> </style>
<style> <style>
/* 全局样式(保留原有下拉框样式) */
.custom-select-popup { .custom-select-popup {
background-color: #2A2D34 !important; background-color: #2A2D34 !important;
border-radius: 12px !important; border-radius: 12px !important;

View File

@ -7,6 +7,20 @@
<div class="upload-section"> <div class="upload-section">
<!-- 上传视频 --> <!-- 上传视频 -->
<div class="upload-item"> <div class="upload-item">
<!-- 已上传时显示预览 -->
<div v-if="videoBase64" class="uploaded-preview">
<img :src="videoBase64" alt="视频预览" class="uploaded-image" />
<div class="uploaded-actions">
<span class="uploaded-action" @click="handlePreview('video')">
<el-icon><ZoomIn /></el-icon>
</span>
<span class="uploaded-action" @click="handleRemoveVideo">
<el-icon><Delete /></el-icon>
</span>
</div>
</div>
<!-- 未上传时显示上传框 -->
<div v-else class="upload-placeholder">
<div class="upload-icon"> <div class="upload-icon">
<svg width="1em" height="1em" fill="none" viewBox="0 0 25 25" xmlns="http://www.w3.org/2000/svg"> <svg width="1em" height="1em" fill="none" viewBox="0 0 25 25" xmlns="http://www.w3.org/2000/svg">
<path d="M12.28 5.96875L12.2617 19.9688" stroke="currentColor" stroke-linecap="round" <path d="M12.28 5.96875L12.2617 19.9688" stroke="currentColor" stroke-linecap="round"
@ -17,10 +31,13 @@
</div> </div>
<div class="upload-text"> <div class="upload-text">
<span class="upload-title">上传视频 / 主体</span> <span class="upload-title">上传视频 / 主体</span>
<span class="upload-desc">最多可上传1个5-8s或2个5s内视频 / 主体</span> <span class="upload-desc">最多可上传1个视频</span>
</div>
<el-upload action="#" :auto-upload="false" :show-file-list="false" accept="video/*"
:on-change="handleVideoChange" class="upload-button">
<el-button type="primary" size="small">选择文件</el-button>
</el-upload>
</div> </div>
<img src="https://image01.vidu.zone/vidu/media-asset/video-a09ab611.webp" alt="视频示例"
class="upload-preview" />
</div> </div>
<!-- 分割线 --> <!-- 分割线 -->
@ -28,6 +45,20 @@
<!-- 上传图片 --> <!-- 上传图片 -->
<div class="upload-item"> <div class="upload-item">
<!-- 已上传时显示预览 -->
<div v-if="imageBase64" class="uploaded-preview">
<img :src="imageBase64" alt="图片预览" class="uploaded-image" />
<div class="uploaded-actions">
<span class="uploaded-action" @click="handlePreview('image')">
<el-icon><ZoomIn /></el-icon>
</span>
<span class="uploaded-action" @click="handleRemoveImage">
<el-icon><Delete /></el-icon>
</span>
</div>
</div>
<!-- 未上传时显示上传框 -->
<div v-else class="upload-placeholder">
<div class="upload-icon"> <div class="upload-icon">
<svg width="1em" height="1em" fill="none" viewBox="0 0 25 25" xmlns="http://www.w3.org/2000/svg"> <svg width="1em" height="1em" fill="none" viewBox="0 0 25 25" xmlns="http://www.w3.org/2000/svg">
<path d="M12.28 5.96875L12.2617 19.9688" stroke="currentColor" stroke-linecap="round" <path d="M12.28 5.96875L12.2617 19.9688" stroke="currentColor" stroke-linecap="round"
@ -38,9 +69,13 @@
</div> </div>
<div class="upload-text"> <div class="upload-text">
<span class="upload-title">上传图片 / 主体</span> <span class="upload-title">上传图片 / 主体</span>
<span class="upload-desc">最多可上传1-4张图片</span> <span class="upload-desc">最多可上传1张图片</span>
</div>
<el-upload action="#" :auto-upload="false" :show-file-list="false" accept="image/*"
:on-change="handleImageChange" class="upload-button">
<el-button type="primary" size="small">选择文件</el-button>
</el-upload>
</div> </div>
<img src="https://image01.vidu.zone/vidu/media-asset/img-052445cb.webp" alt="图片示例" class="upload-preview" />
</div> </div>
</div> </div>
@ -144,13 +179,20 @@
</div> </div>
</el-card> </el-card>
</div> </div>
<!-- 图片预览对话框 -->
<el-dialog v-model="dialogVisible">
<img w-full :src="dialogImageUrl" alt="Preview Image" />
</el-dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch, defineEmits } from 'vue' import { ref, computed, watch, defineEmits } from 'vue'
import { ElMessage } from 'element-plus'
import { Delete, ZoomIn } from '@element-plus/icons-vue'
import type { UploadFile } from 'element-plus'
// 定义 emit 事件 // 定义 emit 事件
const emit = defineEmits(['price-update', 'prompt-change']) const emit = defineEmits(['price-update', 'prompt-change', 'task-created'])
// 下拉选中项 // 下拉选中项
const selectModel = ref('lucy') const selectModel = ref('lucy')
@ -167,6 +209,14 @@ const durations = [2, 3, 4, 5, 6, 7, 8]
// 提示词文本 // 提示词文本
const promptText = ref('') const promptText = ref('')
// 上传文件的base64
const videoBase64 = ref('')
const imageBase64 = ref('')
// 预览对话框
const dialogVisible = ref(false)
const dialogImageUrl = ref('')
// 价格计算 // 价格计算
const price = computed(() => { const price = computed(() => {
// 基础价格:5秒及以下每个 1 单位,超过5秒每个 2 单位 // 基础价格:5秒及以下每个 1 单位,超过5秒每个 2 单位
@ -198,6 +248,125 @@ const handleChange = (value: string) => {
const handlePromptInput = (value: string) => { const handlePromptInput = (value: string) => {
promptText.value = value promptText.value = value
} }
// 处理视频上传
const handleVideoChange = (file: UploadFile) => {
if (videoBase64.value) {
ElMessage.warning('只能上传一个视频,请先删除已有视频')
return
}
const rawFile = file.raw as File
const reader = new FileReader()
reader.onload = (e) => {
const base64 = e.target?.result as string
videoBase64.value = base64
console.log('视频上传成功,Base64长度:', base64.length)
ElMessage.success('视频上传成功')
}
reader.readAsDataURL(rawFile)
}
// 处理图片上传
const handleImageChange = (file: UploadFile) => {
if (imageBase64.value) {
ElMessage.warning('只能上传一张图片,请先删除已有图片')
return
}
const rawFile = file.raw as File
const reader = new FileReader()
reader.onload = (e) => {
const base64 = e.target?.result as string
imageBase64.value = base64
console.log('图片上传成功,Base64长度:', base64.length)
ElMessage.success('图片上传成功')
}
reader.readAsDataURL(rawFile)
}
// 删除视频
const handleRemoveVideo = () => {
videoBase64.value = ''
ElMessage.info('已删除视频')
}
// 删除图片
const handleRemoveImage = () => {
imageBase64.value = ''
ElMessage.info('已删除图片')
}
// 预览
const handlePreview = (type: 'video' | 'image') => {
dialogImageUrl.value = type === 'video' ? videoBase64.value : imageBase64.value
dialogVisible.value = true
}
// 构建提交参数
const buildSubmitParams = () => {
const params: Record<string, any> = {
llmid: selectModel.value,
prompt: promptText.value,
duration: selectedDuration.value,
voice: selectVoice.value,
quality: selectQuality.value,
codec: selectCodec.value,
ratio: selectRatio.value,
count: selectedCount.value,
}
// 添加视频base64
if (videoBase64.value) {
params.video = videoBase64.value
console.log('添加视频参数,Base64长度:', videoBase64.value.length)
}
// 添加图片base64
if (imageBase64.value) {
params.image = imageBase64.value
console.log('添加图片参数,Base64长度:', imageBase64.value.length)
}
console.log('提交参数:', JSON.stringify(params, null, 2))
return params
}
// 生成视频
const generate = async () => {
if (!videoBase64.value && !imageBase64.value) {
ElMessage.warning('请至少上传一个视频或图片')
return false
}
try {
const params = buildSubmitParams()
// TODO: 调用实际的生成接口
console.log('生成参数:', params)
ElMessage.success('生成任务已提交')
return true
} catch (error) {
console.error('生成失败:', error)
ElMessage.error('生成失败,请重试')
return false
}
}
// 重置表单
const resetForm = () => {
promptText.value = ''
videoBase64.value = ''
imageBase64.value = ''
selectedDuration.value = 5
selectedCount.value = 1
emit('prompt-change', true)
}
// 暴露给父组件
defineExpose({
generate,
resetForm,
})
</script> </script>
<style scoped lang="less"> <style scoped lang="less">
@ -463,6 +632,82 @@ const handlePromptInput = (value: string) => {
background-image: linear-gradient(to bottom, #7280FF, #DADFFF); background-image: linear-gradient(to bottom, #7280FF, #DADFFF);
font-weight: 600; font-weight: 600;
} }
// 上传预览相关样式
.uploaded-preview {
position: relative;
width: 100%;
aspect-ratio: 185/75;
border-radius: 8px;
overflow: hidden;
background-color: #1a1c20;
.uploaded-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.uploaded-actions {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
opacity: 0;
transition: opacity 0.3s;
&:hover {
opacity: 1;
}
.uploaded-action {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.2);
color: #fff;
cursor: pointer;
transition: background-color 0.3s;
&:hover {
background-color: rgba(255, 255, 255, 0.4);
}
.el-icon {
font-size: 18px;
}
}
}
}
.upload-placeholder {
position: relative;
display: flex;
align-items: center;
gap: 16px;
padding: 16px 24px;
height: 88px;
cursor: pointer;
transition: background-color 0.2s;
&:hover {
background-color: rgba(255, 255, 255, 0.08);
}
.upload-button {
position: absolute;
right: 24px;
}
}
</style> </style>

View File

@ -8,6 +8,7 @@ import ElementPlus from 'element-plus'
// element-ui css // element-ui css
import 'element-plus/dist/index.css' import 'element-plus/dist/index.css'
import 'element-plus/theme-chalk/index.css' import 'element-plus/theme-chalk/index.css'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
// pinia持久化 // pinia持久化
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
// pinia // pinia
@ -15,11 +16,13 @@ import { createPinia } from 'pinia'
// icon // icon
import './assets/css/iconfont/iconfont.css' import './assets/css/iconfont/iconfont.css'
const pinia = createPinia() const pinia = createPinia()
pinia.use(piniaPluginPersistedstate) pinia.use(piniaPluginPersistedstate)
const app = createApp(App) const app = createApp(App)
app.use(router) app.use(router)
app.use(ElementPlus) app.use(ElementPlus, {
locale: zhCn,
})
app.use(pinia) app.use(pinia)
app.mount('#app') app.mount('#app')

View File

@ -29,6 +29,12 @@ const router = createRouter({
name: 'index', name: 'index',
component: () => import('@/views/index/index.vue') component: () => import('@/views/index/index.vue')
}, },
// 消费记录
{
path: '/bill',
name: 'bill',
component: () => import('@/views/Bill/Bill.vue')
},
// ImgGenerate // ImgGenerate
{ {
path: '/ImgGenerate', path: '/ImgGenerate',

783
src/views/Bill/Bill.vue Normal file
View File

@ -0,0 +1,783 @@
<template>
<div class="bill-container">
<el-card 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-popper"
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.name || 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>
</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"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue';
import { ElMessage } from 'element-plus';
import { consumptionRecord } from '@/apis/home';
import { useLoginStore } from '@/store/LoginStore';
// ==================== 实例和状态 ====================
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 updatePaginatedList = () => {
// 先根据状态筛选
let filteredList = allOrderList.value;
if (statusFilter.value) {
filteredList = allOrderList.value.filter(order => {
const status = (order.status || '').toUpperCase();
return status === statusFilter.value.toUpperCase();
});
console.log('状态筛选 - 筛选条件:', statusFilter.value, '筛选后数量:', filteredList.length);
}
// 更新总数为筛选后的数量
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);
console.log('前端分页 - 从索引', startIndex, '到', endIndex, '显示', orderList.value.length, '条数据', '总数:', total.value);
};
// ==================== 事件处理 ====================
/**
* 获取状态显示文本
*/
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) => {
console.log('分页大小变化:', size);
pageSize.value = size;
currentPage.value = 1; // 切换每页条数通常重置为第一页
updatePaginatedList(); // 前端分页,更新显示数据
};
/**
* 当前页码变化
*/
const handleCurrentChange = (page: number) => {
console.log('页码变化:', page);
currentPage.value = page;
updatePaginatedList(); // 前端分页,更新显示数据
};
/**
* 筛选条件变化
*/
const handleFilterChange = () => {
currentPage.value = 1; // 筛选变化时重置为第一页
updatePaginatedList(); // 重新筛选和分页
};
// ==================== 数据加载 ====================
/**
* 加载订单列表
*/
const loadOrderList = async () => {
if (!isLoggedIn.value) {
ElMessage.warning('请先登录');
return;
}
isLoading.value = true;
try {
console.log('=== 开始加载订单列表 ===');
// 不传递分页参数,获取所有数据
const res = await consumptionRecord<any, any>({});
console.log('=== API响应 ===');
console.log('完整响应:', res);
console.log('响应状态:', res.status);
console.log('响应数据:', res.data);
if (res.status === 'ok' && res.data) {
const rawData = res.data.rows || [];
allOrderList.value = rawData; // 存储所有数据
total.value = rawData.length || 0; // 使用实际数据长度作为总数
console.log('=== 处理结果 ===');
console.log('原始数据长度:', rawData.length);
console.log('总数:', total.value);
// 更新当前页显示的数据
updatePaginatedList();
} else {
allOrderList.value = [];
orderList.value = [];
total.value = 0;
console.log('API返回非ok状态或无数据');
}
} catch (error) {
console.error('=== 获取消费记录失败 ===');
console.error('错误详情:', error);
ElMessage.error('获取消费记录失败');
allOrderList.value = [];
orderList.value = [];
total.value = 0;
} finally {
isLoading.value = false;
}
};
// ==================== 生命周期 ====================
onMounted(() => {
if (isLoggedIn.value) {
loadOrderList();
}
});
</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-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;
}
}
}
// 订单底部
.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-tooltip__trigger){
background-color: #14161a!important;
border: none!important;
}
:deep(.el-pagination) {
.el-pager li {
background-color: #1e2024;
border: 1px solid #2a2d32;
color: rgba(255, 255, 255, 0.8);
border-radius: 6px;
// transition: all 0.3s;
margin:0 4px;
&.is-active {
background-color: #00c4b3;
border-color: #00c4b3;
color: #1a1c20;
font-weight: 600;
}
&:hover:not(.is-active) {
border-color: #00c4b3;
color: #00c4b3;
}
}
.btn-prev,
.btn-next {
background-color: #1e2024;
border: 1px solid #2a2d32;
color: rgba(255, 255, 255, 0.8);
border-radius: 6px;
transition: all 0.3s;
&:hover:not(:disabled) {
border-color: #00c4b3;
color: #00c4b3;
}
}
.el-pagination__sizes,
.el-pagination__jump {
.el-input__wrapper {
background-color: #25282e;
border: 1px solid #3a3e45;
border-radius: 6px;
transition: all 0.3s;
&:hover {
border-color: #00c4b3;
}
.el-input__inner {
color: rgba(255, 255, 255, 0.9);
}
}
}
.el-pagination__total {
color: rgba(255, 255, 255, 0.7);
font-weight: 500;
}
}
}
</style>
<style>
.dark-popper .el-select-dropdown__list {
background-color: #1e2024 !important;
}
.dark-popper .el-select-dropdown__item {
color: #ffffff !important;
}
.dark-popper .el-select-dropdown__item:hover {
background-color: #3a3d44 !important;
}
.dark-popper .el-select-dropdown__item.is-selected {
background-color: #2c2f36 !important;
color: #409eff !important;
}
</style>