[verified] refactor: establish service and frontend boundaries

This commit is contained in:
chick
2026-07-15 02:43:41 +08:00
parent 0ac6faa6a3
commit e04a16e817
29 changed files with 917 additions and 598 deletions
+48 -52
View File
@@ -1,9 +1,13 @@
import { escapeHtml, value, formatBytes } from './domain/formatters.js'
import { createApiClient, normalizeTargetUrl } from './infrastructure/api-client.js'
import { RequestCoordinator } from './state/request-coordinator.js'
import { createViewRouter, resolveView } from './router/view-router.js'
let instances = []
let statuses = new Map()
let catalog = []
let activeId = null
let activeFilter = 'all'
let activeModule = 'all'
let configPath = 'config.json'
let autoRefreshTimer = null
@@ -20,10 +24,10 @@ const modules = [
{ id: 'notify-auto-ota', title: '通知 / 自动化 / OTA', desc: '通知配置与日志、自动化任务日志、在线升级状态。', tone: 'neutral', endpoints: ['/api/notifications/config', '/api/notifications/logs', '/api/automation/config', '/api/automation/logs', '/api/ota/status'] },
]
function escapeHtml(text) {
return String(text ?? '').replace(/[&<>"']/g, ch => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[ch]))
}
function value(v) { return v === undefined || v === null || v === '' ? '-' : String(v) }
const requests = new RequestCoordinator()
const api = createApiClient()
const routeView = createViewRouter({ controls: () => $$('[data-view]'), panels: () => $$('.view') })
function boolText(v) {
if (v === true) return '开启'
if (v === false) return '关闭'
@@ -79,27 +83,10 @@ function inferMethod(endpoint) {
if (/send|config|data$|roaming$|airplane-mode$|radio-mode$|band-lock$|cell-lock$|work-mode|login|logout/i.test(endpoint)) return 'POST'
return 'GET'
}
function proxiedUrl(endpoint) {
if (!activeId) return null
const normalized = endpoint.startsWith('/') ? endpoint : `/${endpoint}`
return `/api/proxy/${encodeURIComponent(activeId)}${normalized}`
}
function firstValue(...items) {
for (const item of items) if (item !== undefined && item !== null && item !== '') return item
return null
}
function formatBytes(n) {
if (n === undefined || n === null || n === '') return '-'
const num = Number(n)
if (!Number.isFinite(num)) return value(n)
const units = ['B', 'KB', 'MB', 'GB', 'TB']
let size = Math.abs(num)
let i = 0
while (size >= 1024 && i < units.length - 1) { size /= 1024; i += 1 }
const signed = num < 0 ? -size : size
return `${signed.toFixed(size >= 10 || i === 0 ? 0 : 1)} ${units[i]}`
}
function formatSpeed(speed) {
if (!speed || typeof speed !== 'object') return '-'
if (Array.isArray(speed.interfaces)) {
@@ -236,6 +223,7 @@ function renderInstances() { renderHomeCards() }
function openDeviceDetail(id) {
const item = instances.find(x => x.id === id)
if (!item) return
if (activeId !== id) { requests.invalidate('api-console'); requests.invalidate('login') }
activeId = id
localStorage.setItem('multi-simadmin-active', id)
$('loginBtn').disabled = false
@@ -252,6 +240,7 @@ function openDeviceDetail(id) {
}
function setActiveHome() {
requests.invalidate('api-console'); requests.invalidate('login')
activeId = null
localStorage.removeItem('multi-simadmin-active')
$('loginBtn').disabled = true
@@ -364,10 +353,8 @@ function renderEndpointList() {
}
function switchView(view) {
if (!['home', 'detail', 'rawpage', 'api'].includes(view)) view = activeId ? 'detail' : 'home'
$$('[data-view]').forEach(btn => btn.classList.toggle('active', btn.dataset.view === view))
$$('.view').forEach(panel => panel.classList.toggle('active', panel.id === `${view}View`))
localStorage.setItem('multi-simadmin-view', view)
view = resolveView(view, Boolean(activeId))
routeView(view)
}
function selectInstance(id) { openDeviceDetail(id) }
@@ -399,11 +386,8 @@ async function reloadConfigAndRefresh({ selectId = activeId, toast = '' } = {})
async function saveInstanceFromForm() {
const originalId = $('instanceOriginalId').value
const payload = instancePayloadFromForm()
const method = originalId ? 'PUT' : 'POST'
const url = originalId ? `/api/instances/${encodeURIComponent(originalId)}` : '/api/instances'
const res = await fetch(url, { method, headers: { 'content-type': 'application/json' }, body: JSON.stringify(payload) })
const data = await res.json().catch(() => ({}))
if (!res.ok) throw new Error(data.error || `保存失败:HTTP ${res.status}`)
payload.url = normalizeTargetUrl(payload.url)
const data = originalId ? await api.updateInstance(originalId, payload) : await api.createInstance(payload)
$('instanceDialog').close()
await reloadConfigAndRefresh({ selectId: data.instance?.id || payload.id, toast: originalId ? '设备地址已更新' : '设备地址已添加' })
}
@@ -412,12 +396,7 @@ async function deleteInstance(id) {
const item = instances.find(x => x.id === id)
if (!item) return
if (!confirm(`确定删除设备“${item.name || item.id}”?\n配置会立即写入 config.json。`)) return
const res = await fetch(`/api/instances/${encodeURIComponent(id)}`, { method: 'DELETE' })
const data = await res.json().catch(() => ({}))
if (!res.ok) {
showToast(data.error || `删除失败:HTTP ${res.status}`, 4200)
return
}
try { await api.deleteInstance(id) } catch (error) { showToast(error.message, 4200); return }
await reloadConfigAndRefresh({ selectId: null, toast: '设备地址已删除' })
}
@@ -429,13 +408,11 @@ async function deleteCurrentInstanceFromDialog() {
}
async function loadConfig({ preserveSelection = activeId } = {}) {
const [configRes, catalogRes] = await Promise.all([fetch('/api/config'), fetch('/api/catalog')])
if (!configRes.ok) throw new Error(`配置读取失败:HTTP ${configRes.status}`)
const data = await configRes.json()
const [data, catalogData] = await Promise.all([api.config(), api.catalog().catch(() => ({ groups: [] }))])
instances = data.instances || []
configPath = data.configPath || 'config.json'
$('configPath').textContent = configPath
if (catalogRes.ok) catalog = (await catalogRes.json()).groups || []
catalog = catalogData.groups || []
const saved = preserveSelection || localStorage.getItem('multi-simadmin-active')
activeId = saved && instances.some(x => x.id === saved) ? saved : null
renderEndpointList()
@@ -447,9 +424,12 @@ async function refreshStatus({ silent = false } = {}) {
$('refreshBtn').disabled = true
if (!silent) showToast('正在刷新全部实例…', 1200)
try {
const res = await fetch('/api/status')
if (!res.ok) throw new Error(`状态读取失败:HTTP ${res.status}`)
const data = await res.json()
const result = await requests.run('fleet-status', 'fleet', () => api.status())
if (!result.accepted || result.error) {
if (result.accepted && result.error) showToast(result.error.message || '刷新失败', 4200)
return
}
const data = result.value
statuses = new Map((data.instances || []).map(item => [item.id, item]))
renderHomeCards()
if (activeId) {
@@ -471,13 +451,16 @@ async function loginActive() {
if (!item) return
// If config already has a saved password, try direct refresh first; otherwise ask for password.
if (item.auth?.hasPassword) {
const loginOwnerId = activeId
showToast('正在使用本地配置密码刷新会话…', 1600)
const res = await fetch(`/api/instances/${encodeURIComponent(activeId)}/login`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({}) })
if (!res.ok) showToast(`登录失败:HTTP ${res.status}`, 4200)
const result = await requests.run('login', loginOwnerId, () => api.login(loginOwnerId))
if (!result.accepted || result.ownerId !== activeId) return
if (result.error) { showToast(result.error.message || '登录失败', 4200); return }
await refreshStatus({ silent: true })
return
}
$('loginDialog').showModal()
$('loginDialog').dataset.instanceId = activeId
setTimeout(() => $('passwordInput').focus(), 50)
}
@@ -492,6 +475,7 @@ async function runEndpoint() {
showToast('请先在首页选择设备卡片')
return
}
const requestOwnerId = activeId
const endpoint = $('endpointInput').value.trim()
const method = $('methodSelect').value
if (!endpoint.startsWith('/')) {
@@ -507,9 +491,19 @@ async function runEndpoint() {
if (body !== undefined) init.body = JSON.stringify(body)
}
const started = performance.now()
const res = await fetch(proxiedUrl(endpoint), init)
const result = await requests.run('api-console', requestOwnerId, async () => {
const normalized = endpoint.startsWith('/') ? endpoint : `/${endpoint}`
const res = await api.proxy(requestOwnerId, normalized, init)
return { res, text: await res.text() }
})
if (!result.accepted || result.ownerId !== activeId) return
if (result.error) {
$('rawOutput').textContent = result.error.message || String(result.error)
showToast('接口调用失败', 4200)
return
}
const { res, text } = result.value
const elapsed = Math.round(performance.now() - started)
const text = await res.text()
let formatted = text
try { formatted = JSON.stringify(JSON.parse(text), null, 2) } catch {}
$('rawOutput').textContent = `HTTP ${res.status} · ${elapsed}ms · ${method} ${endpoint}\n\n${formatted}`
@@ -539,13 +533,15 @@ function bindEvents() {
}))
$('loginForm').addEventListener('submit', async (event) => {
event.preventDefault()
if (!activeId) return
const loginInstanceId = $('loginDialog').dataset.instanceId
if (!loginInstanceId) return
const password = $('passwordInput').value
$('passwordInput').value = ''
$('loginDialog').close()
const res = await fetch(`/api/instances/${encodeURIComponent(activeId)}/login`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ password }) })
if (!res.ok) showToast(`登录失败:HTTP ${res.status}`, 4200)
else showToast('会话已刷新')
const result = await requests.run('login', loginInstanceId, () => api.login(loginInstanceId, password))
if (!result.accepted || result.ownerId !== activeId) return
if (result.error) { showToast(result.error.message || '登录失败', 4200); return }
showToast('会话已刷新')
await refreshStatus({ silent: true })
})
$('instanceForm').addEventListener('submit', async (event) => {
+10
View File
@@ -0,0 +1,10 @@
export function value(input) { return input === undefined || input === null || input === '' ? '-' : String(input) }
export function escapeHtml(text) { return String(text ?? '').replace(/[&<>"']/g, ch => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[ch])) }
export function formatBytes(input) {
if (input === undefined || input === null || input === '') return '-'
const number = Number(input)
if (!Number.isFinite(number)) return value(input)
const units = ['B', 'KB', 'MB', 'GB', 'TB']; let size = Math.abs(number); let index = 0
while (size >= 1024 && index < units.length - 1) { size /= 1024; index += 1 }
return `${number < 0 ? -size : size.toFixed(size >= 10 || index === 0 ? 0 : 1)} ${units[index]}`
}
+27
View File
@@ -0,0 +1,27 @@
export function normalizeTargetUrl(raw) {
const url = new URL(raw)
if (!['http:', 'https:'].includes(url.protocol)) throw new Error('target URL must use http or https')
url.hash = ''; url.search = ''
return url.toString().replace(/\/$/, '')
}
export function createApiClient({ fetchImpl = globalThis.fetch } = {}) {
const request = async (url, options) => {
const response = await fetchImpl(url, options)
const data = await response.json().catch(() => ({}))
if (!response.ok) throw new Error(data.error || `HTTP ${response.status}`)
return data
}
return {
request,
config: () => request('/api/config'),
catalog: () => request('/api/catalog'),
status: () => request('/api/status'),
proxy: (id, endpoint, options) => fetchImpl(`/api/proxy/${encodeURIComponent(id)}${endpoint}`, options),
createInstance: payload => request('/api/instances', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(payload) }),
updateInstance: (id, payload) => request(`/api/instances/${encodeURIComponent(id)}`, { method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify(payload) }),
deleteInstance: id => request(`/api/instances/${encodeURIComponent(id)}`, { method: 'DELETE' }),
login: (id, password) => request(`/api/instances/${encodeURIComponent(id)}/login`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(password === undefined ? {} : { password }) }),
logout: id => request(`/api/instances/${encodeURIComponent(id)}/logout`, { method: 'POST' }),
}
}
+11
View File
@@ -0,0 +1,11 @@
export const VIEW_IDS = Object.freeze(['home', 'detail', 'rawpage', 'api'])
export function resolveView(view, hasActiveInstance = false) {
return VIEW_IDS.includes(view) ? view : hasActiveInstance ? 'detail' : 'home'
}
export function createViewRouter({ panels, controls, storage = globalThis.localStorage } = {}) {
return view => {
for (const control of controls()) control.classList.toggle('active', control.dataset.view === view)
for (const panel of panels()) panel.classList.toggle('active', panel.id === `${view}View`)
storage?.setItem('multi-simadmin-view', view)
}
}
+14
View File
@@ -0,0 +1,14 @@
export class RequestCoordinator {
#versions = new Map()
async run(channel, ownerId, operation) {
const version = (this.#versions.get(channel) || 0) + 1
this.#versions.set(channel, version)
try {
const value = await operation()
return { accepted: this.#versions.get(channel) === version, ownerId, value }
} catch (error) {
return { accepted: this.#versions.get(channel) === version, ownerId, error }
}
}
invalidate(channel) { this.#versions.set(channel, (this.#versions.get(channel) || 0) + 1) }
}
+4 -17
View File
@@ -1,17 +1,4 @@
:root{
--animal-primary-color:#19c8b9;--animal-primary-color-hover:#3dd4c6;--animal-primary-color-active:#50b9ab;--animal-primary-color-bg:#e6f9f6;
--animal-success-color:#6fba2c;--animal-warning-color:#f5c31c;--animal-error-color:#e05a5a;
--animal-text-color:#794f27;--animal-text-color-body:#725d42;--animal-text-color-secondary:#9f927d;--animal-text-color-muted:#8a7b66;--animal-text-color-disabled:#c4b89e;
--animal-border-color:#aaa69d;--animal-border-color-hover:#827157;--animal-border-color-light:#e8e2d6;
--animal-bg-color:#f8f8f0;--animal-bg-color-content:rgb(247,243,223);--animal-bg-color-secondary:#f0e8d8;--animal-bg-color-disabled:#f0ece2;
--animal-font-family:Nunito,'Noto Sans SC',-apple-system,'PingFang SC','Hiragino Sans GB','Microsoft YaHei',sans-serif;
--animal-shadow-sm:0 2px 4px 0 rgba(61,52,40,.06);--animal-shadow-base:0 3px 10px 0 rgba(61,52,40,.1);--animal-shadow-lg:0 8px 24px 0 rgba(61,52,40,.14);
--animal-motion-duration-base:.25s;--animal-motion-ease:cubic-bezier(.4,0,.2,1);
}
*{box-sizing:border-box}html,body{min-height:100%}body{margin:0;min-height:100vh;font-family:var(--animal-font-family);font-weight:500;letter-spacing:.01em;color:var(--animal-text-color);background:var(--animal-bg-color);overflow:hidden}button,input,select,textarea{font:inherit;color:inherit}button{cursor:pointer}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible,.sim-device-card:focus-visible{outline:3px solid #ffcc00;outline-offset:3px}
.island-bg{position:fixed;inset:0;z-index:-1;overflow:hidden;background:radial-gradient(circle at 12% 10%,rgba(130,213,187,.28),transparent 24%),radial-gradient(circle at 88% 6%,rgba(247,205,103,.28),transparent 22%),linear-gradient(180deg,#f8f8f0 0%,#f4efd9 54%,#e8dfbd 100%)}.island-bg:before{content:"";position:absolute;inset:0;background-image:radial-gradient(circle,rgba(196,184,158,.15) 1.5px,transparent 1.5px),radial-gradient(circle,rgba(196,184,158,.1) 1px,transparent 1px);background-size:28px 28px,14px 14px;background-position:0 0,7px 7px}.cloud{position:absolute;border-radius:999px;background:rgba(255,255,255,.72)}.cloud:before,.cloud:after{content:"";position:absolute;border-radius:50%;background:inherit}.cloud-a{width:170px;height:48px;left:8%;top:7%}.cloud-a:before{width:72px;height:72px;left:28px;top:-28px}.cloud-a:after{width:56px;height:56px;right:26px;top:-18px}.cloud-b{width:210px;height:58px;right:8%;top:12%;opacity:.7}.cloud-b:before{width:86px;height:86px;left:34px;top:-36px}.cloud-b:after{width:72px;height:72px;right:28px;top:-26px}.leaf{position:absolute;color:#8ac68a;font-size:18px;opacity:.42;animation:leafFloat 7s ease-in-out infinite}.leaf-a{left:46%;top:8%}.leaf-b{right:26%;bottom:9%;animation-delay:1.7s}@keyframes leafFloat{0%,100%{transform:translateY(0) rotate(0)}50%{transform:translateY(-12px) rotate(12deg)}}
.island-card{border-radius:24px;background:var(--animal-bg-color-content);color:var(--animal-text-color-body);border:1.5px solid #d4c4a8;box-shadow:var(--animal-shadow-base)}.pattern-default{background:radial-gradient(circle,rgba(196,184,158,.15) 1.5px,transparent 1.5px),radial-gradient(circle,rgba(196,184,158,.1) 1px,transparent 1px),rgb(247,243,223);background-size:28px 28px,14px 14px;background-position:0 0,7px 7px}.pattern-app-teal{background:radial-gradient(circle,rgba(130,213,187,.18) 1.5px,transparent 1.5px),radial-gradient(circle,rgba(170,235,210,.12) 1px,transparent 1px),#e8faf5;background-size:28px 28px,14px 14px;border-color:#82d5bb;color:#2a6b5a}.pattern-app-yellow{background:radial-gradient(circle,rgba(247,205,103,.18) 1.5px,transparent 1.5px),radial-gradient(circle,rgba(255,230,160,.12) 1px,transparent 1px),#fff8e0;background-size:28px 28px,14px 14px;border-color:#f7cd67;color:#7a6528}.pattern-app-blue{background:radial-gradient(circle,rgba(136,157,240,.18) 1.5px,transparent 1.5px),radial-gradient(circle,rgba(180,195,255,.12) 1px,transparent 1px),#e8edff;background-size:28px 28px,14px 14px;border-color:#889df0;color:#4a5a8a}.pattern-purple{background:radial-gradient(circle,rgba(183,125,238,.18) 1.5px,transparent 1.5px),radial-gradient(circle,rgba(220,180,255,.12) 1px,transparent 1px),#f0e8ff;background-size:28px 28px,14px 14px;border-color:#b77dee;color:#6a3a9a}.pattern-app-pink{background:#ffe8ee;border-color:#f8a6b2}.section-ribbon{--rf:#8ac68a;--rb:#509050;display:inline-flex;align-items:center;min-height:34px;padding:6px 18px;border-radius:12px;background:var(--rf);color:#fff;font-weight:900;letter-spacing:.04em;box-shadow:0 4px 0 0 var(--rb)}.color-app-yellow{--rf:#f7cd67;--rb:#d4a030;color:#725d42}.color-app-green{--rf:#8ac68a;--rb:#509050}.color-purple{--rf:#b77dee;--rb:#8a4ac0}.color-app-pink{--rf:#f8a6b2;--rb:#d77485}
h1,h2,h3,h4,p{margin:0}.eyebrow{margin:0 0 6px;color:#8a7b66;font-size:12px;font-weight:900;letter-spacing:.08em;text-transform:uppercase}h1{font-size:34px;line-height:1.05;font-weight:900;color:#794f27}.topbar-copy,.board-caption{margin-top:8px;color:#725d42;line-height:1.5}.animal-btn{position:relative;display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:45px;padding:0 20px;border:2px solid transparent;border-radius:50px;font-weight:800;text-decoration:none;line-height:1;transition:all var(--animal-motion-duration-base) var(--animal-motion-ease);user-select:none}.primary-btn{color:#794f27;background:#f8f8f0;border-color:#f8f8f0;box-shadow:0 5px 0 0 #bdaea0}.primary-btn:hover:not(:disabled),.default-btn:hover:not(:disabled){transform:translateY(-1px)}.default-btn{color:var(--animal-text-color);background:var(--animal-bg-color);border-color:var(--animal-border-color);box-shadow:var(--animal-shadow-sm)}.danger-btn{color:#fff;background:var(--animal-error-color);border-color:var(--animal-error-color)}.disabled,button:disabled{opacity:.5;pointer-events:none}
.sim-shell{height:100vh;display:grid;grid-template-rows:auto minmax(0,1fr) auto;gap:16px;padding:20px}.sim-topbar{display:flex;align-items:center;justify-content:space-between;gap:18px;padding:20px 24px}.hero-actions{display:flex;gap:10px;flex-wrap:wrap;justify-content:flex-end}.sim-main{min-height:0;overflow:hidden}.view{display:none;min-height:0;height:100%;overflow:auto}.view.active{display:block}.home-view{display:none}.home-view.active{display:grid;grid-template-rows:auto minmax(0,1fr);gap:16px}.home-toolbar{display:grid;grid-template-columns:minmax(280px,.8fr) minmax(420px,1.2fr);gap:16px;align-items:end;padding:18px}.home-tools{display:grid;grid-template-columns:260px minmax(220px,1fr);gap:12px;align-items:center}.wallet-row{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}.fleet-card{border-radius:999px;background:#d1da49;color:#3d5a1a;border:2px solid rgba(90,107,40,.18);padding:9px 8px;text-align:center;box-shadow:inset 0 -2px 0 rgba(61,52,40,.08)}.fleet-card b{display:block;font-size:22px;font-weight:900;line-height:1}.fleet-card span{display:block;margin-top:2px;font-size:11px;font-weight:800}.search-pill{height:48px;display:flex;align-items:center;gap:8px;background:rgb(247,243,223);border:2.5px solid #c4b89e;border-radius:50px;padding:0 16px;color:#8a7b66}.search-pill input{width:100%;border:0;outline:0;background:transparent}.island-tabs{grid-column:1 / -1;display:flex;gap:8px;flex-wrap:wrap}.tab-leaf{border:0;border-radius:999px;background:#f7f3df;color:#725d42;padding:9px 16px;font-weight:900}.tab-leaf.active{background:#19c8b9;color:#fff}.home-device-grid{min-height:0;overflow:auto;display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:16px;padding:2px 4px 16px}.sim-device-card{border:2px solid rgba(212,196,168,.88);border-radius:28px;background:linear-gradient(180deg,rgba(255,255,255,.76),rgba(247,243,223,.92));padding:18px;box-shadow:0 8px 22px rgba(61,52,40,.1);transition:transform .2s ease, box-shadow .2s ease, border-color .2s ease}.sim-device-card:hover{transform:translateY(-3px);box-shadow:0 14px 30px rgba(61,52,40,.15);border-color:#19c8b9}.sim-card-head{display:flex;justify-content:space-between;gap:12px;align-items:flex-start}.sim-card-head h2{font-size:24px;color:#4d3a22}.instance-url{display:block;margin-top:4px;color:#8a7b66;font-size:12px;word-break:break-all}.status-pill{display:inline-flex;align-items:center;justify-content:center;min-height:28px;padding:0 10px;border-radius:999px;background:#eee;color:#725d42;font-size:12px;font-weight:900;white-space:nowrap}.status-pill.ok,.sim-device-card.ok .status-pill{background:#dff5d7;color:#3f7624}.status-pill.warn,.sim-device-card.warn .status-pill{background:#fff0bf;color:#8a6010}.status-pill.bad,.sim-device-card.bad .status-pill{background:#ffe0e0;color:#9a3030}.sim-card-model{margin-top:14px;color:#725d42;font-weight:900}.sim-card-kpis{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin-top:14px}.sim-card-kpis span,.signal-item,.metric{border-radius:18px;background:rgba(255,255,255,.62);border:2px solid rgba(232,226,214,.8);padding:12px;min-width:0}.sim-card-kpis em,.signal-item span,.metric span{display:block;color:#9f927d;font-size:12px;font-weight:900;font-style:normal}.sim-card-kpis b,.signal-item b,.metric b{display:block;margin-top:5px;color:#794f27;font-size:16px;font-weight:900}.sim-card-kpis b{overflow:visible;text-overflow:clip;white-space:normal;overflow-wrap:anywhere;word-break:break-word;line-height:1.35}.sim-card-foot{display:flex;justify-content:space-between;gap:10px;margin-top:14px;color:#8a7b66;font-size:12px;font-weight:800;flex-wrap:wrap;overflow:visible;text-overflow:clip;white-space:normal}.sim-card-foot span{overflow-wrap:anywhere;word-break:break-word}.sim-card-model,.instance-url{overflow:visible;text-overflow:clip;white-space:normal;overflow-wrap:anywhere;word-break:break-word;line-height:1.35}.sim-config-note{display:flex;gap:8px;align-items:center;justify-content:flex-end;color:#8a7b66;font-size:12px}.sim-config-note code{background:rgba(255,255,255,.6);border-radius:8px;padding:4px 8px}
.detail-view.active{display:grid;grid-template-rows:auto minmax(0,1fr);gap:16px}.workspace-hero{padding:20px 24px;display:flex;align-items:center;justify-content:space-between;gap:18px}.detail-layout{min-height:0;display:grid;grid-template-columns:minmax(320px,.75fr) minmax(0,1.25fr);grid-template-rows:auto minmax(0,1fr) auto;gap:16px;overflow:auto}.signal-board{padding:18px}.signal-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:10px;margin-top:18px}.nook-metrics{display:grid;grid-template-columns:repeat(2,minmax(140px,1fr));gap:12px}.device-detail-board{grid-column:2;grid-row:1 / span 2;padding:18px;overflow:auto}.device-detail-grid{display:grid;grid-template-columns:repeat(2,minmax(220px,1fr));gap:14px;margin-top:18px}.device-info-card{background:rgba(255,255,255,.62);border:2px solid rgba(212,196,168,.72);border-radius:24px;padding:16px;min-width:0;box-shadow:inset 0 -2px 0 rgba(61,52,40,.04)}.device-info-card h4{margin:0 0 12px;color:#794f27;font-size:18px;font-weight:900}.device-info-card dl{display:grid;gap:8px;margin:0}.device-info-card dl div{display:grid;grid-template-columns:86px minmax(0,1fr);gap:10px;align-items:baseline}.device-info-card dt{color:#9f927d;font-size:12px;font-weight:900}.device-info-card dd{margin:0;color:#4d3a22;font-weight:800;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.detail-actions-card{grid-column:1 / -1;padding:18px}.detail-actions{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px;margin-top:18px}.detail-action{min-height:64px;border:2px solid rgba(212,196,168,.85);background:rgba(255,255,255,.72);border-radius:20px;padding:10px 14px;text-align:left;font-weight:900;color:#4d3a22}.detail-action:hover{border-color:#19c8b9;transform:translateY(-1px)}.detail-action span{display:block}.detail-action code{display:block;margin-top:4px;color:#8a7b66;font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.raw-action{background:#e8edff;color:#4a5a8a}.api-layout.active{display:grid;grid-template-columns:320px minmax(0,1fr);gap:16px}.api-layout:before{content:'调试接口为次级入口;日常使用请从首页卡片进入设备详情。';display:block;grid-column:1 / -1;color:#8a7b66;font-weight:800;margin-left:4px}.api-catalog,.api-console{padding:18px}.endpoint-list{margin-top:16px;display:grid;gap:12px}.endpoint-group h4{margin:0 0 8px}.endpoint-row{width:100%;border:0;border-radius:14px;background:rgba(255,255,255,.58);padding:9px 10px;text-align:left;font-size:12px}.endpoint-row span{font-weight:900;color:#6a3a9a;margin-right:6px}.console-toolbar{display:grid;grid-template-columns:110px minmax(0,1fr) 100px;gap:10px}.console-toolbar select,.console-toolbar input,.request-body{border:2px solid #d4c4a8;border-radius:14px;background:#fff;padding:10px}.request-body{width:100%;min-height:110px;margin-top:12px}.raw-output{min-height:340px;overflow:auto;background:#3a3028;color:#f8f0dc;border-radius:18px;padding:16px;white-space:pre-wrap}.rawpage-view.active{display:grid;grid-template-rows:auto minmax(0,1fr);gap:12px}.frame-toolbar{padding:14px 18px;display:flex;gap:12px;align-items:center}.frame{width:100%;height:100%;border:0;border-radius:24px;background:#fff}.empty-state{padding:28px;text-align:center}.login-card,.instance-form{width:min(560px,calc(100vw - 32px));padding:24px}.login-card input,.instance-form input,.instance-form textarea{width:100%;border:2px solid #d4c4a8;border-radius:14px;background:#fff;padding:10px;margin-top:6px}.instance-form{display:grid;gap:12px}.instance-form label{font-weight:900}.form-menu,dialog menu{display:flex;justify-content:flex-end;gap:10px;padding:0;margin:10px 0 0}.toast{position:fixed;right:20px;bottom:20px;background:#4d3a22;color:#fff;border-radius:999px;padding:12px 18px;font-weight:900;box-shadow:var(--animal-shadow-lg);z-index:20}.compact .sim-device-card{padding:14px}.compact .sim-card-kpis{grid-template-columns:repeat(3,1fr);gap:8px}.compact .device-info-card{padding:12px}.compact .device-info-card dl{gap:5px}
@media(max-width:1260px){.home-toolbar{grid-template-columns:1fr}.detail-layout{grid-template-columns:1fr}.device-detail-board{grid-column:auto;grid-row:auto}.detail-actions-card{grid-column:auto}.device-detail-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.nook-metrics{grid-template-columns:repeat(4,1fr)}}@media(max-width:900px){body{overflow:auto}.sim-shell{height:auto;padding:14px}.sim-topbar,.workspace-hero{flex-direction:column;align-items:flex-start}.sim-main{overflow:visible}.view{overflow:visible}.home-tools{grid-template-columns:1fr}.home-device-grid{overflow:visible;grid-template-columns:1fr}.api-layout.active{grid-template-columns:1fr}.nook-metrics,.device-detail-grid{grid-template-columns:1fr 1fr}.hero-actions{justify-content:flex-start}}@media(max-width:560px){.wallet-row,.signal-grid,.nook-metrics,.device-detail-grid,.sim-card-kpis{grid-template-columns:1fr}.hero-actions{width:100%}.hero-actions .animal-btn{width:100%}.console-toolbar{grid-template-columns:1fr}.device-info-card dl div{grid-template-columns:76px minmax(0,1fr)}}@media(prefers-reduced-motion:reduce){*,*:before,*:after{animation:none!important;transition:none!important}}
@import url('./styles/tokens.css');
@import url('./styles/base.css');
@import url('./styles/components.css');
@import url('./styles/responsive.css');
+2
View File
@@ -0,0 +1,2 @@
*{box-sizing:border-box}html,body{min-height:100%}body{margin:0;min-height:100vh;font-family:var(--animal-font-family);font-weight:500;letter-spacing:.01em;color:var(--animal-text-color);background:var(--animal-bg-color);overflow:hidden}button,input,select,textarea{font:inherit;color:inherit}button{cursor:pointer}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible,.sim-device-card:focus-visible{outline:3px solid #ffcc00;outline-offset:3px}
.island-bg{position:fixed;inset:0;z-index:-1;overflow:hidden;background:radial-gradient(circle at 12% 10%,rgba(130,213,187,.28),transparent 24%),radial-gradient(circle at 88% 6%,rgba(247,205,103,.28),transparent 22%),linear-gradient(180deg,#f8f8f0 0%,#f4efd9 54%,#e8dfbd 100%)}.island-bg:before{content:"";position:absolute;inset:0;background-image:radial-gradient(circle,rgba(196,184,158,.15) 1.5px,transparent 1.5px),radial-gradient(circle,rgba(196,184,158,.1) 1px,transparent 1px);background-size:28px 28px,14px 14px;background-position:0 0,7px 7px}.cloud{position:absolute;border-radius:999px;background:rgba(255,255,255,.72)}.cloud:before,.cloud:after{content:"";position:absolute;border-radius:50%;background:inherit}.cloud-a{width:170px;height:48px;left:8%;top:7%}.cloud-a:before{width:72px;height:72px;left:28px;top:-28px}.cloud-a:after{width:56px;height:56px;right:26px;top:-18px}.cloud-b{width:210px;height:58px;right:8%;top:12%;opacity:.7}.cloud-b:before{width:86px;height:86px;left:34px;top:-36px}.cloud-b:after{width:72px;height:72px;right:28px;top:-26px}.leaf{position:absolute;color:#8ac68a;font-size:18px;opacity:.42;animation:leafFloat 7s ease-in-out infinite}.leaf-a{left:46%;top:8%}.leaf-b{right:26%;bottom:9%;animation-delay:1.7s}@keyframes leafFloat{0%,100%{transform:translateY(0) rotate(0)}50%{transform:translateY(-12px) rotate(12deg)}}
+4
View File
@@ -0,0 +1,4 @@
.island-card{border-radius:24px;background:var(--animal-bg-color-content);color:var(--animal-text-color-body);border:1.5px solid #d4c4a8;box-shadow:var(--animal-shadow-base)}.pattern-default{background:radial-gradient(circle,rgba(196,184,158,.15) 1.5px,transparent 1.5px),radial-gradient(circle,rgba(196,184,158,.1) 1px,transparent 1px),rgb(247,243,223);background-size:28px 28px,14px 14px;background-position:0 0,7px 7px}.pattern-app-teal{background:radial-gradient(circle,rgba(130,213,187,.18) 1.5px,transparent 1.5px),radial-gradient(circle,rgba(170,235,210,.12) 1px,transparent 1px),#e8faf5;background-size:28px 28px,14px 14px;border-color:#82d5bb;color:#2a6b5a}.pattern-app-yellow{background:radial-gradient(circle,rgba(247,205,103,.18) 1.5px,transparent 1.5px),radial-gradient(circle,rgba(255,230,160,.12) 1px,transparent 1px),#fff8e0;background-size:28px 28px,14px 14px;border-color:#f7cd67;color:#7a6528}.pattern-app-blue{background:radial-gradient(circle,rgba(136,157,240,.18) 1.5px,transparent 1.5px),radial-gradient(circle,rgba(180,195,255,.12) 1px,transparent 1px),#e8edff;background-size:28px 28px,14px 14px;border-color:#889df0;color:#4a5a8a}.pattern-purple{background:radial-gradient(circle,rgba(183,125,238,.18) 1.5px,transparent 1.5px),radial-gradient(circle,rgba(220,180,255,.12) 1px,transparent 1px),#f0e8ff;background-size:28px 28px,14px 14px;border-color:#b77dee;color:#6a3a9a}.pattern-app-pink{background:#ffe8ee;border-color:#f8a6b2}.section-ribbon{--rf:#8ac68a;--rb:#509050;display:inline-flex;align-items:center;min-height:34px;padding:6px 18px;border-radius:12px;background:var(--rf);color:#fff;font-weight:900;letter-spacing:.04em;box-shadow:0 4px 0 0 var(--rb)}.color-app-yellow{--rf:#f7cd67;--rb:#d4a030;color:#725d42}.color-app-green{--rf:#8ac68a;--rb:#509050}.color-purple{--rf:#b77dee;--rb:#8a4ac0}.color-app-pink{--rf:#f8a6b2;--rb:#d77485}
h1,h2,h3,h4,p{margin:0}.eyebrow{margin:0 0 6px;color:#8a7b66;font-size:12px;font-weight:900;letter-spacing:.08em;text-transform:uppercase}h1{font-size:34px;line-height:1.05;font-weight:900;color:#794f27}.topbar-copy,.board-caption{margin-top:8px;color:#725d42;line-height:1.5}.animal-btn{position:relative;display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:45px;padding:0 20px;border:2px solid transparent;border-radius:50px;font-weight:800;text-decoration:none;line-height:1;transition:all var(--animal-motion-duration-base) var(--animal-motion-ease);user-select:none}.primary-btn{color:#794f27;background:#f8f8f0;border-color:#f8f8f0;box-shadow:0 5px 0 0 #bdaea0}.primary-btn:hover:not(:disabled),.default-btn:hover:not(:disabled){transform:translateY(-1px)}.default-btn{color:var(--animal-text-color);background:var(--animal-bg-color);border-color:var(--animal-border-color);box-shadow:var(--animal-shadow-sm)}.danger-btn{color:#fff;background:var(--animal-error-color);border-color:var(--animal-error-color)}.disabled,button:disabled{opacity:.5;pointer-events:none}
.sim-shell{height:100vh;display:grid;grid-template-rows:auto minmax(0,1fr) auto;gap:16px;padding:20px}.sim-topbar{display:flex;align-items:center;justify-content:space-between;gap:18px;padding:20px 24px}.hero-actions{display:flex;gap:10px;flex-wrap:wrap;justify-content:flex-end}.sim-main{min-height:0;overflow:hidden}.view{display:none;min-height:0;height:100%;overflow:auto}.view.active{display:block}.home-view{display:none}.home-view.active{display:grid;grid-template-rows:auto minmax(0,1fr);gap:16px}.home-toolbar{display:grid;grid-template-columns:minmax(280px,.8fr) minmax(420px,1.2fr);gap:16px;align-items:end;padding:18px}.home-tools{display:grid;grid-template-columns:260px minmax(220px,1fr);gap:12px;align-items:center}.wallet-row{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}.fleet-card{border-radius:999px;background:#d1da49;color:#3d5a1a;border:2px solid rgba(90,107,40,.18);padding:9px 8px;text-align:center;box-shadow:inset 0 -2px 0 rgba(61,52,40,.08)}.fleet-card b{display:block;font-size:22px;font-weight:900;line-height:1}.fleet-card span{display:block;margin-top:2px;font-size:11px;font-weight:800}.search-pill{height:48px;display:flex;align-items:center;gap:8px;background:rgb(247,243,223);border:2.5px solid #c4b89e;border-radius:50px;padding:0 16px;color:#8a7b66}.search-pill input{width:100%;border:0;outline:0;background:transparent}.island-tabs{grid-column:1 / -1;display:flex;gap:8px;flex-wrap:wrap}.tab-leaf{border:0;border-radius:999px;background:#f7f3df;color:#725d42;padding:9px 16px;font-weight:900}.tab-leaf.active{background:#19c8b9;color:#fff}.home-device-grid{min-height:0;overflow:auto;display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:16px;padding:2px 4px 16px}.sim-device-card{border:2px solid rgba(212,196,168,.88);border-radius:28px;background:linear-gradient(180deg,rgba(255,255,255,.76),rgba(247,243,223,.92));padding:18px;box-shadow:0 8px 22px rgba(61,52,40,.1);transition:transform .2s ease, box-shadow .2s ease, border-color .2s ease}.sim-device-card:hover{transform:translateY(-3px);box-shadow:0 14px 30px rgba(61,52,40,.15);border-color:#19c8b9}.sim-card-head{display:flex;justify-content:space-between;gap:12px;align-items:flex-start}.sim-card-head h2{font-size:24px;color:#4d3a22}.instance-url{display:block;margin-top:4px;color:#8a7b66;font-size:12px;word-break:break-all}.status-pill{display:inline-flex;align-items:center;justify-content:center;min-height:28px;padding:0 10px;border-radius:999px;background:#eee;color:#725d42;font-size:12px;font-weight:900;white-space:nowrap}.status-pill.ok,.sim-device-card.ok .status-pill{background:#dff5d7;color:#3f7624}.status-pill.warn,.sim-device-card.warn .status-pill{background:#fff0bf;color:#8a6010}.status-pill.bad,.sim-device-card.bad .status-pill{background:#ffe0e0;color:#9a3030}.sim-card-model{margin-top:14px;color:#725d42;font-weight:900}.sim-card-kpis{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin-top:14px}.sim-card-kpis span,.signal-item,.metric{border-radius:18px;background:rgba(255,255,255,.62);border:2px solid rgba(232,226,214,.8);padding:12px;min-width:0}.sim-card-kpis em,.signal-item span,.metric span{display:block;color:#9f927d;font-size:12px;font-weight:900;font-style:normal}.sim-card-kpis b,.signal-item b,.metric b{display:block;margin-top:5px;color:#794f27;font-size:16px;font-weight:900}.sim-card-kpis b{overflow:visible;text-overflow:clip;white-space:normal;overflow-wrap:anywhere;word-break:break-word;line-height:1.35}.sim-card-foot{display:flex;justify-content:space-between;gap:10px;margin-top:14px;color:#8a7b66;font-size:12px;font-weight:800;flex-wrap:wrap;overflow:visible;text-overflow:clip;white-space:normal}.sim-card-foot span{overflow-wrap:anywhere;word-break:break-word}.sim-card-model,.instance-url{overflow:visible;text-overflow:clip;white-space:normal;overflow-wrap:anywhere;word-break:break-word;line-height:1.35}.sim-config-note{display:flex;gap:8px;align-items:center;justify-content:flex-end;color:#8a7b66;font-size:12px}.sim-config-note code{background:rgba(255,255,255,.6);border-radius:8px;padding:4px 8px}
.detail-view.active{display:grid;grid-template-rows:auto minmax(0,1fr);gap:16px}.workspace-hero{padding:20px 24px;display:flex;align-items:center;justify-content:space-between;gap:18px}.detail-layout{min-height:0;display:grid;grid-template-columns:minmax(320px,.75fr) minmax(0,1.25fr);grid-template-rows:auto minmax(0,1fr) auto;gap:16px;overflow:auto}.signal-board{padding:18px}.signal-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:10px;margin-top:18px}.nook-metrics{display:grid;grid-template-columns:repeat(2,minmax(140px,1fr));gap:12px}.device-detail-board{grid-column:2;grid-row:1 / span 2;padding:18px;overflow:auto}.device-detail-grid{display:grid;grid-template-columns:repeat(2,minmax(220px,1fr));gap:14px;margin-top:18px}.device-info-card{background:rgba(255,255,255,.62);border:2px solid rgba(212,196,168,.72);border-radius:24px;padding:16px;min-width:0;box-shadow:inset 0 -2px 0 rgba(61,52,40,.04)}.device-info-card h4{margin:0 0 12px;color:#794f27;font-size:18px;font-weight:900}.device-info-card dl{display:grid;gap:8px;margin:0}.device-info-card dl div{display:grid;grid-template-columns:86px minmax(0,1fr);gap:10px;align-items:baseline}.device-info-card dt{color:#9f927d;font-size:12px;font-weight:900}.device-info-card dd{margin:0;color:#4d3a22;font-weight:800;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.detail-actions-card{grid-column:1 / -1;padding:18px}.detail-actions{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px;margin-top:18px}.detail-action{min-height:64px;border:2px solid rgba(212,196,168,.85);background:rgba(255,255,255,.72);border-radius:20px;padding:10px 14px;text-align:left;font-weight:900;color:#4d3a22}.detail-action:hover{border-color:#19c8b9;transform:translateY(-1px)}.detail-action span{display:block}.detail-action code{display:block;margin-top:4px;color:#8a7b66;font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.raw-action{background:#e8edff;color:#4a5a8a}.api-layout.active{display:grid;grid-template-columns:320px minmax(0,1fr);gap:16px}.api-layout:before{content:'调试接口为次级入口;日常使用请从首页卡片进入设备详情。';display:block;grid-column:1 / -1;color:#8a7b66;font-weight:800;margin-left:4px}.api-catalog,.api-console{padding:18px}.endpoint-list{margin-top:16px;display:grid;gap:12px}.endpoint-group h4{margin:0 0 8px}.endpoint-row{width:100%;border:0;border-radius:14px;background:rgba(255,255,255,.58);padding:9px 10px;text-align:left;font-size:12px}.endpoint-row span{font-weight:900;color:#6a3a9a;margin-right:6px}.console-toolbar{display:grid;grid-template-columns:110px minmax(0,1fr) 100px;gap:10px}.console-toolbar select,.console-toolbar input,.request-body{border:2px solid #d4c4a8;border-radius:14px;background:#fff;padding:10px}.request-body{width:100%;min-height:110px;margin-top:12px}.raw-output{min-height:340px;overflow:auto;background:#3a3028;color:#f8f0dc;border-radius:18px;padding:16px;white-space:pre-wrap}.rawpage-view.active{display:grid;grid-template-rows:auto minmax(0,1fr);gap:12px}.frame-toolbar{padding:14px 18px;display:flex;gap:12px;align-items:center}.frame{width:100%;height:100%;border:0;border-radius:24px;background:#fff}.empty-state{padding:28px;text-align:center}.login-card,.instance-form{width:min(560px,calc(100vw - 32px));padding:24px}.login-card input,.instance-form input,.instance-form textarea{width:100%;border:2px solid #d4c4a8;border-radius:14px;background:#fff;padding:10px;margin-top:6px}.instance-form{display:grid;gap:12px}.instance-form label{font-weight:900}.form-menu,dialog menu{display:flex;justify-content:flex-end;gap:10px;padding:0;margin:10px 0 0}.toast{position:fixed;right:20px;bottom:20px;background:#4d3a22;color:#fff;border-radius:999px;padding:12px 18px;font-weight:900;box-shadow:var(--animal-shadow-lg);z-index:20}.compact .sim-device-card{padding:14px}.compact .sim-card-kpis{grid-template-columns:repeat(3,1fr);gap:8px}.compact .device-info-card{padding:12px}.compact .device-info-card dl{gap:5px}
+1
View File
@@ -0,0 +1 @@
@media(max-width:1260px){.home-toolbar{grid-template-columns:1fr}.detail-layout{grid-template-columns:1fr}.device-detail-board{grid-column:auto;grid-row:auto}.detail-actions-card{grid-column:auto}.device-detail-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.nook-metrics{grid-template-columns:repeat(4,1fr)}}@media(max-width:900px){body{overflow:auto}.sim-shell{height:auto;padding:14px}.sim-topbar,.workspace-hero{flex-direction:column;align-items:flex-start}.sim-main{overflow:visible}.view{overflow:visible}.home-tools{grid-template-columns:1fr}.home-device-grid{overflow:visible;grid-template-columns:1fr}.api-layout.active{grid-template-columns:1fr}.nook-metrics,.device-detail-grid{grid-template-columns:1fr 1fr}.hero-actions{justify-content:flex-start}}@media(max-width:560px){.wallet-row,.signal-grid,.nook-metrics,.device-detail-grid,.sim-card-kpis{grid-template-columns:1fr}.hero-actions{width:100%}.hero-actions .animal-btn{width:100%}.console-toolbar{grid-template-columns:1fr}.device-info-card dl div{grid-template-columns:76px minmax(0,1fr)}}@media(prefers-reduced-motion:reduce){*,*:before,*:after{animation:none!important;transition:none!important}}
+10
View File
@@ -0,0 +1,10 @@
:root{
--animal-primary-color:#19c8b9;--animal-primary-color-hover:#3dd4c6;--animal-primary-color-active:#50b9ab;--animal-primary-color-bg:#e6f9f6;
--animal-success-color:#6fba2c;--animal-warning-color:#f5c31c;--animal-error-color:#e05a5a;
--animal-text-color:#794f27;--animal-text-color-body:#725d42;--animal-text-color-secondary:#9f927d;--animal-text-color-muted:#8a7b66;--animal-text-color-disabled:#c4b89e;
--animal-border-color:#aaa69d;--animal-border-color-hover:#827157;--animal-border-color-light:#e8e2d6;
--animal-bg-color:#f8f8f0;--animal-bg-color-content:rgb(247,243,223);--animal-bg-color-secondary:#f0e8d8;--animal-bg-color-disabled:#f0ece2;
--animal-font-family:Nunito,'Noto Sans SC',-apple-system,'PingFang SC','Hiragino Sans GB','Microsoft YaHei',sans-serif;
--animal-shadow-sm:0 2px 4px 0 rgba(61,52,40,.06);--animal-shadow-base:0 3px 10px 0 rgba(61,52,40,.1);--animal-shadow-lg:0 8px 24px 0 rgba(61,52,40,.14);
--animal-motion-duration-base:.25s;--animal-motion-ease:cubic-bezier(.4,0,.2,1);
}