调整,音乐模块
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import ResultArea from '../components/ResultArea.vue'
|
||||
|
||||
const token = ref('')
|
||||
const error = ref('')
|
||||
|
||||
function base64UrlDecode(s: string): string {
|
||||
const cleaned = s.replace(/-/g, '+').replace(/_/g, '/')
|
||||
const pad = cleaned.length % 4
|
||||
const normalized = pad ? cleaned + '='.repeat(4 - pad) : cleaned
|
||||
const bin = atob(normalized)
|
||||
const bytes = Uint8Array.from(bin, ch => ch.charCodeAt(0))
|
||||
return new TextDecoder().decode(bytes)
|
||||
}
|
||||
|
||||
interface TimeClaim {
|
||||
name: string
|
||||
value: string
|
||||
local: string
|
||||
status: 'ok' | 'expired' | 'not-yet' | 'unknown'
|
||||
}
|
||||
|
||||
const parsed = computed<{
|
||||
header: string
|
||||
payload: string
|
||||
signature: string
|
||||
timeClaims: TimeClaim[]
|
||||
} | null>(() => {
|
||||
error.value = ''
|
||||
const t = token.value.trim()
|
||||
if (!t) return null
|
||||
const parts = t.split('.')
|
||||
if (parts.length < 2) {
|
||||
error.value = 'JWT 格式不正确(应为 header.payload.signature)'
|
||||
return null
|
||||
}
|
||||
try {
|
||||
const header = formatJson(base64UrlDecode(parts[0]))
|
||||
const payload = formatJson(base64UrlDecode(parts[1]))
|
||||
const signature = parts[2] ?? ''
|
||||
return { header, payload, signature, timeClaims: parseTimeClaims(payload) }
|
||||
} catch (e) {
|
||||
error.value = '解码失败:' + String(e)
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
function formatJson(s: string): string {
|
||||
let pretty = s
|
||||
try {
|
||||
pretty = JSON.stringify(JSON.parse(s), null, 2)
|
||||
} catch {
|
||||
/* 非 JSON(如已损毁),原样展示 */
|
||||
}
|
||||
return pretty
|
||||
}
|
||||
|
||||
/** 解读 iat / nbf / exp 等时间声明(秒级时间戳) */
|
||||
function parseTimeClaims(payload: string): TimeClaim[] {
|
||||
let obj: unknown
|
||||
try {
|
||||
obj = JSON.parse(payload)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
if (!obj || typeof obj !== 'object') return []
|
||||
const p = obj as Record<string, unknown>
|
||||
const claims: TimeClaim[] = []
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const NAMES: Record<string, string> = { iat: 'iat(签发时间)', nbf: 'nbf(生效时间)', exp: 'exp(过期时间)' }
|
||||
const pad = (x: number) => String(x).padStart(2, '0')
|
||||
for (const key of ['iat', 'nbf', 'exp']) {
|
||||
const v = p[key]
|
||||
if (typeof v !== 'number' || !Number.isFinite(v)) continue
|
||||
const d = new Date(v * 1000)
|
||||
if (Number.isNaN(d.getTime())) continue
|
||||
const local = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
||||
let status: TimeClaim['status'] = 'ok'
|
||||
if (key === 'exp' && v < now) status = 'expired'
|
||||
else if (key === 'nbf' && v > now) status = 'not-yet'
|
||||
else if (key === 'iat' && v > now + 60) status = 'unknown'
|
||||
claims.push({ name: NAMES[key], value: String(v), local, status })
|
||||
}
|
||||
return claims
|
||||
}
|
||||
|
||||
const STATUS_BADGE: Record<TimeClaim['status'], { label: string; class: string } | null> = {
|
||||
ok: null,
|
||||
expired: { label: '已过期', class: 'bg-red-500/10 text-red-600 dark:text-red-400 border-red-500/30' },
|
||||
'not-yet': { label: '尚未生效', class: 'bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/30' },
|
||||
unknown: { label: '签发时间在未来(时钟偏差?)', class: 'bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/30' }
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<Label class="text-xs">JWT Token</Label>
|
||||
<Textarea
|
||||
v-model="token"
|
||||
placeholder="eyJhbGciOi...(header.payload.signature)"
|
||||
class="min-h-[70px] font-mono text-xs resize-y break-all"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template v-if="parsed">
|
||||
<ResultArea :text="parsed.header" label="Header" placeholder="Header" />
|
||||
<ResultArea :text="parsed.payload" label="Payload(载荷)" placeholder="Payload" />
|
||||
<ResultArea :text="parsed.signature" label="Signature(签名)" placeholder="Signature" />
|
||||
|
||||
<div v-if="parsed.timeClaims.length > 0" class="flex flex-col gap-1.5">
|
||||
<Label class="text-xs">时间声明解读</Label>
|
||||
<div class="rounded-md border border-border divide-y divide-border text-xs">
|
||||
<div v-for="c in parsed.timeClaims" :key="c.name" class="flex items-center gap-3 px-3 py-2 flex-wrap">
|
||||
<span class="font-mono text-muted-foreground">{{ c.name }}</span>
|
||||
<span class="font-mono">{{ c.local }}</span>
|
||||
<Badge v-if="STATUS_BADGE[c.status]" variant="outline" :class="['py-0 text-[10px] ml-auto', STATUS_BADGE[c.status]!.class]">
|
||||
{{ STATUS_BADGE[c.status]!.label }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<p v-else-if="!token" class="text-xs text-muted-foreground">在左侧粘贴 JWT,下方将自动解析 Header 与 Payload。</p>
|
||||
<p v-else class="text-xs text-destructive">{{ error }}</p>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user