4.17更新
This commit is contained in:
parent
f84af8856e
commit
eb6059267d
@ -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;
|
||||
}
|
||||
|
||||
|
||||
547
src/assets/less/Recharge/Recharge.less
Normal file
547
src/assets/less/Recharge/Recharge.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 = () => {
|
||||
|
||||
74
src/components/LoginPrompt/LoginPrompt.vue
Normal file
74
src/components/LoginPrompt/LoginPrompt.vue
Normal 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>
|
||||
@ -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>
|
||||
@ -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');
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理筛选变化
|
||||
*/
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -217,7 +217,6 @@ onUnmounted(() => {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding-bottom: 96px;
|
||||
}
|
||||
.create-button-container {
|
||||
z-index: 999;
|
||||
|
||||
@ -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'>
|
||||
|
||||
@ -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');
|
||||
};
|
||||
|
||||
// ==================== 数据加载 ====================
|
||||
/**
|
||||
* 加载订单列表
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user