init
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
page: number
|
||||
pageSize: number
|
||||
total: number
|
||||
loading?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'change', page: number): void
|
||||
}>()
|
||||
|
||||
function goPrev() {
|
||||
if (props.page <= 1 || props.loading) {
|
||||
return
|
||||
}
|
||||
|
||||
emit('change', props.page - 1)
|
||||
}
|
||||
|
||||
function goNext() {
|
||||
if (props.loading || props.page * props.pageSize >= props.total) {
|
||||
return
|
||||
}
|
||||
|
||||
emit('change', props.page + 1)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer class="pagination-bar">
|
||||
<span>第 {{ page }} 页,共 {{ Math.max(1, Math.ceil(total / pageSize)) }} 页,合计 {{ total }} 条</span>
|
||||
<div class="actions">
|
||||
<el-button :disabled="page <= 1 || loading" round @click="goPrev">上一页</el-button>
|
||||
<el-button :disabled="page * pageSize >= total || loading" round @click="goNext">下一页</el-button>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.pagination-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-top: 14px;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 780px) {
|
||||
.pagination-bar {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,130 @@
|
||||
<script setup lang="ts">
|
||||
import { formatStatusWithRaw } from '@/utils/admin-display'
|
||||
|
||||
const props = defineProps<{
|
||||
status: string
|
||||
}>()
|
||||
|
||||
function resolveTone(status: string) {
|
||||
const normalized = String(status || '').trim().toLowerCase()
|
||||
|
||||
if (['paid', 'link_generated', 'redeemed', 'available', 'delivered', 'success', 'active', 'system_bound', 'binding_completed'].includes(normalized)) {
|
||||
return 'success'
|
||||
}
|
||||
|
||||
if (['claimed', 'role_confirmed', 'redeeming', 'reserved', 'processing', 'admin', 'waiting_user_claim', 'link_opened', 'binding_confirmed', 'binding_in_progress', 'user_binding'].includes(normalized)) {
|
||||
return 'primary'
|
||||
}
|
||||
|
||||
if (['waiting_inventory', 'retry_pending', 'manual_review', 'invalid', 'expired', 'operator', 'pending_binding', 'binding_exception', 'not_started'].includes(normalized)) {
|
||||
return 'warning'
|
||||
}
|
||||
|
||||
if (['closed', 'failed', 'unpaid', 'revoked', 'disabled'].includes(normalized)) {
|
||||
return 'danger'
|
||||
}
|
||||
|
||||
return 'default'
|
||||
}
|
||||
|
||||
function resolveLabel(status: string) {
|
||||
const normalized = String(status || '').trim()
|
||||
|
||||
if (!normalized) {
|
||||
return '-'
|
||||
}
|
||||
|
||||
const labelMap: Record<string, string> = {
|
||||
created: '已创建',
|
||||
paid: '已支付',
|
||||
unpaid: '未支付',
|
||||
failed: '失败',
|
||||
refunded: '已退款',
|
||||
refunding: '退款中',
|
||||
closed: '已关闭',
|
||||
pending_payment: '待支付',
|
||||
waiting_inventory: '待库存',
|
||||
cdk_reserved: '已预占',
|
||||
link_generated: '已生成链接',
|
||||
claimed: '已创建会话',
|
||||
role_confirmed: '已确认角色',
|
||||
redeeming: '兑换中',
|
||||
redeemed: '已兑换',
|
||||
retry_pending: '待重试',
|
||||
manual_review: '人工处理',
|
||||
expired: '已过期',
|
||||
available: '可用',
|
||||
reserved: '已预占',
|
||||
delivered: '已发放',
|
||||
invalid: '已作废',
|
||||
active: '生效中',
|
||||
admin: '管理员',
|
||||
operator: '普通运营',
|
||||
revoked: '已撤销',
|
||||
disabled: '已停用',
|
||||
success: '成功',
|
||||
processing: '处理中',
|
||||
system_bound: '系统已绑定',
|
||||
pending_binding: '待系统绑定',
|
||||
not_started: '未开始',
|
||||
waiting_user_claim: '待用户打开链接',
|
||||
link_opened: '已打开链接',
|
||||
binding_confirmed: '已提交绑定',
|
||||
binding_in_progress: '绑定处理中',
|
||||
binding_completed: '完整绑定成功',
|
||||
binding_exception: '绑定异常',
|
||||
user_binding: '用户绑定中',
|
||||
}
|
||||
|
||||
return formatStatusWithRaw(normalized, labelMap)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="status-tag" :data-tone="resolveTone(props.status)">
|
||||
{{ resolveLabel(props.status) }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.status-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 28px;
|
||||
padding: 0 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.status-tag[data-tone='success'] {
|
||||
color: #0f766e;
|
||||
background: #ecfdf3;
|
||||
border-color: #a6f4c5;
|
||||
}
|
||||
|
||||
.status-tag[data-tone='primary'] {
|
||||
color: #175cd3;
|
||||
background: #eff8ff;
|
||||
border-color: #b2ddff;
|
||||
}
|
||||
|
||||
.status-tag[data-tone='warning'] {
|
||||
color: #b54708;
|
||||
background: #fffaeb;
|
||||
border-color: #fedf89;
|
||||
}
|
||||
|
||||
.status-tag[data-tone='danger'] {
|
||||
color: #b42318;
|
||||
background: #fef3f2;
|
||||
border-color: #fecdca;
|
||||
}
|
||||
|
||||
.status-tag[data-tone='default'] {
|
||||
color: #475467;
|
||||
background: #f2f4f7;
|
||||
border-color: #d0d5dd;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,389 @@
|
||||
<script setup lang="ts">
|
||||
import type { TencentLoginType } from '@/types/tencent/session'
|
||||
|
||||
type LoginTab = {
|
||||
value: TencentLoginType
|
||||
label: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
currentLoginTabLabel: string
|
||||
hasSession: boolean
|
||||
initButtonLabel: string
|
||||
loginTabs: LoginTab[]
|
||||
loginType: TencentLoginType
|
||||
loginTypeLabel: string
|
||||
qrFigureStyle: { width: string; maxWidth: string }
|
||||
qrImage: string
|
||||
scanInstruction: string
|
||||
sessionStatus?: string
|
||||
sessionId: string
|
||||
sessionLoading: boolean
|
||||
sessionNotice: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
createSession: [loginType: TencentLoginType]
|
||||
openQrPreview: []
|
||||
qrImageLoad: [event: Event]
|
||||
reloadSession: []
|
||||
switchLoginType: [loginType: TencentLoginType]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="auth-card">
|
||||
<div class="login-tabs">
|
||||
<button
|
||||
v-for="tab in props.loginTabs"
|
||||
:key="tab.value"
|
||||
:class="['login-tab', { active: props.loginType === tab.value }]"
|
||||
type="button"
|
||||
@click="emit('switchLoginType', tab.value)"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="qr-stage">
|
||||
<button
|
||||
v-if="props.qrImage && props.sessionStatus === 'waiting_scan'"
|
||||
class="qr-figure"
|
||||
:style="props.qrFigureStyle"
|
||||
type="button"
|
||||
@click="emit('openQrPreview')"
|
||||
>
|
||||
<img
|
||||
:src="props.qrImage"
|
||||
:alt="`Tencent ${props.loginTypeLabel} Login QR`"
|
||||
decoding="async"
|
||||
@load="emit('qrImageLoad', $event)"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<div v-else-if="props.sessionStatus === 'scanned'" class="status-state confirm-state">
|
||||
<strong>确认中</strong>
|
||||
<p>扫码已完成,请在手机上点击确认登录。确认后页面会继续同步角色信息。</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="['logged_in', 'ready_to_redeem', 'redeeming', 'redeemed'].includes(String(props.sessionStatus || ''))"
|
||||
class="status-state success-state"
|
||||
>
|
||||
<strong>已完成登录</strong>
|
||||
<p>二维码已自动收起,当前会话已登录,正在同步角色或等待后续兑换操作。</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="qr-empty">
|
||||
<strong>{{ props.hasSession ? '二维码生成中' : props.currentLoginTabLabel }}</strong>
|
||||
<p>
|
||||
{{
|
||||
props.hasSession
|
||||
? '浏览器会话已启动,等待二维码加载。'
|
||||
: `先选择${props.loginTypeLabel},再点击下方按钮开始初始化。`
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="auth-copy">
|
||||
<p class="scan-hint">
|
||||
{{ props.hasSession ? props.scanInstruction : `已选择${props.loginTypeLabel},等待开始初始化` }}
|
||||
</p>
|
||||
<p class="scan-note">
|
||||
{{
|
||||
props.sessionNotice ||
|
||||
(props.hasSession
|
||||
? `请使用${props.loginTypeLabel}扫码登录`
|
||||
: `点击“${props.initButtonLabel}”后,页面才会真正创建浏览器会话`)
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="auth-footer">
|
||||
<div class="session-strip">
|
||||
<span>当前会话</span>
|
||||
<strong>{{ props.sessionId || '尚未创建' }}</strong>
|
||||
</div>
|
||||
|
||||
<div class="action-row">
|
||||
<el-button
|
||||
:loading="props.sessionLoading"
|
||||
class="primary-action"
|
||||
round
|
||||
size="large"
|
||||
type="primary"
|
||||
@click="emit('createSession', props.loginType)"
|
||||
>
|
||||
{{ props.hasSession ? '重新生成二维码' : props.initButtonLabel }}
|
||||
</el-button>
|
||||
<el-button
|
||||
:disabled="!props.hasSession"
|
||||
:loading="props.sessionLoading"
|
||||
class="secondary-action"
|
||||
round
|
||||
size="large"
|
||||
@click="emit('reloadSession')"
|
||||
>
|
||||
刷新后端页面
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.auth-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 22px;
|
||||
border-radius: 30px;
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
border: 1px solid rgba(86, 108, 138, 0.1);
|
||||
box-shadow: 0 22px 70px rgba(30, 49, 78, 0.08);
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
.login-tabs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0;
|
||||
padding: 0;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #dbe4ee;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.login-tab {
|
||||
min-height: 60px;
|
||||
padding: 0 16px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #606f84;
|
||||
cursor: pointer;
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
transition:
|
||||
background-color 180ms ease,
|
||||
color 180ms ease,
|
||||
box-shadow 180ms ease;
|
||||
}
|
||||
|
||||
.login-tab.active {
|
||||
color: #ffffff;
|
||||
background: linear-gradient(180deg, #6ba5f7 0%, #5b8ff0 100%);
|
||||
box-shadow: inset 0 -1px 0 rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.qr-stage {
|
||||
margin-top: 18px;
|
||||
padding: 18px 16px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(86, 108, 138, 0.08);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(245, 248, 252, 0.98), rgba(252, 253, 255, 0.96)),
|
||||
radial-gradient(circle at top, rgba(101, 146, 233, 0.08), transparent 48%);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.qr-figure {
|
||||
display: block;
|
||||
width: min(220px, 100%);
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
.qr-figure img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 16px;
|
||||
border: 1px solid #d9e2ef;
|
||||
box-shadow: 0 10px 24px rgba(31, 50, 79, 0.08);
|
||||
image-rendering: -moz-crisp-edges;
|
||||
image-rendering: crisp-edges;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.qr-empty {
|
||||
width: min(220px, 100%);
|
||||
min-height: 220px;
|
||||
padding: 18px;
|
||||
border-radius: 16px;
|
||||
border: 1px dashed #d4ddeb;
|
||||
background: rgba(247, 250, 254, 0.92);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.qr-empty strong {
|
||||
font-size: 18px;
|
||||
color: #27384f;
|
||||
}
|
||||
|
||||
.qr-empty p {
|
||||
margin: 10px 0 0;
|
||||
color: #67778d;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.status-state {
|
||||
width: min(220px, 100%);
|
||||
min-height: 220px;
|
||||
padding: 18px;
|
||||
border-radius: 16px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.status-state strong {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.status-state p {
|
||||
margin: 10px 0 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.confirm-state {
|
||||
border: 1px solid #f9c74f;
|
||||
background: linear-gradient(180deg, rgba(255, 247, 214, 0.95), rgba(255, 251, 235, 0.98));
|
||||
}
|
||||
|
||||
.confirm-state strong {
|
||||
color: #9a3412;
|
||||
}
|
||||
|
||||
.confirm-state p {
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
.success-state {
|
||||
border: 1px solid #86efac;
|
||||
background: linear-gradient(180deg, rgba(220, 252, 231, 0.96), rgba(240, 253, 244, 0.98));
|
||||
}
|
||||
|
||||
.success-state strong {
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.success-state p {
|
||||
color: #15803d;
|
||||
}
|
||||
|
||||
.auth-copy {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.scan-hint {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #243852;
|
||||
}
|
||||
|
||||
.scan-note {
|
||||
margin: 8px 0 0;
|
||||
color: #64748b;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.auth-footer {
|
||||
margin-top: auto;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.session-strip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
min-height: 46px;
|
||||
padding: 0 16px;
|
||||
border-radius: 16px;
|
||||
background: #f6f9fd;
|
||||
border: 1px solid #e2e9f2;
|
||||
}
|
||||
|
||||
.session-strip span {
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.session-strip strong {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
color: #22354e;
|
||||
font-size: 13px;
|
||||
font-family: 'JetBrains Mono', 'SFMono-Regular', ui-monospace, monospace;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.action-row {
|
||||
margin-top: 16px;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.45fr) minmax(132px, 0.95fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.primary-action :deep(span) {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.action-row :deep(.el-button) {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.action-row :deep(.el-button > span) {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.primary-action,
|
||||
.secondary-action {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.auth-card {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.login-tab {
|
||||
font-size: 15px;
|
||||
min-height: 54px;
|
||||
}
|
||||
|
||||
.scan-hint {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.session-strip,
|
||||
.action-row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.action-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.action-row :deep(.el-button) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,284 @@
|
||||
<script setup lang="ts">
|
||||
type FactItem = {
|
||||
label: string
|
||||
value: string
|
||||
accent?: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
canRedeem: boolean
|
||||
canConfirmRole?: boolean
|
||||
confirmActionLabel?: string
|
||||
confirmActionLoading?: boolean
|
||||
confirmActionVisible?: boolean
|
||||
confirmReadonly?: boolean
|
||||
hideRedeemForm?: boolean
|
||||
loginTypeLabel: string
|
||||
maxAttempts: number
|
||||
redeemBlockedReason: string
|
||||
redeemButtonLabel: string
|
||||
redeemCode: string
|
||||
redeemLoading: boolean
|
||||
roleConfirmed: boolean
|
||||
roleFacts: FactItem[]
|
||||
roleReady: boolean
|
||||
statusLabel: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
confirmRole: []
|
||||
redeem: []
|
||||
'update:maxAttempts': [value: number]
|
||||
'update:redeemCode': [value: string]
|
||||
'update:roleConfirmed': [value: boolean]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="panel-card">
|
||||
<div class="section-head">
|
||||
<strong>角色与兑换</strong>
|
||||
</div>
|
||||
|
||||
<div class="inline-summary">
|
||||
<div class="inline-summary-item">
|
||||
<span>会话状态</span>
|
||||
<strong>{{ props.statusLabel }}</strong>
|
||||
</div>
|
||||
<div class="inline-summary-item">
|
||||
<span>登录方式</span>
|
||||
<strong>{{ props.loginTypeLabel }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fact-grid role-grid">
|
||||
<article
|
||||
v-for="fact in props.roleFacts"
|
||||
:key="fact.label"
|
||||
:class="['fact-card', { accent: fact.accent }]"
|
||||
>
|
||||
<span>{{ fact.label }}</span>
|
||||
<strong>{{ fact.value }}</strong>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div v-if="props.confirmActionVisible" class="warning-action-box">
|
||||
<div class="warning-copy">
|
||||
<strong>请先确认当前角色与大区准确无误</strong>
|
||||
<p>确认后将按当前识别到的角色继续兑换,选错角色可能导致发放错误。</p>
|
||||
</div>
|
||||
<el-button
|
||||
:disabled="!props.canConfirmRole"
|
||||
:loading="props.confirmActionLoading"
|
||||
class="warning-action-button"
|
||||
round
|
||||
size="large"
|
||||
type="danger"
|
||||
@click="emit('confirmRole')"
|
||||
>
|
||||
{{ props.confirmActionLabel || '确认当前角色' }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div v-else class="confirm-box">
|
||||
<el-checkbox
|
||||
:model-value="props.roleConfirmed"
|
||||
:disabled="props.confirmReadonly || !props.roleReady"
|
||||
@update:model-value="emit('update:roleConfirmed', Boolean($event))"
|
||||
>
|
||||
我已确认当前角色与大区无误
|
||||
</el-checkbox>
|
||||
</div>
|
||||
|
||||
<el-form v-if="!props.hideRedeemForm" label-position="top" class="redeem-form">
|
||||
<el-form-item label="兑换码">
|
||||
<el-input
|
||||
:model-value="props.redeemCode"
|
||||
placeholder="请输入兑换码"
|
||||
@update:model-value="emit('update:redeemCode', String($event))"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<div class="field-row compact">
|
||||
<el-form-item label="验证码重试次数">
|
||||
<el-input-number
|
||||
:max="12"
|
||||
:min="1"
|
||||
:model-value="props.maxAttempts"
|
||||
@update:model-value="emit('update:maxAttempts', Number($event || 1))"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
|
||||
<div class="redeem-actions">
|
||||
<el-button
|
||||
:disabled="!props.canRedeem"
|
||||
:loading="props.redeemLoading"
|
||||
class="redeem-button"
|
||||
round
|
||||
size="large"
|
||||
type="success"
|
||||
@click="emit('redeem')"
|
||||
>
|
||||
{{ props.redeemButtonLabel }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<p class="helper-copy">
|
||||
{{ props.redeemBlockedReason || '当前状态允许直接发起兑换' }}
|
||||
</p>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.panel-card {
|
||||
height: 100%;
|
||||
padding: 22px;
|
||||
border-radius: 30px;
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
border: 1px solid rgba(86, 108, 138, 0.1);
|
||||
box-shadow: 0 22px 70px rgba(30, 49, 78, 0.08);
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
.section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.section-head strong {
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
color: #22354e;
|
||||
}
|
||||
|
||||
.inline-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.inline-summary-item {
|
||||
padding: 12px 14px;
|
||||
border-radius: 14px;
|
||||
background: #f7f9fc;
|
||||
}
|
||||
|
||||
.inline-summary-item span,
|
||||
.fact-card span {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #708096;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.inline-summary-item strong,
|
||||
.fact-card strong {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
color: #1f324a;
|
||||
line-height: 1.5;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.fact-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.role-grid {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.fact-card {
|
||||
padding: 14px;
|
||||
border-radius: 14px;
|
||||
background: #f7f9fc;
|
||||
}
|
||||
|
||||
.fact-card.accent {
|
||||
background: linear-gradient(180deg, rgba(86, 143, 244, 0.14), rgba(86, 143, 244, 0.08));
|
||||
}
|
||||
|
||||
.confirm-box {
|
||||
margin-top: 14px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 14px;
|
||||
background: rgba(95, 150, 245, 0.08);
|
||||
border: 1px solid rgba(95, 150, 245, 0.14);
|
||||
}
|
||||
|
||||
.warning-action-box {
|
||||
margin-top: 14px;
|
||||
padding: 16px;
|
||||
border-radius: 18px;
|
||||
background: linear-gradient(180deg, rgba(254, 226, 226, 0.92), rgba(254, 242, 242, 0.96));
|
||||
border: 1px solid rgba(248, 113, 113, 0.3);
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.warning-copy strong {
|
||||
display: block;
|
||||
color: #991b1b;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.warning-copy p {
|
||||
margin: 6px 0 0;
|
||||
color: #b91c1c;
|
||||
line-height: 1.7;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.warning-action-button {
|
||||
min-width: 220px;
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.redeem-form {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.field-row.compact {
|
||||
display: grid;
|
||||
grid-template-columns: 220px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.redeem-actions {
|
||||
margin-top: 16px;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.redeem-button {
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.helper-copy {
|
||||
margin: 12px 0 0;
|
||||
color: #64748b;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
@media (max-width: 1140px) {
|
||||
.inline-summary,
|
||||
.fact-grid,
|
||||
.role-grid,
|
||||
.field-row.compact {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.panel-card {
|
||||
padding: 18px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,158 @@
|
||||
<script setup lang="ts">
|
||||
type FactItem = {
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
resultFacts: FactItem[]
|
||||
screenshotEmptyMessage: string
|
||||
screenshotEmptyTitle: string
|
||||
screenshotUrl: string
|
||||
showScreenshot: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
openScreenshotPreview: []
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="result-card">
|
||||
<div class="section-head">
|
||||
<strong>兑换结果</strong>
|
||||
</div>
|
||||
|
||||
<div class="result-layout">
|
||||
<div class="fact-grid result-grid">
|
||||
<article v-for="fact in props.resultFacts" :key="fact.label" class="fact-card">
|
||||
<span>{{ fact.label }}</span>
|
||||
<strong>{{ fact.value }}</strong>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-if="props.showScreenshot"
|
||||
class="screenshot-figure"
|
||||
type="button"
|
||||
@click="emit('openScreenshotPreview')"
|
||||
>
|
||||
<img :src="props.screenshotUrl" alt="Tencent redeem screenshot" />
|
||||
</button>
|
||||
|
||||
<div v-else class="empty-block">
|
||||
<strong>{{ props.screenshotEmptyTitle }}</strong>
|
||||
<p>{{ props.screenshotEmptyMessage }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.result-card {
|
||||
padding: 22px 22px 20px;
|
||||
border-radius: 30px;
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
border: 1px solid rgba(86, 108, 138, 0.1);
|
||||
box-shadow: 0 22px 70px rgba(30, 49, 78, 0.08);
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
.section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.section-head strong {
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
color: #22354e;
|
||||
}
|
||||
|
||||
.result-layout {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.fact-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.fact-card {
|
||||
padding: 14px;
|
||||
border-radius: 14px;
|
||||
background: #f7f9fc;
|
||||
}
|
||||
|
||||
.fact-card span {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #708096;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.fact-card strong {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
color: #1f324a;
|
||||
line-height: 1.5;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.screenshot-figure {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
border: 1px solid #e2e9f2;
|
||||
border-radius: 22px;
|
||||
overflow: hidden;
|
||||
background: #f4f8fc;
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
.screenshot-figure img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.empty-block {
|
||||
margin-top: 14px;
|
||||
min-height: 180px;
|
||||
border-radius: 18px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
text-align: center;
|
||||
padding: 24px;
|
||||
background: #f8fbff;
|
||||
border: 1px dashed #d5dfec;
|
||||
}
|
||||
|
||||
.empty-block strong {
|
||||
color: #2c4058;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.empty-block p {
|
||||
margin: 8px 0 0;
|
||||
color: #6b7a91;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
@media (max-width: 1140px) {
|
||||
.fact-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.result-card {
|
||||
padding: 18px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user