feat: 更新附录交互组件和文档
This commit is contained in:
@@ -1,654 +1,57 @@
|
||||
<!--
|
||||
UrlParserDemo.vue
|
||||
URL解析演示 - 网购订单隐喻版
|
||||
|
||||
设计理念:
|
||||
1. 隐喻对齐:严格对应 url-to-browser.md 中的"网购订单"比喻。
|
||||
2. 视觉映射:将枯燥的 URL 字符串映射为一张清晰的"购物清单"。
|
||||
3. 实时交互:输入即解析,所见即所得。
|
||||
-->
|
||||
<template>
|
||||
<div class="url-parser-order">
|
||||
<!-- 顶部:输入区 -->
|
||||
<div class="input-section">
|
||||
<div
|
||||
class="url-input-box"
|
||||
:class="{ 'has-error': error }"
|
||||
>
|
||||
<span class="input-label">URL</span>
|
||||
<input
|
||||
v-model="urlInput"
|
||||
type="text"
|
||||
placeholder="https://www.example.com/path?query=1"
|
||||
class="real-input"
|
||||
@input="parseUrl"
|
||||
>
|
||||
<button
|
||||
v-if="urlInput"
|
||||
class="clear-btn"
|
||||
@click="clear"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div class="quick-actions">
|
||||
<span class="action-label">试一试:</span>
|
||||
<button
|
||||
v-for="ex in examples"
|
||||
:key="ex.name"
|
||||
class="action-chip"
|
||||
:class="{ active: currentExample === ex.name }"
|
||||
@click="useExample(ex)"
|
||||
>
|
||||
{{ ex.name }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="url-parser-demo">
|
||||
<div class="url-line">
|
||||
<span class="part protocol">https://</span><span class="part host">www.google.com</span><span class="part path">/search</span><span class="part query">?q=hello</span>
|
||||
</div>
|
||||
|
||||
<!-- 核心区域:左右对照布局 -->
|
||||
<template v-if="parsed.protocol">
|
||||
<div class="core-stage">
|
||||
<!-- 左侧:解析结果 (技术视角) -->
|
||||
<div class="tech-view">
|
||||
<div class="view-header">
|
||||
<span class="icon">💻</span>
|
||||
<span class="title">技术解析</span>
|
||||
</div>
|
||||
<div class="code-blocks">
|
||||
<div
|
||||
v-for="(field, key) in formFields"
|
||||
v-show="shouldShowField(key)"
|
||||
:key="key"
|
||||
class="code-block"
|
||||
:class="[key, { active: hovered === key }]"
|
||||
:style="{ '--color': field.color }"
|
||||
@mouseenter="hovered = key"
|
||||
@mouseleave="hovered = null"
|
||||
>
|
||||
<span class="field-name">{{ key }}</span>
|
||||
<span class="field-value">{{ getDisplayValue(key) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 中间:转换箭头 -->
|
||||
<div class="transform-arrow">
|
||||
<span class="arrow-icon">➔</span>
|
||||
<span class="arrow-text">映射为</span>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:购物单 (生活视角) -->
|
||||
<div class="life-view">
|
||||
<div class="view-header">
|
||||
<span class="icon">🧾</span>
|
||||
<span class="title">购物订单</span>
|
||||
</div>
|
||||
<div class="order-ticket">
|
||||
<div class="ticket-hole" />
|
||||
|
||||
<div
|
||||
v-for="(field, key) in formFields"
|
||||
v-show="shouldShowField(key)"
|
||||
:key="key"
|
||||
class="ticket-row"
|
||||
:class="{ active: hovered === key }"
|
||||
:style="{ '--color': field.color }"
|
||||
@mouseenter="hovered = key"
|
||||
@mouseleave="hovered = null"
|
||||
>
|
||||
<div
|
||||
class="ticket-icon"
|
||||
:style="{ backgroundColor: field.color }"
|
||||
>
|
||||
{{ field.icon }}
|
||||
</div>
|
||||
<div class="ticket-content">
|
||||
<span class="ticket-label">{{ field.analogyLabel }}</span>
|
||||
<span class="ticket-desc">{{ field.analogyDesc }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 技术答疑面板 -->
|
||||
<transition name="fade">
|
||||
<div
|
||||
v-if="activeQa"
|
||||
class="qa-panel"
|
||||
>
|
||||
<div class="qa-header">
|
||||
{{ activeQa.title }}
|
||||
</div>
|
||||
<div class="qa-content">
|
||||
<div
|
||||
v-for="(item, idx) in activeQa.content"
|
||||
:key="idx"
|
||||
class="qa-item"
|
||||
>
|
||||
<div class="qa-q">
|
||||
Q: {{ item.q }}
|
||||
</div>
|
||||
<div class="qa-a">
|
||||
A: {{ item.a }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="qa-placeholder"
|
||||
>
|
||||
👆 鼠标悬停在上方色块,查看详细技术解释
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<!-- 空状态引导 -->
|
||||
<div
|
||||
v-else
|
||||
class="empty-state"
|
||||
>
|
||||
<div class="empty-icon">
|
||||
🛒
|
||||
</div>
|
||||
<div class="empty-text">
|
||||
<p>输入网址,生成你的"数字购物单"</p>
|
||||
<span class="sub-text">看看浏览器如何理解这一长串字符</span>
|
||||
</div>
|
||||
<div class="labels">
|
||||
<span class="label protocol">协议</span>
|
||||
<span class="label host">域名</span>
|
||||
<span class="label path">路径</span>
|
||||
<span class="label query">参数</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const urlInput = ref('')
|
||||
const parsed = ref({})
|
||||
const hovered = ref(null)
|
||||
const currentExample = ref('')
|
||||
const error = ref(false)
|
||||
|
||||
const examples = [
|
||||
{ name: '标准网购', url: 'https://www.nike.com/shoes/running?size=42&color=red' },
|
||||
{ name: '带端口', url: 'http://localhost:8080/api/status' },
|
||||
{ name: '带锚点', url: 'https://vuejs.org/guide.html#setup' }
|
||||
]
|
||||
|
||||
// 定义字段映射(严格对齐 url-to-browser.md)
|
||||
const formFields = {
|
||||
protocol: {
|
||||
color: '#f43f5e', // Red
|
||||
icon: '🚚',
|
||||
analogyLabel: '物流方式',
|
||||
analogyDesc: '决定怎么送货(HTTP普通/HTTPS加密保密)。',
|
||||
qa: {
|
||||
title: '🤔 为什么要写 http/https?',
|
||||
content: [
|
||||
{
|
||||
q: '这两者有什么区别?',
|
||||
a: 'HTTP 就像寄明信片,邮递员(黑客)能看到内容。HTTPS 就像寄密封的信封,只有收件人能拆开看。'
|
||||
},
|
||||
{
|
||||
q: '为什么现在都是 https?',
|
||||
a: '为了安全!现在的浏览器如果发现不是 HTTPS,会提示"不安全",就像快递公司拒收没封口的信件一样。'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
hostname: {
|
||||
color: '#3b82f6', // Blue
|
||||
icon: '🏠',
|
||||
analogyLabel: '店铺名称',
|
||||
analogyDesc: '告诉浏览器去哪家店(服务器)买东西。',
|
||||
qa: {
|
||||
title: '🤔 域名还是 IP?',
|
||||
content: [
|
||||
{
|
||||
q: '浏览器认识域名吗?',
|
||||
a: '其实不认识。浏览器只认识 IP 地址(一串数字)。域名是为了方便人记的。下一步(DNS 查询)就是把这个名字翻译成数字。'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
port: {
|
||||
color: '#f59e0b', // Amber
|
||||
icon: '🔢',
|
||||
analogyLabel: '柜台编号',
|
||||
analogyDesc: '店铺很大,指定去几号柜台办理业务。',
|
||||
qa: {
|
||||
title: '🤔 这里的数字是什么意思?',
|
||||
content: [
|
||||
{
|
||||
q: '为什么平时上网看不到它?',
|
||||
a: '因为有默认值!就像去银行默认去"综合柜台"一样。HTTP 默认是 80,HTTPS 默认是 443。只有特殊的才需要写出来。'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
pathname: {
|
||||
color: '#10b981', // Emerald
|
||||
icon: '📦',
|
||||
analogyLabel: '货架位置',
|
||||
analogyDesc: '商品在仓库里的具体存放位置。',
|
||||
qa: {
|
||||
title: '🤔 这一长串是干嘛的?',
|
||||
content: [
|
||||
{
|
||||
q: '它对应服务器上的文件吗?',
|
||||
a: '通常是的。/shoes/running 就像告诉仓库管理员:去"鞋子区"的"跑步架"上拿货。'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
search: {
|
||||
color: '#8b5cf6', // Violet
|
||||
icon: '📝',
|
||||
analogyLabel: '订单备注',
|
||||
analogyDesc: '给商家的额外要求(如:红色、42码)。',
|
||||
qa: {
|
||||
title: '🤔 问号后面的内容?',
|
||||
content: [
|
||||
{
|
||||
q: '这对网页有什么影响?',
|
||||
a: '就像你点外卖备注"不要香菜"。网页内容会根据这些参数变化,比如只显示红色的鞋子。'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
hash: {
|
||||
color: '#ec4899', // Pink
|
||||
icon: '🔖',
|
||||
analogyLabel: '说明书页码',
|
||||
analogyDesc: '拿到商品后,直接翻到说明书的第几页。',
|
||||
qa: {
|
||||
title: '🤔 为什么要用 # 号?',
|
||||
content: [
|
||||
{
|
||||
q: '这部分会发给服务器吗?',
|
||||
a: '不会。这只是给你自己(浏览器)看的。就像你买书回家后翻到第10页,书店老板并不需要知道你看哪一页。'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const activeField = computed(() => hovered.value || null)
|
||||
|
||||
const activeQa = computed(() => {
|
||||
if (!activeField.value) return null
|
||||
return formFields[activeField.value].qa
|
||||
})
|
||||
|
||||
const shouldShowField = (key) => {
|
||||
const val = parsed.value[key]
|
||||
if (!val) return false
|
||||
if (key === 'search' && (val === '' || val === '?')) return false
|
||||
if (key === 'hash' && (val === '' || val === '#')) return false
|
||||
return true
|
||||
}
|
||||
|
||||
const getDisplayValue = (key) => {
|
||||
let val = parsed.value[key]
|
||||
if (key === 'protocol') return val + '://'
|
||||
if (key === 'port') return ':' + val
|
||||
return val
|
||||
}
|
||||
|
||||
const useExample = (ex) => {
|
||||
urlInput.value = ex.url
|
||||
currentExample.value = ex.name
|
||||
parseUrl()
|
||||
}
|
||||
|
||||
const clear = () => {
|
||||
urlInput.value = ''
|
||||
parsed.value = {}
|
||||
currentExample.value = ''
|
||||
hovered.value = null
|
||||
error.value = false
|
||||
}
|
||||
|
||||
const parseUrl = () => {
|
||||
if (!urlInput.value) {
|
||||
parsed.value = {}
|
||||
return
|
||||
}
|
||||
try {
|
||||
let urlStr = urlInput.value.trim()
|
||||
// 自动补全协议以支持 new URL() 解析
|
||||
if (!urlStr.match(/^https?:\/\//)) {
|
||||
urlStr = (urlStr.startsWith('localhost') ? 'http://' : 'https://') + urlStr
|
||||
}
|
||||
const u = new URL(urlStr)
|
||||
|
||||
parsed.value = {
|
||||
protocol: u.protocol.replace(':', ''),
|
||||
hostname: u.hostname,
|
||||
port: u.port || (u.protocol === 'https:' ? '443 (默认)' : '80 (默认)'),
|
||||
pathname: u.pathname,
|
||||
search: u.search,
|
||||
hash: u.hash
|
||||
}
|
||||
error.value = false
|
||||
} catch (e) {
|
||||
// 解析失败时不更新 parsed,或者显示错误状态
|
||||
// 这里选择静默失败,等待用户输入完整
|
||||
if (urlInput.value.length > 10) {
|
||||
// error.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.url-parser-order {
|
||||
.url-parser-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background-color: var(--vp-c-bg);
|
||||
padding: 20px;
|
||||
margin: 16px 0;
|
||||
font-family: var(--vp-font-family-base);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
/* 输入区 */
|
||||
.input-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.url-input-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border: 2px solid transparent;
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.url-input-box:focus-within {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-bg);
|
||||
}
|
||||
.input-label {
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-right: 12px;
|
||||
user-select: none;
|
||||
}
|
||||
.real-input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
font-family: var(--vp-font-family-mono);
|
||||
font-size: 14px;
|
||||
color: var(--vp-c-text-1);
|
||||
outline: none;
|
||||
min-width: 0;
|
||||
}
|
||||
.clear-btn {
|
||||
color: var(--vp-c-text-3);
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
}
|
||||
.clear-btn:hover { color: var(--vp-c-text-1); }
|
||||
|
||||
.quick-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.action-label {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
padding-top: 4px;
|
||||
}
|
||||
.action-chip {
|
||||
padding: 4px 12px;
|
||||
border-radius: 100px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-1);
|
||||
transition: all 0.2s;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
.action-chip:hover {
|
||||
background: var(--vp-c-brand-soft);
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
.action-chip.active {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 核心展示区 */
|
||||
.core-stage {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 16px;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
/* 通用标题 */
|
||||
.view-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.view-header .icon { font-size: 16px; }
|
||||
.view-header .title { font-weight: bold; font-size: 14px; color: var(--vp-c-text-1); }
|
||||
|
||||
/* 左侧:技术视图 */
|
||||
.tech-view {
|
||||
flex: 1;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.code-blocks {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.code-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg);
|
||||
border-left: 3px solid var(--color);
|
||||
transition: all 0.2s;
|
||||
cursor: default;
|
||||
}
|
||||
.code-block:hover, .code-block.active {
|
||||
transform: translateX(4px);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
.field-name {
|
||||
font-size: 11px;
|
||||
color: var(--vp-c-text-2);
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.field-value {
|
||||
padding: 0.5rem 0.6rem;
|
||||
margin: 0.5rem 0;
|
||||
font-family: var(--vp-font-family-mono);
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.url-line {
|
||||
font-size: 0.75rem;
|
||||
margin-bottom: 0.3rem;
|
||||
word-break: break-all;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 中间:箭头 */
|
||||
.transform-arrow {
|
||||
.part {
|
||||
padding: 0.05rem 0.15rem;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.part.protocol { background: #fee2e2; color: #dc2626; }
|
||||
.part.host { background: #dbeafe; color: #2563eb; }
|
||||
.part.path { background: #d1fae5; color: #059669; }
|
||||
.part.query { background: #ede9fe; color: #7c3aed; }
|
||||
|
||||
.labels {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--vp-c-text-3);
|
||||
font-size: 12px;
|
||||
width: 40px;
|
||||
}
|
||||
.arrow-icon { font-size: 20px; }
|
||||
|
||||
/* 右侧:生活视图 (票据样式) */
|
||||
.life-view {
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
position: relative;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
/* 暗黑模式适配票据背景 */
|
||||
:root.dark .life-view {
|
||||
background: #1e1e20;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.order-ticket {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border: 1px dashed var(--vp-c-divider);
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
position: relative;
|
||||
}
|
||||
.ticket-hole {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
box-shadow: inset 0 1px 3px rgba(0,0,0,0.1);
|
||||
}
|
||||
.ticket-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.ticket-row:hover, .ticket-row.active {
|
||||
background: var(--vp-c-bg-soft);
|
||||
opacity: 1;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
.ticket-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ticket-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.ticket-label {
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
.ticket-desc {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.4;
|
||||
.label {
|
||||
font-size: 0.6rem;
|
||||
padding: 0.05rem 0.25rem;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
color: var(--vp-c-text-3);
|
||||
text-align: center;
|
||||
}
|
||||
.empty-icon { font-size: 48px; margin-bottom: 16px; opacity: 0.5; }
|
||||
.empty-text p { font-size: 16px; font-weight: bold; margin: 0 0 8px 0; color: var(--vp-c-text-2); }
|
||||
.sub-text { font-size: 13px; }
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 640px) {
|
||||
.core-stage {
|
||||
flex-direction: column;
|
||||
}
|
||||
.transform-arrow {
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
gap: 8px;
|
||||
}
|
||||
.arrow-icon { transform: rotate(90deg); }
|
||||
}
|
||||
|
||||
/* QA Panel */
|
||||
.qa-panel {
|
||||
margin-top: 16px;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.qa-header {
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-brand);
|
||||
margin-bottom: 8px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px dashed var(--vp-c-divider);
|
||||
}
|
||||
.qa-item {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.qa-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.qa-q {
|
||||
color: var(--vp-c-text-1);
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.qa-a {
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.5;
|
||||
}
|
||||
.qa-placeholder {
|
||||
margin-top: 16px;
|
||||
text-align: center;
|
||||
color: var(--vp-c-text-3);
|
||||
font-size: 13px;
|
||||
padding: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
border: 1px dashed var(--vp-c-divider);
|
||||
}
|
||||
|
||||
/* Transitions */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
.label.protocol { background: #fee2e2; color: #dc2626; }
|
||||
.label.host { background: #dbeafe; color: #2563eb; }
|
||||
.label.path { background: #d1fae5; color: #059669; }
|
||||
.label.query { background: #ede9fe; color: #7c3aed; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user