4.17更新

This commit is contained in:
hrx 2026-04-17 15:34:33 +08:00
parent f84af8856e
commit eb6059267d
10 changed files with 1138 additions and 812 deletions

View File

@ -12,8 +12,9 @@
justify-content: space-between;
align-items: center;
padding-right: 2rem;
background: rgba(10, 14, 20, 0.8);
backdrop-filter: blur(12px);
background: rgba(10, 12, 18, 0.74);
backdrop-filter: blur(16px);
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
@media (max-width: 768px) {
padding: 0 1rem;
@ -37,11 +38,9 @@
.header-title {
font-family: "Microsoft Yahei", "PingFang SC", sans-serif;
font-size: 28px;
font-weight: 600;
font-weight: 700;
color: #fff;
transform: skew(-8deg);
-webkit-transform: skew(-12deg);
text-shadow: 0 0 10px rgba(0, 255, 204, 0.3);
letter-spacing: 0.04em;
@media (max-width: 768px) {
font-size: 24px;
@ -53,7 +52,7 @@
.header-actions {
display: flex;
align-items: center;
gap: 20px;
gap: 14px;
@media (max-width: 768px) {
gap: 12px;
@ -64,60 +63,112 @@
// 【一体化】余额 + 充值按钮(核心修改)
// ==============================================
.balance-recharge-combo {
height: 44px;
padding: 0 20px;
height: 52px;
padding: 0 10px 0 12px;
display: flex;
align-items: center;
gap: 8px;
gap: 12px;
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;
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: #ff8e53;
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: 600;
color: #ff8e53;
font-weight: 700;
color: #fff;
line-height: 1.1;
}
.combo-divider {
width: 1px;
height: 24px;
background: rgba(255, 255, 255, 0.08);
}
// 充值文字
.recharge-text {
height: 100%;
width: 60px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 10px;
min-width: 72px;
height: 36px;
padding: 0 14px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 12px;
font-size: 13px;
color: #ff8e53;
color: #fff;
font-weight: 600;
background: linear-gradient(135deg, #6e5cff, #8b7cff);
box-shadow: 0 10px 24px rgba(110, 92, 255, 0.3);
}
&:hover {
background: rgba(255, 142, 83, 0.15);
box-shadow: 0 4px 12px rgba(255, 142, 83, 0.2);
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: 36px;
padding: 0 14px;
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 {
padding: 1px 6px;
min-width: 60px;
height: 30px;
padding: 0 10px;
font-size: 12px;
}
}
@ -128,21 +179,21 @@
min-width: 120px;
height: 48px;
font-size: 16px;
background-image: linear-gradient(270deg, #00f2fe 11.11%, #4facfe);
border-radius: 24px;
background-image: linear-gradient(135deg, #6e5cff, #8b7cff);
border-radius: 16px;
display: flex;
justify-content: center;
align-items: center;
font-weight: 600;
color: #0a1428;
color: #fff;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 12px rgba(0, 242, 254, 0.3);
box-shadow: 0 12px 28px rgba(110, 92, 255, 0.3);
&:hover {
background-image: linear-gradient(270deg, #4facfe 11.11%, #00f2fe);
background-image: linear-gradient(135deg, #7d6dff, #978bff);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 242, 254, 0.4);
box-shadow: 0 16px 36px rgba(110, 92, 255, 0.34);
}
@media (max-width: 768px) {
@ -158,35 +209,48 @@
display: inline-block;
}
// 下拉菜单打开时,箭头保持指向上
.user-profile.dropdown-open .user-trigger .dropdown-icon {
transform: rotate(180deg);
color: #00ffcc;
color: #8b7cff;
}
// 用户触发区域
.user-trigger {
display: flex;
align-items: center;
gap: 8px;
padding: 0 20px;
height: 44px;
background: rgba(255, 255, 255, 0.1);
border-radius: 22px;
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 transparent;
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.15);
border-color: rgba(0, 255, 204, 0.3);
box-shadow: 0 0 15px rgba(0, 255, 204, 0.2);
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: 13px;
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: 500;
font-weight: 600;
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
@ -197,17 +261,19 @@
width: 16px;
height: 16px;
transition: transform 0.3s ease;
color: rgba(255, 255, 255, 0.8);
}
&:hover .dropdown-icon {
transform: rotate(180deg);
color: #00ffcc;
color: rgba(255, 255, 255, 0.56);
}
@media (max-width: 768px) {
height: 36px;
padding: 0 14px;
padding: 0 12px;
.user-avatar {
width: 24px;
height: 24px;
border-radius: 8px;
font-size: 11px;
}
.user-name {
max-width: 80px;
@ -223,11 +289,11 @@
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);
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;
@ -240,7 +306,7 @@
height: 0;
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-bottom: 8px solid rgba(10, 14, 20, 0.95);
border-bottom: 8px solid rgba(16, 19, 28, 0.98);
}
}
@ -254,7 +320,7 @@
cursor: pointer;
&:hover {
background: rgba(0, 255, 204, 0.1);
background: rgba(255, 255, 255, 0.05);
}
&.user-id {
@ -266,7 +332,7 @@
}
.item-value {
color: #00ffcc;
color: #8b7cff;
font-family: monospace;
font-size: 13px;
}
@ -295,7 +361,7 @@
// 下拉菜单分隔线
.dropdown-divider {
height: 1px;
background: rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.06);
margin: 4px 0;
}

View File

@ -0,0 +1,547 @@
@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: 420px;
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;
padding: 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;
}
}
}

View File

@ -13,14 +13,23 @@
<template v-if="loginStore.isLoggedIn">
<!-- 一体化余额 + 充值按钮 -->
<div class="balance-recharge-combo" @click="showRechargeModal = true">
<i class="iconfont icon-yue"></i>
<span class="balance-text">{{ balances }}</span>
<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>
</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">{{ userInitial }}</div>
<div class="user-name">{{ loginStore.userInfo?.user?.name || loginStore.userInfo?.user?.username }}</div>
<svg class="dropdown-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M6 9l6 6 6-6" />
@ -59,7 +68,7 @@
</template>
<script setup lang='ts'>
import { ref, onMounted, onUnmounted, watch } from 'vue';
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';
@ -75,6 +84,9 @@ 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 = () => {

View File

@ -0,0 +1,74 @@
<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 type="primary" 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: 32px 16px;
box-sizing: border-box;
&.is-full-height {
height: 100%;
}
}
.login-prompt__content {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
text-align: center;
}
.login-prompt__title {
font-size: 18px;
font-weight: 600;
color: rgba(255, 255, 255, 0.9);
}
.login-prompt__description {
max-width: 520px;
line-height: 1.7;
color: rgba(255, 255, 255, 0.65);
}
.login-prompt__button {
min-width: 120px;
}
</style>

View File

@ -3,7 +3,10 @@
<div class="recharge-container">
<!-- 头部 -->
<div class="modal-header">
<h2>充值账户</h2>
<div class="header-copy">
<h2>充值账户</h2>
<p class="header-desc">选择金额后跳转到安全支付页面余额到账后可立即使用</p>
</div>
<button class="close-btn" @click="closeModal"></button>
</div>
@ -20,6 +23,7 @@
>
<span class="iconfont icon-zhifubaozhifu"></span>
<span class="provider-name">{{ provider.name }}</span>
<span class="provider-tag">推荐</span>
</button>
</div>
</div>
@ -49,8 +53,14 @@
<!-- 当前选中金额展示 -->
<div class="selected-info" v-if="selectedAmount">
<span>充值金额</span>
<strong class="selected-amount">¥{{ selectedAmount }}</strong>
<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>
@ -66,6 +76,11 @@
<path d="M12 6v6l4 2" />
</svg>
</button>
<div class="recharge-tips">
<span>支付由第三方渠道完成</span>
<span>如长时间未到账请刷新余额或联系客服</span>
</div>
</div>
<!-- 自定义金额弹窗 (独立遮罩) -->
@ -277,448 +292,5 @@ const closeModal = () => {
</script>
<style scoped lang="less">
//
@primary: #00f2fe;
@primary-dark: #4facfe;
@bg-dark: rgba(10, 20, 40, 0.92);
@glass-bg: rgba(20, 30, 55, 0.75);
@border-glow: 0 0 10px rgba(0, 242, 254, 0.3);
@text-light: #eef5ff;
@text-secondary: #b0c4ff;
//
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
//
.recharge-modal-mask {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.65);
backdrop-filter: blur(8px);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
transition: all 0.3s;
}
//
.recharge-container {
width: 520px;
max-width: 90vw;
background: @glass-bg;
backdrop-filter: blur(12px);
border-radius: 32px;
padding: 28px 32px 36px;
// box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(0, 242, 254, 0.2), 0 0 20px rgba(0, 242, 254, 0.3);
transition: transform 0.2s ease;
// border: 1px solid rgba(0, 242, 254, 0.4);
}
//
.modal-header {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 28px;
h2 {
font-size: 28px;
font-weight: 600;
background: linear-gradient(135deg, #fff, @primary);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
letter-spacing: 1px;
margin: 0;
text-shadow: 0 0 5px rgba(0, 242, 254, 0.3);
}
.close-btn {
background: rgba(255, 255, 255, 0.1);
border: none;
font-size: 20px;
cursor: pointer;
color: @text-secondary;
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
&:hover {
background: rgba(255, 255, 255, 0.25);
color: #fff;
transform: rotate(90deg);
}
}
}
//
.provider-section {
margin-bottom: 24px;
.section-title {
font-size: 14px;
text-transform: uppercase;
letter-spacing: 2px;
color: @text-secondary;
margin-bottom: 16px;
font-weight: 400;
}
}
.provider-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
margin-bottom: 24px;
}
.provider-btn {
background: rgba(15, 25, 45, 0.7);
border: 1px solid rgba(0, 242, 254, 0.3);
border-radius: 20px;
padding: 16px 0;
font-size: 18px;
font-weight: 600;
color: @text-light;
cursor: pointer;
transition: all 0.25s ease;
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
.provider-icon {
font-size: 24px;
}
.provider-name {
font-size: 16px;
}
&:hover {
transform: translateY(-2px);
border-color: @primary;
box-shadow: 0 0 12px rgba(0, 242, 254, 0.5);
background: rgba(0, 242, 254, 0.15);
}
&.active {
background: linear-gradient(135deg, rgba(0, 242, 254, 0.3), rgba(79, 172, 254, 0.3));
border-color: @primary;
box-shadow: 0 0 15px rgba(0, 242, 254, 0.6);
color: #fff;
// text-shadow: 0 0 4px rgba(0, 242, 254, 0.8);
}
.iconfont{
color: #00f2fe;
}
}
//
.amount-section {
margin-bottom: 32px;
.section-title {
font-size: 14px;
text-transform: uppercase;
letter-spacing: 2px;
color: @text-secondary;
margin-bottom: 16px;
font-weight: 400;
}
}
.amount-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
margin-bottom: 24px;
@media (min-width: 480px) {
grid-template-columns: repeat(4, 1fr);
}
}
.amount-btn {
background: rgba(15, 25, 45, 0.7);
border: 1px solid rgba(0, 242, 254, 0.3);
border-radius: 20px;
padding: 12px 0;
font-size: 20px;
font-weight: 600;
color: @text-light;
cursor: pointer;
transition: all 0.25s ease;
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
.currency {
font-size: 16px;
font-weight: 400;
opacity: 0.8;
}
&:hover {
transform: translateY(-2px);
border-color: @primary;
box-shadow: 0 0 12px rgba(0, 242, 254, 0.5);
background: rgba(0, 242, 254, 0.15);
}
&.active {
background: linear-gradient(135deg, rgba(0, 242, 254, 0.3), rgba(79, 172, 254, 0.3));
border-color: @primary;
box-shadow: 0 0 15px rgba(0, 242, 254, 0.6);
color: #fff;
text-shadow: 0 0 4px rgba(0, 242, 254, 0.8);
}
}
.custom-btn {
.currency {
font-size: 18px;
}
}
.selected-info {
text-align: right;
padding: 12px 8px 0;
border-top: 1px dashed rgba(0, 242, 254, 0.3);
font-size: 16px;
color: @text-secondary;
.selected-amount {
font-size: 24px;
font-weight: 700;
color: @primary;
margin-left: 8px;
text-shadow: 0 0 6px rgba(0, 242, 254, 0.5);
}
}
//
.confirm-btn {
width: 100%;
background: linear-gradient(90deg, @primary-dark, @primary);
border: none;
border-radius: 40px;
padding: 14px 0;
font-size: 18px;
font-weight: 600;
color: #0a1428;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
transition: all 0.3s;
box-shadow: 0 4px 12px rgba(0, 242, 254, 0.3);
letter-spacing: 2px;
.btn-icon {
width: 20px;
height: 20px;
transition: transform 0.2s;
&.loading {
animation: spin 1s linear infinite;
}
}
&:hover:not(:disabled) {
transform: scale(1.02);
box-shadow: 0 8px 20px rgba(0, 242, 254, 0.5);
filter: brightness(1.05);
.btn-icon:not(.loading) {
transform: translateX(4px);
}
}
&:active:not(:disabled) {
transform: scale(0.98);
}
&:disabled {
opacity: 0.7;
cursor: not-allowed;
transform: none !important;
box-shadow: none !important;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
}
// ()
.custom-modal-mask {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.75);
backdrop-filter: blur(12px);
display: flex;
align-items: center;
justify-content: center;
z-index: 1100;
}
.custom-dialog {
width: 100%;
max-width: 25vw;
background: rgba(12, 22, 40, 0.95);
backdrop-filter: blur(16px);
border-radius: 28px;
border: 1px solid rgba(0, 242, 254, 0.5);
box-shadow: 0 20px 35px rgba(0, 0, 0, 0.5), 0 0 25px rgba(0, 242, 254, 0.2);
overflow: hidden;
animation: slideUp 0.25s ease;
.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px 12px;
border-bottom: 1px solid rgba(0, 242, 254, 0.2);
h3 {
font-size: 22px;
font-weight: 500;
color: @text-light;
margin: 0;
background: linear-gradient(135deg, #fff, @primary);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
}
.close-btn {
background: transparent;
border: none;
font-size: 24px;
cursor: pointer;
color: @text-secondary;
transition: all 0.2s;
&:hover {
color: @primary;
transform: rotate(90deg);
}
}
}
.dialog-body {
padding: 28px 24px 20px;
.input-group {
display: flex;
align-items: center;
background: rgba(0, 0, 0, 0.4);
border-radius: 48px;
border: 1px solid rgba(0, 242, 254, 0.4);
transition: all 0.2s;
&:focus-within {
border-color: @primary;
box-shadow: 0 0 12px rgba(0, 242, 254, 0.4);
}
.input-currency {
font-size: 28px;
font-weight: 500;
padding: 0 16px;
color: @primary;
}
.custom-input {
flex: 1;
background: transparent;
border: none;
padding: 16px 16px 16px 0;
font-size: 26px;
font-weight: 500;
color: @text-light;
outline: none;
&::placeholder {
color: rgba(176, 196, 255, 0.5);
font-size: 20px;
font-weight: 400;
}
/* 去除 number 箭头 */
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
-moz-appearance: textfield;
}
}
.error-msg {
color: #ff6b6b;
font-size: 13px;
margin-top: 12px;
padding-left: 16px;
text-shadow: 0 0 2px rgba(0,0,0,0.5);
}
}
.dialog-footer {
display: flex;
gap: 16px;
padding: 12px 24px 28px;
.dialog-btn {
flex: 1;
padding: 12px 0;
border-radius: 40px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
border: none;
background: transparent;
&.cancel {
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.2);
color: @text-secondary;
&:hover {
background: rgba(255, 255, 255, 0.15);
color: #fff;
}
}
&.confirm {
background: linear-gradient(90deg, @primary-dark, @primary);
color: #0a1428;
box-shadow: 0 0 10px rgba(0, 242, 254, 0.4);
&:hover {
transform: scale(1.02);
filter: brightness(1.05);
}
}
}
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@import url('@/assets/less/Recharge/Recharge.less');
</style>

View File

@ -2,14 +2,12 @@
<div>
<el-card class="rig">
<!-- 未登录状态 -->
<div v-if="!isLoggedIn" class="login-require">
<div class="login-require-content">
<div class="message">请登录后再进行创作哦~</div>
<el-button type="primary" class="login-btn" @click="handleLogin">
登录
</el-button>
</div>
</div>
<LoginPrompt
v-if="!isLoggedIn"
class="login-require"
title="请登录后再进行创作哦~"
button-text="登录"
/>
<!-- 已登录状态 -->
<template v-else>
@ -144,15 +142,14 @@
<script setup lang='ts'>
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
import { useRouter } from 'vue-router';
import { ElLoading, ElMessage } from 'element-plus';
import { Loading } from '@element-plus/icons-vue';
import { useLoginStore } from '@/store/LoginStore';
import { getAsyncTaskStatus, getTodayTask } from '@/apis/Model';
import type { TaskItem, TaskResult } from '@/types/model';
import LoginPrompt from '@/components/LoginPrompt/LoginPrompt.vue';
// ==================== ====================
const router = useRouter();
const loginStore = useLoginStore();
// ==================== ====================
@ -211,13 +208,6 @@ const filteredTasks = computed(() => {
});
// ==================== ====================
/**
* 处理登录按钮点击
*/
const handleLogin = () => {
router.push('/login');
};
/**
* 处理筛选变化
*/

View File

@ -3,78 +3,75 @@
<!-- 顶部卡片 -->
<el-card class="card">
<div class="reference-container">
<!-- 上传模块 -->
<div class="upload-section">
<!-- 上传视频 -->
<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">
<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 class="reference-header">
<div class="reference-header__copy">
<div class="reference-header__title">参考多张图片中的主体生成视频</div>
<div class="reference-header__desc">
你可以上传图片选择主体或者直接使用已保存的主体
</div>
</div>
<!-- 分割线 -->
<div class="divider"></div>
<div class="reference-header__actions">
<input
ref="imageInputRef"
class="hidden-upload-input"
type="file"
accept="image/*"
@change="handleImageSelect"
/>
<input
ref="videoInputRef"
class="hidden-upload-input"
type="file"
accept="video/*"
@change="handleVideoSelect"
/>
<!-- 上传图片 -->
<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>
<button class="reference-action-btn" type="button" @click="triggerImageUpload">
<span class="reference-action-btn__icon">+</span>
<span>图片</span>
</button>
<button class="reference-action-btn" type="button" @click="triggerSubjectUpload">
<span class="reference-action-btn__icon">+</span>
<span>视频</span>
</button>
</div>
<div v-if="imageBase64 || videoBase64" class="reference-assets">
<div v-if="imageBase64" class="reference-asset-card">
<img :src="imagePreviewUrl" alt="图片预览" class="reference-asset-card__image" />
<div class="reference-asset-card__meta">
<span class="reference-asset-card__label">图片</span>
<div class="reference-asset-card__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>
<!-- 未上传时显示上传框 -->
<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 v-if="videoBase64" class="reference-asset-card">
<video
:src="videoPreviewUrl"
class="reference-asset-card__image"
muted
playsinline
preload="metadata"
/>
<div class="reference-asset-card__meta">
<span class="reference-asset-card__label">视频</span>
<div class="reference-asset-card__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张图片</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>
</div>
@ -181,7 +178,14 @@
</div>
<!-- 图片预览对话框 -->
<el-dialog v-model="dialogVisible">
<img w-full :src="dialogImageUrl" alt="Preview Image" />
<video
v-if="dialogPreviewType === 'video'"
class="dialog-preview-media"
:src="dialogPreviewUrl"
controls
autoplay
/>
<img v-else class="dialog-preview-media" :src="dialogPreviewUrl" alt="Preview Image" />
</el-dialog>
</template>
@ -189,7 +193,6 @@
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', 'task-created'])
@ -212,10 +215,18 @@ const promptText = ref('')
// base64
const videoBase64 = ref('')
const imageBase64 = ref('')
const videoMimeType = ref('')
const imageMimeType = ref('')
const imageInputRef = ref<HTMLInputElement | null>(null)
const videoInputRef = ref<HTMLInputElement | null>(null)
//
const dialogVisible = ref(false)
const dialogImageUrl = ref('')
const dialogPreviewType = ref<'image' | 'video'>('image')
const imagePreviewUrl = computed(() => toDataUrl(imageMimeType.value, imageBase64.value))
const videoPreviewUrl = computed(() => toDataUrl(videoMimeType.value, videoBase64.value))
const dialogPreviewUrl = computed(() => dialogPreviewType.value === 'video' ? videoPreviewUrl.value : imagePreviewUrl.value)
//
const price = computed(() => {
@ -249,57 +260,143 @@ const handlePromptInput = (value: string) => {
promptText.value = value
}
const triggerImageUpload = () => {
imageInputRef.value?.click()
}
const triggerSubjectUpload = () => {
videoInputRef.value?.click()
}
const toDataUrl = (mimeType: string, base64: string) => {
if (!mimeType || !base64) {
return ''
}
return `data:${mimeType};base64,${base64}`
}
const normalizeBase64 = (result: string) => {
const [, pureBase64 = ''] = result.split(',')
return pureBase64
}
const readFileAsBase64 = (file: File) => {
return new Promise<string>((resolve, reject) => {
const reader = new FileReader()
reader.onload = (event) => {
const result = event.target?.result
if (typeof result !== 'string') {
reject(new Error('文件读取失败'))
return
}
resolve(normalizeBase64(result))
}
reader.onerror = () => reject(new Error('文件读取失败'))
reader.readAsDataURL(file)
})
}
const resetInputValue = (inputRef: typeof imageInputRef | typeof videoInputRef) => {
if (inputRef.value) {
inputRef.value.value = ''
}
}
const handleImageSelect = async (event: Event) => {
const input = event.target as HTMLInputElement
const file = input.files?.[0]
if (!file) {
return
}
await handleImageChange(file)
resetInputValue(imageInputRef)
}
const handleVideoSelect = async (event: Event) => {
const input = event.target as HTMLInputElement
const file = input.files?.[0]
if (!file) {
return
}
await handleVideoChange(file)
resetInputValue(videoInputRef)
}
//
const handleVideoChange = (file: UploadFile) => {
const handleVideoChange = async (file: File) => {
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
if (!file.type.startsWith('video/')) {
ElMessage.warning('请上传视频文件')
return
}
try {
const base64 = await readFileAsBase64(file)
videoBase64.value = base64
videoMimeType.value = file.type
console.log('视频上传成功Base64长度:', base64.length)
ElMessage.success('视频上传成功')
} catch (error) {
console.error('视频上传失败:', error)
ElMessage.error('视频上传失败,请重试')
}
reader.readAsDataURL(rawFile)
}
//
const handleImageChange = (file: UploadFile) => {
const handleImageChange = async (file: File) => {
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
if (!file.type.startsWith('image/')) {
ElMessage.warning('请上传图片文件')
return
}
try {
const base64 = await readFileAsBase64(file)
imageBase64.value = base64
imageMimeType.value = file.type
console.log('图片上传成功Base64长度:', base64.length)
ElMessage.success('图片上传成功')
} catch (error) {
console.error('图片上传失败:', error)
ElMessage.error('图片上传失败,请重试')
}
reader.readAsDataURL(rawFile)
}
//
const handleRemoveVideo = () => {
videoBase64.value = ''
videoMimeType.value = ''
resetInputValue(videoInputRef)
ElMessage.info('已删除视频')
}
//
const handleRemoveImage = () => {
imageBase64.value = ''
imageMimeType.value = ''
resetInputValue(imageInputRef)
ElMessage.info('已删除图片')
}
//
const handlePreview = (type: 'video' | 'image') => {
dialogImageUrl.value = type === 'video' ? videoBase64.value : imageBase64.value
dialogPreviewType.value = type
dialogVisible.value = true
}
@ -319,12 +416,14 @@ const buildSubmitParams = () => {
// base64
if (videoBase64.value) {
params.video = videoBase64.value
params.videoMimeType = videoMimeType.value
console.log('添加视频参数Base64长度:', videoBase64.value.length)
}
// base64
if (imageBase64.value) {
params.image = imageBase64.value
params.imageMimeType = imageMimeType.value
console.log('添加图片参数Base64长度:', imageBase64.value.length)
}
@ -357,6 +456,10 @@ const resetForm = () => {
promptText.value = ''
videoBase64.value = ''
imageBase64.value = ''
videoMimeType.value = ''
imageMimeType.value = ''
resetInputValue(imageInputRef)
resetInputValue(videoInputRef)
selectedDuration.value = 5
selectedCount.value = 1
emit('prompt-change', true)
@ -448,85 +551,115 @@ defineExpose({
color: #fff;
}
.upload-section {
border: 1px dashed #35383F;
.reference-header {
display: flex;
flex-direction: column;
gap: 18px;
padding: 8px 0 4px;
}
.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);
}
}
.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-color: rgba(0, 0, 0, 0.36);
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.upload-item {
position: relative;
display: flex;
align-items: center;
gap: 16px;
padding: 16px 0 16px 24px;
height: 88px;
cursor: pointer;
transition: background-color 0.2s;
.reference-asset-card__image {
display: block;
width: 100%;
height: 110px;
object-fit: cover;
background: rgba(0, 0, 0, 0.24);
}
&:hover {
background-color: rgba(255, 255, 255, 0.08);
}
.dialog-preview-media {
display: block;
width: 100%;
max-height: 70vh;
object-fit: contain;
}
&:first-child {
border-radius: 12px 12px 0 0;
}
.reference-asset-card__meta {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 10px 12px;
}
&:last-child {
border-radius: 0 0 12px 12px;
}
.reference-asset-card__label {
font-size: 13px;
color: rgba(255, 255, 255, 0.8);
}
.upload-icon {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.08);
font-size: 20px;
color: #fff;
flex-shrink: 0;
}
.upload-text {
display: flex;
flex-direction: column;
z-index: 10;
.upload-title {
font-weight: 600;
font-size: 14px;
line-height: 1.25;
color: #fff;
margin-bottom: 2px;
}
.upload-desc {
font-size: 12px;
line-height: 1.25;
color: rgba(255, 255, 255, 0.48);
}
}
.upload-preview {
position: absolute;
right: 0;
bottom: 0;
width: 148px;
aspect-ratio: 185/75;
object-fit: cover;
@media (max-width: 768px) {
width: 185px;
}
}
}
.divider {
height: 1px;
margin: 0 24px;
background-color: rgba(255, 255, 255, 0.08);
}
.reference-asset-card__actions {
display: flex;
align-items: center;
gap: 8px;
}
.input {
@ -633,79 +766,24 @@ defineExpose({
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;
.uploaded-action {
display: flex;
align-items: center;
gap: 16px;
padding: 16px 24px;
height: 88px;
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;
transition: background-color 0.2s ease;
&:hover {
background-color: rgba(255, 255, 255, 0.08);
background-color: rgba(255, 255, 255, 0.24);
}
.upload-button {
position: absolute;
right: 24px;
.el-icon {
font-size: 16px;
}
}
</style>

View File

@ -217,7 +217,6 @@ onUnmounted(() => {
flex: 1;
min-height: 0;
overflow-y: auto;
padding-bottom: 96px;
}
.create-button-container {
z-index: 999;

View File

@ -1,6 +1,14 @@
<template>
<div class="apikey-page">
<el-card class="apikey-card">
<LoginPrompt
v-if="!isLoggedIn"
class="login-fullscreen"
title="请先登录后管理 API Key"
description="登录后可创建、复制、查看和删除自己的访问令牌。"
full-height
/>
<el-card v-else class="apikey-card">
<div class="page-header">
<div class="header-main">
<div class="title-row">
@ -26,44 +34,33 @@
</div>
</div>
<div v-if="!isLoggedIn" class="login-require">
<div class="login-require-content">
<img src="../../assets/images/preparation.png" alt="">
<div class="login-title">请先登录后管理 API Key</div>
<div class="login-desc">登录后可创建复制查看和删除自己的访问令牌</div>
<el-button type="primary" @click="handleLogin">去登录</el-button>
<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>
<template v-else>
<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>
</template>
<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 对话框 -->
@ -117,12 +114,11 @@
import { computed, reactive, ref, watch, onMounted } from 'vue';
import { ElMessage } from 'element-plus';
import type { FormInstance, FormRules } from 'element-plus';
import { useRouter } from 'vue-router';
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 router = useRouter();
const loginStore = useLoginStore();
const isLoggedIn = computed(() => loginStore.isLoggedIn);
@ -277,11 +273,6 @@ const handleViewDialogClose = () => {
viewingKey.value = null;
};
//
const handleLogin = () => {
router.push('/login');
};
//
watch(
() => loginStore.isLoggedIn,
@ -303,6 +294,10 @@ onMounted(() => {
</script>
<style scoped lang='less'>
@import url('@/assets/less/apiKey/apikey.less');
.login-fullscreen {
height: 100%;
}
</style>
<style lang='less'>

View File

@ -30,14 +30,11 @@
<!-- 内容区 -->
<div class="bill-content">
<div v-if="!isLoggedIn" class="login-require">
<div class="login-require-content">
<div class="login-require-text">请先登录后查看消费记录</div>
<el-button type="primary" class="login-btn" @click="handleLogin">
去登录
</el-button>
</div>
</div>
<LoginPrompt
v-if="!isLoggedIn"
class="login-require"
title="请先登录后查看消费记录"
/>
<!-- 加载中 -->
<div v-else-if="isLoading" class="loading-container">
@ -127,7 +124,7 @@ import { ref, onMounted, computed, watch } from 'vue';
import { ElMessage } from 'element-plus';
import { consumptionRecord } from '@/apis/home';
import { useLoginStore } from '@/store/LoginStore';
import router from '@/router';
import LoginPrompt from '@/components/LoginPrompt/LoginPrompt.vue';
// ==================== ====================
const loginStore = useLoginStore();
@ -286,10 +283,6 @@ const handleFilterChange = () => {
updatePaginatedList(); //
};
const handleLogin = () => {
router.push('/login');
};
// ==================== ====================
/**
* 加载订单列表