622 lines
12 KiB
Vue
622 lines
12 KiB
Vue
|
|
<!--
|
||
|
|
CacheLifecycleDemo.vue
|
||
|
|
缓存生命周期演示 - 展示缓存条目的写入、命中、过期、淘汰过程
|
||
|
|
-->
|
||
|
|
<template>
|
||
|
|
<div class="cache-lifecycle-demo">
|
||
|
|
<div class="header">
|
||
|
|
<div class="title">缓存生命周期演示</div>
|
||
|
|
<div class="subtitle">观察缓存条目从创建到淘汰的完整过程</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="cache-container">
|
||
|
|
<div class="cache-header">
|
||
|
|
<div class="cache-title">
|
||
|
|
缓存存储 (容量: {{ cacheSize }}/{{ maxCacheSize }})
|
||
|
|
</div>
|
||
|
|
<div class="cache-stats">
|
||
|
|
<span>命中率: {{ hitRate }}%</span>
|
||
|
|
<span>淘汰: {{ evictionCount }}</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="cache-entries">
|
||
|
|
<div
|
||
|
|
v-for="entry in cacheEntries"
|
||
|
|
:key="entry.id"
|
||
|
|
class="cache-entry"
|
||
|
|
:class="{
|
||
|
|
hit: entry.status === 'hit',
|
||
|
|
expiring: entry.status === 'expiring',
|
||
|
|
evicting: entry.status === 'evicting',
|
||
|
|
new: entry.status === 'new'
|
||
|
|
}"
|
||
|
|
>
|
||
|
|
<div class="entry-header">
|
||
|
|
<div class="entry-id">{{ entry.key }}</div>
|
||
|
|
<div class="entry-status">
|
||
|
|
<span v-if="entry.status === 'new'" class="status-badge new"
|
||
|
|
>NEW</span
|
||
|
|
>
|
||
|
|
<span v-if="entry.status === 'hit'" class="status-badge hit"
|
||
|
|
>HIT</span
|
||
|
|
>
|
||
|
|
<span
|
||
|
|
v-if="entry.status === 'expiring'"
|
||
|
|
class="status-badge expiring"
|
||
|
|
>EXPIRING</span
|
||
|
|
>
|
||
|
|
<span
|
||
|
|
v-if="entry.status === 'evicting'"
|
||
|
|
class="status-badge evicting"
|
||
|
|
>EVICTING</span
|
||
|
|
>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="entry-ttl">
|
||
|
|
<div class="ttl-bar">
|
||
|
|
<div
|
||
|
|
class="ttl-fill"
|
||
|
|
:style="{ width: entry.ttlPercent + '%' }"
|
||
|
|
></div>
|
||
|
|
</div>
|
||
|
|
<div class="ttl-text">TTL: {{ entry.ttl }}s</div>
|
||
|
|
</div>
|
||
|
|
<div class="entry-meta">
|
||
|
|
<span>命中: {{ entry.hits }}</span>
|
||
|
|
<span>访问: {{ entry.lastAccess }}s前</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="controls">
|
||
|
|
<div class="control-group">
|
||
|
|
<label>操作</label>
|
||
|
|
<button class="action-btn read" @click="readData">读取数据</button>
|
||
|
|
<button class="action-btn write" @click="writeData">写入新数据</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="control-group">
|
||
|
|
<label>自动模拟</label>
|
||
|
|
<button
|
||
|
|
class="action-btn auto"
|
||
|
|
:class="{ active: autoMode }"
|
||
|
|
@click="toggleAuto"
|
||
|
|
>
|
||
|
|
{{ autoMode ? '停止' : '开始' }}自动模拟
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="timeline">
|
||
|
|
<div class="timeline-title">事件时间线</div>
|
||
|
|
<div class="timeline-events">
|
||
|
|
<div
|
||
|
|
v-for="(event, index) in events"
|
||
|
|
:key="index"
|
||
|
|
class="event"
|
||
|
|
:class="event.type"
|
||
|
|
>
|
||
|
|
<div class="event-time">{{ event.time }}</div>
|
||
|
|
<div class="event-content">
|
||
|
|
<span class="event-icon">{{ event.icon }}</span>
|
||
|
|
<span class="event-text">{{ event.text }}</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="legend">
|
||
|
|
<div class="legend-item">
|
||
|
|
<span class="legend-color new"></span>
|
||
|
|
<span>新写入</span>
|
||
|
|
</div>
|
||
|
|
<div class="legend-item">
|
||
|
|
<span class="legend-color hit"></span>
|
||
|
|
<span>缓存命中</span>
|
||
|
|
</div>
|
||
|
|
<div class="legend-item">
|
||
|
|
<span class="legend-color expiring"></span>
|
||
|
|
<span>即将过期</span>
|
||
|
|
</div>
|
||
|
|
<div class="legend-item">
|
||
|
|
<span class="legend-color evicting"></span>
|
||
|
|
<span>淘汰中</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script setup>
|
||
|
|
import { ref, computed, onUnmounted } from 'vue'
|
||
|
|
|
||
|
|
const maxCacheSize = 6
|
||
|
|
const cacheEntries = ref([])
|
||
|
|
const events = ref([])
|
||
|
|
const autoMode = ref(false)
|
||
|
|
let autoInterval = null
|
||
|
|
let eventCounter = 0
|
||
|
|
|
||
|
|
const cacheSize = computed(() => cacheEntries.value.length)
|
||
|
|
const hitRate = computed(() => {
|
||
|
|
const hitEvents = events.value.filter((e) => e.type === 'hit').length
|
||
|
|
const totalEvents = events.value.filter(
|
||
|
|
(e) => e.type === 'hit' || e.type === 'miss'
|
||
|
|
).length
|
||
|
|
return totalEvents > 0 ? Math.round((hitEvents / totalEvents) * 100) : 0
|
||
|
|
})
|
||
|
|
const evictionCount = computed(
|
||
|
|
() => events.value.filter((e) => e.type === 'eviction').length
|
||
|
|
)
|
||
|
|
|
||
|
|
const addEvent = (type, icon, text) => {
|
||
|
|
const now = new Date()
|
||
|
|
events.value.unshift({
|
||
|
|
time: `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`,
|
||
|
|
type,
|
||
|
|
icon,
|
||
|
|
text
|
||
|
|
})
|
||
|
|
|
||
|
|
if (events.value.length > 10) {
|
||
|
|
events.value.pop()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const writeData = () => {
|
||
|
|
if (cacheEntries.value.length >= maxCacheSize) {
|
||
|
|
// LRU: Remove least recently used
|
||
|
|
const lruIndex = cacheEntries.value.reduce(
|
||
|
|
(minIdx, entry, idx, arr) =>
|
||
|
|
entry.lastAccess > arr[minIdx].lastAccess ? minIdx : idx,
|
||
|
|
0
|
||
|
|
)
|
||
|
|
|
||
|
|
const evicting = cacheEntries.value[lruIndex]
|
||
|
|
evicting.status = 'evicting'
|
||
|
|
addEvent('eviction', '🗑️', `淘汰 ${evicting.key} (LRU)`)
|
||
|
|
|
||
|
|
setTimeout(() => {
|
||
|
|
cacheEntries.value.splice(lruIndex, 1)
|
||
|
|
}, 500)
|
||
|
|
}
|
||
|
|
|
||
|
|
const newId = `key_${++eventCounter}`
|
||
|
|
const newEntry = {
|
||
|
|
key: newId,
|
||
|
|
status: 'new',
|
||
|
|
ttl: 30,
|
||
|
|
ttlPercent: 100,
|
||
|
|
hits: 0,
|
||
|
|
lastAccess: 0
|
||
|
|
}
|
||
|
|
|
||
|
|
cacheEntries.value.push(newEntry)
|
||
|
|
addEvent('write', '✨', `写入 ${newId}`)
|
||
|
|
|
||
|
|
setTimeout(() => {
|
||
|
|
newEntry.status = null
|
||
|
|
}, 500)
|
||
|
|
|
||
|
|
startTTLDecay(newEntry)
|
||
|
|
}
|
||
|
|
|
||
|
|
const readData = () => {
|
||
|
|
if (cacheEntries.value.length === 0) {
|
||
|
|
addEvent('miss', '❌', '缓存为空,未命中')
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
const randomIndex = Math.floor(Math.random() * cacheEntries.value.length)
|
||
|
|
const entry = cacheEntries.value[randomIndex]
|
||
|
|
|
||
|
|
entry.status = 'hit'
|
||
|
|
entry.hits++
|
||
|
|
entry.lastAccess = 0
|
||
|
|
entry.ttl = Math.min(entry.ttl + 5, 30) // Refresh TTL on hit
|
||
|
|
entry.ttlPercent = (entry.ttl / 30) * 100
|
||
|
|
|
||
|
|
addEvent('hit', '✅', `命中 ${entry.key} (第${entry.hits}次)`)
|
||
|
|
|
||
|
|
setTimeout(() => {
|
||
|
|
entry.status = null
|
||
|
|
}, 500)
|
||
|
|
}
|
||
|
|
|
||
|
|
const startTTLDecay = (entry) => {
|
||
|
|
const interval = setInterval(() => {
|
||
|
|
if (!cacheEntries.value.includes(entry)) {
|
||
|
|
clearInterval(interval)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
entry.lastAccess++
|
||
|
|
entry.ttl--
|
||
|
|
entry.ttlPercent = (entry.ttl / 30) * 100
|
||
|
|
|
||
|
|
if (entry.ttl <= 10) {
|
||
|
|
entry.status = 'expiring'
|
||
|
|
}
|
||
|
|
|
||
|
|
if (entry.ttl <= 0) {
|
||
|
|
addEvent('expiration', '⏰', `${entry.key} 过期`)
|
||
|
|
const idx = cacheEntries.value.indexOf(entry)
|
||
|
|
if (idx !== -1) {
|
||
|
|
cacheEntries.value.splice(idx, 1)
|
||
|
|
}
|
||
|
|
clearInterval(interval)
|
||
|
|
}
|
||
|
|
}, 1000)
|
||
|
|
}
|
||
|
|
|
||
|
|
const toggleAuto = () => {
|
||
|
|
autoMode.value = !autoMode.value
|
||
|
|
|
||
|
|
if (autoMode.value) {
|
||
|
|
autoInterval = setInterval(() => {
|
||
|
|
const action = Math.random()
|
||
|
|
if (action < 0.4 || cacheEntries.value.length === 0) {
|
||
|
|
writeData()
|
||
|
|
} else {
|
||
|
|
readData()
|
||
|
|
}
|
||
|
|
}, 1500)
|
||
|
|
} else {
|
||
|
|
if (autoInterval) {
|
||
|
|
clearInterval(autoInterval)
|
||
|
|
autoInterval = null
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
onUnmounted(() => {
|
||
|
|
if (autoInterval) {
|
||
|
|
clearInterval(autoInterval)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<style scoped>
|
||
|
|
.cache-lifecycle-demo {
|
||
|
|
border: 1px solid var(--vp-c-divider);
|
||
|
|
background: var(--vp-c-bg-soft);
|
||
|
|
border-radius: 12px;
|
||
|
|
padding: 1.5rem;
|
||
|
|
margin: 1.5rem 0;
|
||
|
|
font-family: var(--vp-font-family-base);
|
||
|
|
}
|
||
|
|
|
||
|
|
.header {
|
||
|
|
margin-bottom: 1.5rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.title {
|
||
|
|
font-weight: 700;
|
||
|
|
font-size: 1.05rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.subtitle {
|
||
|
|
color: var(--vp-c-text-2);
|
||
|
|
font-size: 0.9rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.cache-container {
|
||
|
|
background: var(--vp-c-bg);
|
||
|
|
border-radius: 10px;
|
||
|
|
padding: 1.25rem;
|
||
|
|
margin-bottom: 1.5rem;
|
||
|
|
border: 1px solid var(--vp-c-divider);
|
||
|
|
}
|
||
|
|
|
||
|
|
.cache-header {
|
||
|
|
display: flex;
|
||
|
|
justify-content: space-between;
|
||
|
|
align-items: center;
|
||
|
|
margin-bottom: 1rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.cache-title {
|
||
|
|
font-weight: 600;
|
||
|
|
font-size: 0.95rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.cache-stats {
|
||
|
|
display: flex;
|
||
|
|
gap: 1rem;
|
||
|
|
font-size: 0.85rem;
|
||
|
|
color: var(--vp-c-text-2);
|
||
|
|
}
|
||
|
|
|
||
|
|
.cache-entries {
|
||
|
|
display: grid;
|
||
|
|
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||
|
|
gap: 1rem;
|
||
|
|
min-height: 150px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.cache-entry {
|
||
|
|
background: var(--vp-c-bg-soft);
|
||
|
|
border: 2px solid var(--vp-c-divider);
|
||
|
|
border-radius: 8px;
|
||
|
|
padding: 0.75rem;
|
||
|
|
transition: all 0.3s;
|
||
|
|
}
|
||
|
|
|
||
|
|
.cache-entry.new {
|
||
|
|
border-color: #22c55e;
|
||
|
|
background: #f0fdf4;
|
||
|
|
animation: slideIn 0.3s;
|
||
|
|
}
|
||
|
|
|
||
|
|
.cache-entry.hit {
|
||
|
|
border-color: #3b82f6;
|
||
|
|
background: #eff6ff;
|
||
|
|
}
|
||
|
|
|
||
|
|
.cache-entry.expiring {
|
||
|
|
border-color: #f59e0b;
|
||
|
|
background: #fef3c7;
|
||
|
|
}
|
||
|
|
|
||
|
|
.cache-entry.evicting {
|
||
|
|
border-color: #ef4444;
|
||
|
|
background: #fef2f2;
|
||
|
|
animation: shake 0.5s;
|
||
|
|
}
|
||
|
|
|
||
|
|
.entry-header {
|
||
|
|
display: flex;
|
||
|
|
justify-content: space-between;
|
||
|
|
align-items: center;
|
||
|
|
margin-bottom: 0.5rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.entry-id {
|
||
|
|
font-weight: 600;
|
||
|
|
font-size: 0.9rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-badge {
|
||
|
|
padding: 0.15rem 0.4rem;
|
||
|
|
border-radius: 4px;
|
||
|
|
font-size: 0.7rem;
|
||
|
|
font-weight: 600;
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-badge.new {
|
||
|
|
background: #22c55e;
|
||
|
|
color: white;
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-badge.hit {
|
||
|
|
background: #3b82f6;
|
||
|
|
color: white;
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-badge.expiring {
|
||
|
|
background: #f59e0b;
|
||
|
|
color: white;
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-badge.evicting {
|
||
|
|
background: #ef4444;
|
||
|
|
color: white;
|
||
|
|
}
|
||
|
|
|
||
|
|
.entry-ttl {
|
||
|
|
margin-bottom: 0.5rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.ttl-bar {
|
||
|
|
height: 6px;
|
||
|
|
background: var(--vp-c-bg);
|
||
|
|
border-radius: 999px;
|
||
|
|
overflow: hidden;
|
||
|
|
margin-bottom: 0.25rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.ttl-fill {
|
||
|
|
height: 100%;
|
||
|
|
background: linear-gradient(90deg, #22c55e, #f59e0b, #ef4444);
|
||
|
|
transition: width 1s linear;
|
||
|
|
}
|
||
|
|
|
||
|
|
.ttl-text {
|
||
|
|
font-size: 0.75rem;
|
||
|
|
color: var(--vp-c-text-2);
|
||
|
|
}
|
||
|
|
|
||
|
|
.entry-meta {
|
||
|
|
display: flex;
|
||
|
|
justify-content: space-between;
|
||
|
|
font-size: 0.75rem;
|
||
|
|
color: var(--vp-c-text-2);
|
||
|
|
}
|
||
|
|
|
||
|
|
.controls {
|
||
|
|
display: grid;
|
||
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||
|
|
gap: 1rem;
|
||
|
|
margin-bottom: 1.5rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.control-group {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
gap: 0.5rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.control-group label {
|
||
|
|
font-size: 0.85rem;
|
||
|
|
font-weight: 600;
|
||
|
|
color: var(--vp-c-text-2);
|
||
|
|
}
|
||
|
|
|
||
|
|
.action-btn {
|
||
|
|
padding: 0.75rem 1rem;
|
||
|
|
border: none;
|
||
|
|
border-radius: 8px;
|
||
|
|
cursor: pointer;
|
||
|
|
font-weight: 600;
|
||
|
|
transition: all 0.2s;
|
||
|
|
}
|
||
|
|
|
||
|
|
.action-btn.read {
|
||
|
|
background: #3b82f6;
|
||
|
|
color: white;
|
||
|
|
}
|
||
|
|
|
||
|
|
.action-btn.write {
|
||
|
|
background: #22c55e;
|
||
|
|
color: white;
|
||
|
|
}
|
||
|
|
|
||
|
|
.action-btn.auto {
|
||
|
|
background: var(--vp-c-bg);
|
||
|
|
color: var(--vp-c-text-1);
|
||
|
|
border: 1px solid var(--vp-c-divider);
|
||
|
|
}
|
||
|
|
|
||
|
|
.action-btn.auto.active {
|
||
|
|
background: #ef4444;
|
||
|
|
color: white;
|
||
|
|
border-color: #ef4444;
|
||
|
|
}
|
||
|
|
|
||
|
|
.action-btn:hover {
|
||
|
|
transform: translateY(-2px);
|
||
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||
|
|
}
|
||
|
|
|
||
|
|
.timeline {
|
||
|
|
background: var(--vp-c-bg);
|
||
|
|
border-radius: 10px;
|
||
|
|
padding: 1.25rem;
|
||
|
|
margin-bottom: 1rem;
|
||
|
|
border: 1px solid var(--vp-c-divider);
|
||
|
|
}
|
||
|
|
|
||
|
|
.timeline-title {
|
||
|
|
font-weight: 600;
|
||
|
|
margin-bottom: 1rem;
|
||
|
|
font-size: 0.95rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.timeline-events {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
gap: 0.5rem;
|
||
|
|
max-height: 200px;
|
||
|
|
overflow-y: auto;
|
||
|
|
}
|
||
|
|
|
||
|
|
.event {
|
||
|
|
display: grid;
|
||
|
|
grid-template-columns: 70px 1fr;
|
||
|
|
gap: 0.75rem;
|
||
|
|
padding: 0.5rem;
|
||
|
|
border-radius: 6px;
|
||
|
|
font-size: 0.85rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.event.hit {
|
||
|
|
background: #eff6ff;
|
||
|
|
}
|
||
|
|
|
||
|
|
.event.miss {
|
||
|
|
background: #fef2f2;
|
||
|
|
}
|
||
|
|
|
||
|
|
.event.write {
|
||
|
|
background: #f0fdf4;
|
||
|
|
}
|
||
|
|
|
||
|
|
.event.eviction {
|
||
|
|
background: #fef2f2;
|
||
|
|
}
|
||
|
|
|
||
|
|
.event-time {
|
||
|
|
color: var(--vp-c-text-2);
|
||
|
|
font-size: 0.75rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.event-content {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 0.5rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.event-icon {
|
||
|
|
font-size: 1rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.legend {
|
||
|
|
display: flex;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
gap: 1rem;
|
||
|
|
padding: 1rem;
|
||
|
|
background: var(--vp-c-bg);
|
||
|
|
border-radius: 8px;
|
||
|
|
border: 1px solid var(--vp-c-divider);
|
||
|
|
}
|
||
|
|
|
||
|
|
.legend-item {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 0.5rem;
|
||
|
|
font-size: 0.85rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.legend-color {
|
||
|
|
width: 16px;
|
||
|
|
height: 16px;
|
||
|
|
border-radius: 4px;
|
||
|
|
border: 2px solid;
|
||
|
|
}
|
||
|
|
|
||
|
|
.legend-color.new {
|
||
|
|
border-color: #22c55e;
|
||
|
|
background: #f0fdf4;
|
||
|
|
}
|
||
|
|
|
||
|
|
.legend-color.hit {
|
||
|
|
border-color: #3b82f6;
|
||
|
|
background: #eff6ff;
|
||
|
|
}
|
||
|
|
|
||
|
|
.legend-color.expiring {
|
||
|
|
border-color: #f59e0b;
|
||
|
|
background: #fef3c7;
|
||
|
|
}
|
||
|
|
|
||
|
|
.legend-color.evicting {
|
||
|
|
border-color: #ef4444;
|
||
|
|
background: #fef2f2;
|
||
|
|
}
|
||
|
|
|
||
|
|
@keyframes slideIn {
|
||
|
|
from {
|
||
|
|
opacity: 0;
|
||
|
|
transform: translateY(-10px);
|
||
|
|
}
|
||
|
|
to {
|
||
|
|
opacity: 1;
|
||
|
|
transform: translateY(0);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@keyframes shake {
|
||
|
|
0%,
|
||
|
|
100% {
|
||
|
|
transform: translateX(0);
|
||
|
|
}
|
||
|
|
25% {
|
||
|
|
transform: translateX(-5px);
|
||
|
|
}
|
||
|
|
75% {
|
||
|
|
transform: translateX(5px);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
</style>
|