863 lines
19 KiB
Vue
863 lines
19 KiB
Vue
<!--
|
||
AuthInteractiveLoginDemo.vue
|
||
交互式登录流程演示
|
||
|
||
用途:
|
||
通过模拟真实的登录流程,让用户直观理解认证和授权的概念。
|
||
|
||
互动功能:
|
||
- 模拟登录:输入用户名密码,看到完整的认证流程
|
||
- 可视化数据流:HTTP 请求和响应的实时展示
|
||
- 两种模式对比:Session vs JWT
|
||
-->
|
||
<template>
|
||
<div class="auth-interactive-login">
|
||
<div class="header">
|
||
<div class="title">🔐 认证流程演示</div>
|
||
<div class="subtitle">模拟登录过程,理解认证与授权的区别</div>
|
||
</div>
|
||
|
||
<!-- 模式切换 -->
|
||
<div class="mode-selector">
|
||
<div class="mode-label">选择鉴权方式:</div>
|
||
<div class="mode-buttons">
|
||
<button
|
||
class="mode-btn"
|
||
:class="{ active: mode === 'session' }"
|
||
@click="switchMode('session')"
|
||
>
|
||
🍪 Session + Cookie
|
||
</button>
|
||
<button
|
||
class="mode-btn"
|
||
:class="{ active: mode === 'jwt' }"
|
||
@click="switchMode('jwt')"
|
||
>
|
||
🎫 JWT Token
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="main-content">
|
||
<!-- 登录表单 -->
|
||
<div class="login-section">
|
||
<div class="form-container">
|
||
<div class="form-title">登录表单</div>
|
||
<div class="form-fields">
|
||
<div class="field-group">
|
||
<label>用户名</label>
|
||
<input
|
||
type="text"
|
||
v-model="username"
|
||
placeholder="输入用户名"
|
||
:disabled="locked"
|
||
/>
|
||
</div>
|
||
<div class="field-group">
|
||
<label>密码</label>
|
||
<input
|
||
type="password"
|
||
v-model="password"
|
||
placeholder="输入密码"
|
||
:disabled="locked"
|
||
@keyup.enter="startDemo"
|
||
/>
|
||
</div>
|
||
<button
|
||
class="login-btn"
|
||
@click="startDemo"
|
||
:disabled="!username || !password || locked"
|
||
>
|
||
开始演示
|
||
</button>
|
||
</div>
|
||
|
||
<div class="hints">
|
||
<div class="hint-title">💡 提示</div>
|
||
<div class="hint-text">
|
||
试试用户名 <code>admin</code>,密码 <code>123456</code>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stepper" v-if="flowStep > 0">
|
||
<div class="stepper-title">
|
||
当前步骤:{{ flowStep }} / {{ maxStep }}
|
||
<span class="stepper-hint"
|
||
>(手动推进,避免“自动下一步”误解)</span
|
||
>
|
||
</div>
|
||
<div class="stepper-actions">
|
||
<button
|
||
class="step-btn"
|
||
@click="prevStep"
|
||
:disabled="flowStep <= 1"
|
||
>
|
||
上一步
|
||
</button>
|
||
<button
|
||
class="step-btn primary"
|
||
@click="nextStep"
|
||
:disabled="flowStep >= maxStep"
|
||
>
|
||
下一步
|
||
</button>
|
||
<button class="step-btn" @click="resetDemo">重置</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 实时数据流 -->
|
||
<div class="data-flow">
|
||
<div class="flow-title">📊 数据流可视化</div>
|
||
|
||
<!-- 请求阶段 -->
|
||
<div class="flow-stage request-stage" v-if="currentStage >= 1">
|
||
<div class="stage-header">
|
||
<span class="stage-badge">{{
|
||
currentStage === 1 ? '📤' : '✅'
|
||
}}</span>
|
||
<span class="stage-name">1. 客户端发送登录请求</span>
|
||
</div>
|
||
<div class="request-content" v-if="currentStage >= 1">
|
||
<div class="request-line">
|
||
<span class="method">POST</span>
|
||
<span class="path">/api/login</span>
|
||
</div>
|
||
<div class="request-body">
|
||
<div class="body-title">Body:</div>
|
||
<pre>
|
||
{
|
||
"username": "{{ username }}",
|
||
"password": "******"
|
||
}</pre
|
||
>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flow-arrow" v-if="currentStage >= 1">⬇️</div>
|
||
|
||
<!-- 服务器验证阶段 -->
|
||
<div class="flow-stage server-stage" v-if="currentStage >= 2">
|
||
<div class="stage-header">
|
||
<span class="stage-badge">{{
|
||
currentStage === 2 ? '⚙️' : '✅'
|
||
}}</span>
|
||
<span class="stage-name">2. 服务器验证身份</span>
|
||
</div>
|
||
<div class="server-content" v-if="currentStage >= 2">
|
||
<div class="verification-steps">
|
||
<div
|
||
class="verify-step"
|
||
:class="{ success: verificationStep >= 1 }"
|
||
>
|
||
<span class="step-icon">{{
|
||
verificationStep >= 1 ? '✅' : '⏳'
|
||
}}</span>
|
||
<span class="step-text">查询用户数据库</span>
|
||
</div>
|
||
<div
|
||
class="verify-step"
|
||
:class="{ success: verificationStep >= 2 }"
|
||
>
|
||
<span class="step-icon">{{
|
||
verificationStep >= 2 ? '✅' : '⏳'
|
||
}}</span>
|
||
<span class="step-text">验证密码哈希</span>
|
||
</div>
|
||
<div
|
||
class="verify-step"
|
||
:class="{ success: verificationStep >= 3 }"
|
||
>
|
||
<span class="step-icon">{{
|
||
verificationStep >= 3 ? '✅' : '⏳'
|
||
}}</span>
|
||
<span class="step-text"
|
||
>生成{{
|
||
mode === 'session' ? 'Session' : 'JWT Token'
|
||
}}</span
|
||
>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flow-arrow" v-if="currentStage >= 2">⬇️</div>
|
||
|
||
<!-- 响应阶段 -->
|
||
<div class="flow-stage response-stage" v-if="currentStage >= 3">
|
||
<div class="stage-header">
|
||
<span class="stage-badge">{{
|
||
currentStage === 3 ? '📥' : '✅'
|
||
}}</span>
|
||
<span class="stage-name">3. 服务器返回认证结果</span>
|
||
</div>
|
||
<div class="response-content" v-if="authResult">
|
||
<div class="response-status success">✅ 登录成功</div>
|
||
<div class="response-body">
|
||
<div class="body-title">Response:</div>
|
||
<pre v-if="mode === 'session'">
|
||
{
|
||
"status": "success",
|
||
"user": {
|
||
"id": 123,
|
||
"username": "{{ username }}"
|
||
}
|
||
}</pre
|
||
>
|
||
<pre v-else>
|
||
{
|
||
"status": "success",
|
||
"token": "{{ authResult?.token }}",
|
||
"user": {
|
||
"id": 123,
|
||
"username": "{{ username }}"
|
||
}
|
||
}</pre
|
||
>
|
||
</div>
|
||
<div class="auth-mechanism" v-if="currentStage >= 4">
|
||
<div class="mechanism-title">
|
||
{{ mode === 'session' ? '🍪 Cookie 设置' : '🎫 Token 存储' }}
|
||
</div>
|
||
<div class="mechanism-content">
|
||
<div v-if="mode === 'session'">
|
||
<code
|
||
>Set-Cookie: session_id={{ authResult?.sessionId }};
|
||
HttpOnly; Secure</code
|
||
>
|
||
</div>
|
||
<div v-else>
|
||
<code
|
||
>localStorage.setItem("token", "{{
|
||
authResult?.token
|
||
}}")</code
|
||
>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 后续请求 -->
|
||
<div class="flow-stage request-stage" v-if="currentStage >= 5">
|
||
<div class="stage-header">
|
||
<span class="stage-badge">🔄</span>
|
||
<span class="stage-name">4. 后续请求自动携带认证信息</span>
|
||
</div>
|
||
<div class="subsequent-request">
|
||
<div class="request-line">
|
||
<span class="method">GET</span>
|
||
<span class="path">/api/user/profile</span>
|
||
</div>
|
||
<div class="auth-header">
|
||
<div class="header-title">Header:</div>
|
||
<div v-if="mode === 'session'">
|
||
<code>Cookie: session_id={{ authResult?.sessionId }}</code>
|
||
</div>
|
||
<div v-else>
|
||
<code>Authorization: Bearer {{ authResult?.token }}</code>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 状态说明 -->
|
||
<div class="state-description" v-if="currentStage >= 4">
|
||
<div class="description-title">
|
||
📖 {{ mode === 'session' ? 'Session' : 'JWT' }} 工作原理
|
||
</div>
|
||
<div class="description-content">
|
||
<p v-if="mode === 'session'">
|
||
<strong>Session 模式:</strong>服务器在内存或 Redis 中创建一个
|
||
Session,存储用户信息。 服务器返回一个
|
||
<code>session_id</code> 给客户端,客户端后续请求会自动在 Cookie
|
||
中携带这个 ID。 服务器根据 ID 查找对应的 Session,从而识别用户身份。
|
||
</p>
|
||
<p v-else>
|
||
<strong>JWT 模式:</strong>服务器将用户信息编码成 JWT
|
||
Token,直接返回给客户端。 客户端将 Token 存储在
|
||
localStorage,后续请求在 <code>Authorization</code> Header 中携带。
|
||
服务器验证 Token 的签名即可识别用户,无需存储状态。
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 重置按钮 -->
|
||
<div class="reset-section" v-if="currentStage >= 5">
|
||
<button class="reset-btn" @click="resetDemo">🔄 重新演示</button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed, ref } from 'vue'
|
||
|
||
const mode = ref('session') // 'session' or 'jwt'
|
||
const username = ref('')
|
||
const password = ref('')
|
||
const flowStep = ref(0) // 0 = not started, 1..maxStep = manual steps
|
||
const authResult = ref(null)
|
||
const locked = ref(false)
|
||
|
||
const maxStep = 7
|
||
|
||
const currentStage = computed(() => {
|
||
if (flowStep.value === 0) return 0
|
||
if (flowStep.value === 1) return 1
|
||
if (flowStep.value >= 2 && flowStep.value <= 4) return 2
|
||
if (flowStep.value === 5) return 3
|
||
if (flowStep.value === 6) return 4
|
||
return 5
|
||
})
|
||
|
||
const verificationStep = computed(() => {
|
||
if (flowStep.value < 2) return 0
|
||
if (flowStep.value === 2) return 1
|
||
if (flowStep.value === 3) return 2
|
||
return 3
|
||
})
|
||
|
||
const switchMode = (newMode) => {
|
||
mode.value = newMode
|
||
resetDemo()
|
||
}
|
||
|
||
const buildAuthResult = () => {
|
||
if (mode.value === 'session') {
|
||
return {
|
||
sessionId: 'sess_' + Math.random().toString(36).substring(2, 15)
|
||
}
|
||
}
|
||
|
||
const header = btoa(JSON.stringify({ alg: 'HS256', typ: 'JWT' }))
|
||
const payload = btoa(
|
||
JSON.stringify({
|
||
user_id: 123,
|
||
username: username.value,
|
||
exp: Math.floor(Date.now() / 1000) + 3600
|
||
})
|
||
)
|
||
const signature = btoa(`${header}.${payload}.secret`)
|
||
return {
|
||
token: `${header}.${payload}.${signature}`.substring(0, 50) + '...'
|
||
}
|
||
}
|
||
|
||
const startDemo = () => {
|
||
if (!username.value || !password.value) return
|
||
|
||
// Start at step 1 (request). Other steps are manual via Next.
|
||
authResult.value = buildAuthResult()
|
||
flowStep.value = 1
|
||
locked.value = true
|
||
}
|
||
|
||
const nextStep = () => {
|
||
flowStep.value = Math.min(maxStep, flowStep.value + 1)
|
||
}
|
||
|
||
const prevStep = () => {
|
||
flowStep.value = Math.max(1, flowStep.value - 1)
|
||
}
|
||
|
||
const resetDemo = () => {
|
||
username.value = ''
|
||
password.value = ''
|
||
flowStep.value = 0
|
||
authResult.value = null
|
||
locked.value = false
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.auth-interactive-login {
|
||
border: 1px solid var(--vp-c-divider);
|
||
background: var(--vp-c-bg-soft);
|
||
border-radius: 8px;
|
||
padding: 1.5rem;
|
||
margin: 1rem 0;
|
||
}
|
||
|
||
.header {
|
||
margin-bottom: 1.5rem;
|
||
text-align: center;
|
||
}
|
||
|
||
.title {
|
||
font-weight: 700;
|
||
font-size: 1.2rem;
|
||
margin-bottom: 0.3rem;
|
||
color: var(--vp-c-text-1);
|
||
}
|
||
|
||
.subtitle {
|
||
color: var(--vp-c-text-2);
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
/* 模式切换 */
|
||
.mode-selector {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 1rem;
|
||
margin-bottom: 1.5rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.mode-label {
|
||
font-weight: 600;
|
||
font-size: 0.95rem;
|
||
}
|
||
|
||
.mode-buttons {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.mode-btn {
|
||
padding: 0.6rem 1.2rem;
|
||
border: 2px solid var(--vp-c-divider);
|
||
border-radius: 8px;
|
||
background: var(--vp-c-bg);
|
||
cursor: pointer;
|
||
font-size: 0.9rem;
|
||
font-weight: 600;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.mode-btn:hover {
|
||
border-color: var(--vp-c-brand);
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.mode-btn.active {
|
||
background: var(--vp-c-brand);
|
||
color: var(--vp-c-bg);
|
||
border-color: var(--vp-c-brand);
|
||
}
|
||
|
||
/* 主内容 */
|
||
.main-content {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1.5fr;
|
||
gap: 1.5rem;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
/* 登录表单 */
|
||
.login-section {
|
||
display: contents;
|
||
}
|
||
|
||
.form-container {
|
||
background: var(--vp-c-bg);
|
||
border-radius: 10px;
|
||
padding: 1.5rem;
|
||
border: 1px solid var(--vp-c-divider);
|
||
}
|
||
|
||
.stepper {
|
||
margin-top: 1rem;
|
||
padding-top: 1rem;
|
||
border-top: 1px solid var(--vp-c-divider);
|
||
}
|
||
|
||
.stepper-title {
|
||
font-weight: 700;
|
||
color: var(--vp-c-text-1);
|
||
font-size: 0.9rem;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.stepper-hint {
|
||
margin-left: 0.5rem;
|
||
font-weight: 500;
|
||
color: var(--vp-c-text-2);
|
||
}
|
||
|
||
.stepper-actions {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.step-btn {
|
||
padding: 0.5rem 0.8rem;
|
||
border-radius: 6px;
|
||
border: 1px solid var(--vp-c-divider);
|
||
background: var(--vp-c-bg);
|
||
color: var(--vp-c-text-1);
|
||
cursor: pointer;
|
||
font-weight: 600;
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
.step-btn.primary {
|
||
background: var(--vp-c-brand);
|
||
border-color: var(--vp-c-brand);
|
||
color: var(--vp-c-bg);
|
||
}
|
||
|
||
.step-btn:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.form-title {
|
||
font-weight: 700;
|
||
font-size: 1rem;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.form-fields {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.field-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.4rem;
|
||
}
|
||
|
||
.field-group label {
|
||
font-weight: 600;
|
||
font-size: 0.85rem;
|
||
color: var(--vp-c-text-2);
|
||
}
|
||
|
||
.field-group input {
|
||
padding: 0.6rem;
|
||
border: 1px solid var(--vp-c-divider);
|
||
border-radius: 6px;
|
||
font-size: 0.9rem;
|
||
transition: border-color 0.2s;
|
||
}
|
||
|
||
.field-group input:focus {
|
||
outline: none;
|
||
border-color: var(--vp-c-brand);
|
||
}
|
||
|
||
.field-group input:disabled {
|
||
opacity: 0.6;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.login-btn {
|
||
padding: 0.75rem;
|
||
border: none;
|
||
border-radius: 6px;
|
||
background: var(--vp-c-brand);
|
||
color: white;
|
||
font-weight: 600;
|
||
font-size: 0.95rem;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.login-btn:hover:not(:disabled) {
|
||
background: #2563eb;
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.login-btn:disabled {
|
||
opacity: 0.6;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.hints {
|
||
margin-top: 1rem;
|
||
padding: 0.75rem;
|
||
background: rgba(59, 130, 246, 0.1);
|
||
border-left: 3px solid var(--vp-c-brand);
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.hint-title {
|
||
font-weight: 600;
|
||
font-size: 0.85rem;
|
||
margin-bottom: 0.3rem;
|
||
color: var(--vp-c-brand);
|
||
}
|
||
|
||
.hint-text {
|
||
font-size: 0.8rem;
|
||
color: var(--vp-c-text-2);
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.hint-text code {
|
||
background: var(--vp-c-bg);
|
||
padding: 0.1rem 0.3rem;
|
||
border-radius: 3px;
|
||
font-family: 'Courier New', monospace;
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
/* 数据流 */
|
||
.data-flow {
|
||
background: var(--vp-c-bg);
|
||
border-radius: 10px;
|
||
padding: 1.5rem;
|
||
border: 1px solid var(--vp-c-divider);
|
||
}
|
||
|
||
.flow-title {
|
||
font-weight: 700;
|
||
font-size: 1rem;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.flow-stage {
|
||
background: var(--vp-c-bg-soft);
|
||
border-radius: 8px;
|
||
padding: 1rem;
|
||
border: 1px solid var(--vp-c-divider);
|
||
animation: slideIn 0.4s ease;
|
||
}
|
||
|
||
@keyframes slideIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(10px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
.stage-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
margin-bottom: 0.75rem;
|
||
}
|
||
|
||
.stage-badge {
|
||
font-size: 1.2rem;
|
||
}
|
||
|
||
.stage-name {
|
||
font-weight: 600;
|
||
font-size: 0.95rem;
|
||
}
|
||
|
||
.request-line {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.method {
|
||
font-weight: 700;
|
||
color: #16a34a;
|
||
font-family: 'Courier New', monospace;
|
||
}
|
||
|
||
.path {
|
||
font-family: 'Courier New', monospace;
|
||
color: var(--vp-c-text-1);
|
||
}
|
||
|
||
.request-body,
|
||
.response-body {
|
||
background: #1e293b;
|
||
border-radius: 6px;
|
||
padding: 0.75rem;
|
||
margin-top: 0.5rem;
|
||
}
|
||
|
||
.body-title,
|
||
.header-title {
|
||
font-size: 0.75rem;
|
||
font-weight: 600;
|
||
color: #94a3b8;
|
||
margin-bottom: 0.4rem;
|
||
}
|
||
|
||
.request-body pre,
|
||
.response-body pre {
|
||
margin: 0;
|
||
font-family: 'Courier New', monospace;
|
||
font-size: 0.75rem;
|
||
color: #e2e8f0;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
/* 验证步骤 */
|
||
.verification-steps {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.verify-step {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
padding: 0.5rem;
|
||
background: white;
|
||
border-radius: 6px;
|
||
font-size: 0.85rem;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.verify-step.success {
|
||
background: rgba(34, 197, 94, 0.1);
|
||
}
|
||
|
||
.step-icon {
|
||
font-size: 1rem;
|
||
}
|
||
|
||
.step-text {
|
||
flex: 1;
|
||
}
|
||
|
||
/* 响应 */
|
||
.response-status {
|
||
padding: 0.5rem 0.75rem;
|
||
border-radius: 6px;
|
||
font-weight: 600;
|
||
font-size: 0.9rem;
|
||
margin-bottom: 0.75rem;
|
||
}
|
||
|
||
.response-status.success {
|
||
background: rgba(34, 197, 94, 0.1);
|
||
color: #16a34a;
|
||
}
|
||
|
||
/* 认证机制 */
|
||
.auth-mechanism {
|
||
margin-top: 1rem;
|
||
padding: 0.75rem;
|
||
background: rgba(59, 130, 246, 0.1);
|
||
border-left: 3px solid var(--vp-c-brand);
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.mechanism-title {
|
||
font-weight: 600;
|
||
font-size: 0.85rem;
|
||
margin-bottom: 0.5rem;
|
||
color: var(--vp-c-brand);
|
||
}
|
||
|
||
.mechanism-content code {
|
||
display: block;
|
||
background: #1e293b;
|
||
color: #e2e8f0;
|
||
padding: 0.5rem;
|
||
border-radius: 4px;
|
||
font-family: 'Courier New', monospace;
|
||
font-size: 0.75rem;
|
||
word-break: break-all;
|
||
}
|
||
|
||
/* 后续请求 */
|
||
.subsequent-request {
|
||
margin-top: 0.5rem;
|
||
}
|
||
|
||
.auth-header {
|
||
margin-top: 0.5rem;
|
||
}
|
||
|
||
.auth-header code {
|
||
display: block;
|
||
background: #1e293b;
|
||
color: #e2e8f0;
|
||
padding: 0.5rem;
|
||
border-radius: 4px;
|
||
font-family: 'Courier New', monospace;
|
||
font-size: 0.75rem;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.flow-arrow {
|
||
text-align: center;
|
||
font-size: 1.5rem;
|
||
color: var(--vp-c-text-2);
|
||
margin: 0.5rem 0;
|
||
}
|
||
|
||
/* 状态说明 */
|
||
.state-description {
|
||
background: var(--vp-c-bg);
|
||
border-radius: 10px;
|
||
padding: 1.5rem;
|
||
border: 1px solid var(--vp-c-divider);
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.description-title {
|
||
font-weight: 700;
|
||
font-size: 1rem;
|
||
margin-bottom: 0.75rem;
|
||
}
|
||
|
||
.description-content {
|
||
font-size: 0.9rem;
|
||
color: var(--vp-c-text-2);
|
||
line-height: 1.7;
|
||
}
|
||
|
||
.description-content code {
|
||
background: var(--vp-c-bg-soft);
|
||
padding: 0.1rem 0.3rem;
|
||
border-radius: 3px;
|
||
font-family: 'Courier New', monospace;
|
||
font-size: 0.85rem;
|
||
color: var(--vp-c-brand);
|
||
}
|
||
|
||
/* 重置 */
|
||
.reset-section {
|
||
text-align: center;
|
||
}
|
||
|
||
.reset-btn {
|
||
padding: 0.75rem 2rem;
|
||
border: none;
|
||
border-radius: 8px;
|
||
background: #64748b;
|
||
color: white;
|
||
font-weight: 600;
|
||
font-size: 0.95rem;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.reset-btn:hover {
|
||
background: #475569;
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.main-content {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.mode-selector {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.mode-buttons {
|
||
width: 100%;
|
||
}
|
||
|
||
.mode-btn {
|
||
flex: 1;
|
||
}
|
||
}
|
||
</style>
|