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">
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(() => {
@ -11,10 +12,12 @@ onMounted(() => {
</script>
<template>
<el-config-provider :locale="zhCn">
<div>
<router-view>
</router-view>
</div>
</el-config-provider>
</template>
<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;">
<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">
<span class="icon iconfont">&#xe615;</span>
<div class="name">余额</div>
@ -174,9 +180,9 @@
<pre><code class="language-css"
>@font-face {
font-family: 'iconfont';
src: url('iconfont.woff2?t=1774432601927') format('woff2'),
url('iconfont.woff?t=1774432601927') format('woff'),
url('iconfont.ttf?t=1774432601927') format('truetype');
src: url('iconfont.woff2?t=1775638325739') format('woff2'),
url('iconfont.woff?t=1775638325739') format('woff'),
url('iconfont.ttf?t=1775638325739') format('truetype');
}
</code></pre>
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@ -202,6 +208,15 @@
<div class="content font-class">
<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">
<span class="icon iconfont icon-yue"></span>
<div class="name">
@ -382,6 +397,14 @@
<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-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>

View File

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

File diff suppressed because one or more lines are too long

View File

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

Binary file not shown.

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -7,20 +7,37 @@
<div class="upload-section">
<!-- 上传视频 -->
<div class="upload-item">
<div class="upload-icon">
<svg width="1em" height="1em" fill="none" viewBox="0 0 25 25" xmlns="http://www.w3.org/2000/svg">
<path d="M12.28 5.96875L12.2617 19.9688" stroke="currentColor" stroke-linecap="round"
stroke-linejoin="round" stroke-width="2"></path>
<path d="M5.25 12.9688H19.25" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2"></path>
</svg>
<!-- 已上传时显示预览 -->
<div 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 class="upload-text">
<span class="upload-title">上传视频 / 主体</span>
<span class="upload-desc">最多可上传1个5-8s或2个5s内视频 / 主体</span>
<!-- 未上传时显示上传框 -->
<div v-else class="upload-placeholder">
<div class="upload-icon">
<svg width="1em" height="1em" fill="none" viewBox="0 0 25 25" xmlns="http://www.w3.org/2000/svg">
<path d="M12.28 5.96875L12.2617 19.9688" stroke="currentColor" stroke-linecap="round"
stroke-linejoin="round" stroke-width="2"></path>
<path d="M5.25 12.9688H19.25" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2"></path>
</svg>
</div>
<div class="upload-text">
<span class="upload-title">上传视频 / 主体</span>
<span class="upload-desc">最多可上传1个视频</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>
<img src="https://image01.vidu.zone/vidu/media-asset/video-a09ab611.webp" alt="视频示例"
class="upload-preview" />
</div>
<!-- 分割线 -->
@ -28,19 +45,37 @@
<!-- 上传图片 -->
<div class="upload-item">
<div class="upload-icon">
<svg width="1em" height="1em" fill="none" viewBox="0 0 25 25" xmlns="http://www.w3.org/2000/svg">
<path d="M12.28 5.96875L12.2617 19.9688" stroke="currentColor" stroke-linecap="round"
stroke-linejoin="round" stroke-width="2"></path>
<path d="M5.25 12.9688H19.25" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2"></path>
</svg>
<!-- 已上传时显示预览 -->
<div 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 class="upload-text">
<span class="upload-title">上传图片 / 主体</span>
<span class="upload-desc">最多可上传1-4张图片</span>
<!-- 未上传时显示上传框 -->
<div v-else class="upload-placeholder">
<div class="upload-icon">
<svg width="1em" height="1em" fill="none" viewBox="0 0 25 25" xmlns="http://www.w3.org/2000/svg">
<path d="M12.28 5.96875L12.2617 19.9688" stroke="currentColor" stroke-linecap="round"
stroke-linejoin="round" stroke-width="2"></path>
<path d="M5.25 12.9688H19.25" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2"></path>
</svg>
</div>
<div class="upload-text">
<span class="upload-title">上传图片 / 主体</span>
<span class="upload-desc">最多可上传1张图片</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>
<img src="https://image01.vidu.zone/vidu/media-asset/img-052445cb.webp" alt="图片示例" class="upload-preview" />
</div>
</div>
@ -144,13 +179,20 @@
</div>
</el-card>
</div>
<!-- 图片预览对话框 -->
<el-dialog v-model="dialogVisible">
<img w-full :src="dialogImageUrl" alt="Preview Image" />
</el-dialog>
</template>
<script setup lang="ts">
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
const emit = defineEmits(['price-update', 'prompt-change'])
const emit = defineEmits(['price-update', 'prompt-change', 'task-created'])
//
const selectModel = ref('lucy')
@ -167,6 +209,14 @@ const durations = [2, 3, 4, 5, 6, 7, 8]
//
const promptText = ref('')
// base64
const videoBase64 = ref('')
const imageBase64 = ref('')
//
const dialogVisible = ref(false)
const dialogImageUrl = ref('')
//
const price = computed(() => {
// 5 1 5 2
@ -198,6 +248,125 @@ const handleChange = (value: string) => {
const handlePromptInput = (value: string) => {
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>
<style scoped lang="less">
@ -463,6 +632,82 @@ const handlePromptInput = (value: string) => {
background-image: linear-gradient(to bottom, #7280FF, #DADFFF);
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>

View File

@ -8,6 +8,7 @@ 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
@ -15,11 +16,13 @@ import { createPinia } from 'pinia'
// icon
import './assets/css/iconfont/iconfont.css'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
const app = createApp(App)
app.use(router)
app.use(ElementPlus)
app.use(ElementPlus, {
locale: zhCn,
})
app.use(pinia)
app.mount('#app')
app.mount('#app')

View File

@ -29,6 +29,12 @@ const router = createRouter({
name: 'index',
component: () => import('@/views/index/index.vue')
},
// 消费记录
{
path: '/bill',
name: 'bill',
component: () => import('@/views/Bill/Bill.vue')
},
// ImgGenerate
{
path: '/ImgGenerate',
@ -70,15 +76,15 @@ const router = createRouter({
}
]
},
// 素材广场
{
path: '/material-square',
name: 'material-square',
component: () => import('@/views/videoGenerate/videoGenerate.vue')
}
]
},

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>