mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-30 04:40:18 +08:00
refactor(hermes): emoji → SVG 图标,统一视觉语言
## 问题 新加的 hermes 页面里到处是 emoji(⚠️📁📋💬🔐🔗🖼️📝⚙️📦💬🔊🎙🔍✓ 等), 不同 OS 渲染样式差异大(macOS Apple Color Emoji vs Windows Segoe UI Emoji vs Linux Noto Color Emoji),看着很不专业,也跟现有 SVG 图标系统割裂。 ## 方案 新建 src/engines/hermes/lib/svg-icons.js — 集中所有需要的 SVG path: 状态:alert-triangle, check, x, check-circle, x-circle, info 文件:folder, folder-up, file, file-text, image, link-2, settings 列表:clipboard-list, message-square, inbox 安全:lock, shield, key 媒体:volume, mic, search 导出 svgIcon(name, opts) — 渲染时统一 viewBox / stroke-width / currentColor, size 可选覆盖。 ## 替换覆盖(7 个页面 + 2 处 CSS) - profiles.js: ⚠️ → alert-triangle, 📁 → folder - kanban.js: ⚠️ → alert-triangle, 📋 → clipboard-list, 💬 → message-square - oauth.js: ⚠️ → alert-triangle, 🔐 → lock - group-chat.js: ⚠️ → alert-triangle - logs.js: ⚠️ → 纯文本 [ERROR](log raw 字段不需要图标) - files.js: 📁🔗🖼️📝⚙️📄 全部 → folder/link-2/image/file-text/settings/file ".." → folder-up - lazy-deps.js: 7 个 category emoji → message-square/volume/mic/search/ shield/inbox/image ✓ 装好标识 → check svg 📦 empty → inbox CSS 适配: - .page-inline-error-icon: 加 inline-flex + 自动 currentColor,svg 20×20 - .empty-state .empty-icon: 加 inline-flex + svg 跟 font-size 走(1em) ## 范围控制 本次只处理我最近 3 个 commit 里新加的页面 emoji。 历史代码(setup.js / services.js / chat.js 的 ✅❌✓ 等)暂不动 — 那是 跨工作流的复杂改动,单独评估再做,避免连锁影响 CI。 ## 验证 ✓ npm run build PASS(1.80s)
This commit is contained in:
@@ -15,6 +15,7 @@ import { api } from '../../../lib/tauri-api.js'
|
||||
import { toast } from '../../../components/toast.js'
|
||||
import { showConfirm } from '../../../components/modal.js'
|
||||
import { humanizeError } from '../../../lib/humanize-error.js'
|
||||
import { svgIcon } from '../lib/svg-icons.js'
|
||||
|
||||
function escHtml(s) {
|
||||
return String(s ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
||||
@@ -35,14 +36,13 @@ function formatTime(secs) {
|
||||
}
|
||||
|
||||
function iconForKind(kind, name) {
|
||||
if (kind === 'dir') return '📁'
|
||||
if (kind === 'symlink') return '🔗'
|
||||
if (kind === 'dir') return svgIcon('folder', { size: 14 })
|
||||
if (kind === 'symlink') return svgIcon('link-2', { size: 14 })
|
||||
const ext = (name.split('.').pop() || '').toLowerCase()
|
||||
if (['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'].includes(ext)) return '🖼️'
|
||||
if (['md', 'txt'].includes(ext)) return '📝'
|
||||
if (['json', 'yaml', 'yml', 'toml'].includes(ext)) return '⚙️'
|
||||
if (['py', 'js', 'ts', 'rs', 'go'].includes(ext)) return '📄'
|
||||
return '📄'
|
||||
if (['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'].includes(ext)) return svgIcon('image', { size: 14 })
|
||||
if (['md', 'txt'].includes(ext)) return svgIcon('file-text', { size: 14 })
|
||||
if (['json', 'yaml', 'yml', 'toml'].includes(ext)) return svgIcon('settings', { size: 14 })
|
||||
return svgIcon('file', { size: 14 })
|
||||
}
|
||||
|
||||
export function render() {
|
||||
@@ -118,7 +118,7 @@ export function render() {
|
||||
}
|
||||
return `
|
||||
<div class="hm-files-list">
|
||||
${currentDir ? `<div class="hm-files-entry hm-files-entry--up" data-cd="${escAttr(parentDir(currentDir))}"><span class="hm-files-icon">📁</span><span class="hm-files-name">..</span></div>` : ''}
|
||||
${currentDir ? `<div class="hm-files-entry hm-files-entry--up" data-cd="${escAttr(parentDir(currentDir))}"><span class="hm-files-icon">${svgIcon('folder-up', { size: 14 })}</span><span class="hm-files-name">..</span></div>` : ''}
|
||||
${entries.map(e => `
|
||||
<div class="hm-files-entry ${e.kind === 'dir' ? 'is-dir' : 'is-file'} ${selectedRel === joinRel(currentDir, e.name) ? 'is-selected' : ''}"
|
||||
data-kind="${escAttr(e.kind)}"
|
||||
|
||||
@@ -15,6 +15,7 @@ import { t } from '../../../lib/i18n.js'
|
||||
import { api, isTauriRuntime } from '../../../lib/tauri-api.js'
|
||||
import { toast } from '../../../components/toast.js'
|
||||
import { humanizeError } from '../../../lib/humanize-error.js'
|
||||
import { svgIcon } from '../lib/svg-icons.js'
|
||||
|
||||
function escHtml(s) {
|
||||
return String(s ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
||||
@@ -131,7 +132,7 @@ export function render() {
|
||||
if (m.error) {
|
||||
return `
|
||||
<div class="hm-gc-msg hm-gc-msg--assistant hm-gc-msg--error">
|
||||
<div class="hm-gc-msg-meta">${fromTag} <span style="color:var(--error)">⚠️ ${escHtml(t('engine.hermesGroupChatRunFailed'))}</span></div>
|
||||
<div class="hm-gc-msg-meta">${fromTag} <span style="color:var(--error);display:inline-flex;align-items:center;gap:4px">${svgIcon('alert-triangle', { size: 12 })} ${escHtml(t('engine.hermesGroupChatRunFailed'))}</span></div>
|
||||
<div class="hm-gc-msg-bubble" style="color:var(--error)">${escHtml(m.error)}</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
@@ -18,6 +18,7 @@ import { api } from '../../../lib/tauri-api.js'
|
||||
import { toast } from '../../../components/toast.js'
|
||||
import { showModal, showContentModal } from '../../../components/modal.js'
|
||||
import { humanizeError } from '../../../lib/humanize-error.js'
|
||||
import { svgIcon } from '../lib/svg-icons.js'
|
||||
|
||||
const KANBAN_BASE = '/api/plugins/kanban'
|
||||
|
||||
@@ -30,7 +31,7 @@ function renderInlineError(err) {
|
||||
const h = humanizeError(err, t('engine.hermesKanbanTaskLoadFailed'))
|
||||
return `
|
||||
<div class="page-inline-error">
|
||||
<div class="page-inline-error-icon">⚠️</div>
|
||||
<div class="page-inline-error-icon">${svgIcon('alert-triangle', { size: 20 })}</div>
|
||||
<div class="page-inline-error-body">
|
||||
<div class="page-inline-error-message">${escHtml(h.message)}</div>
|
||||
${h.hint ? `<div class="page-inline-error-hint">${escHtml(h.hint)}</div>` : ''}
|
||||
@@ -77,7 +78,7 @@ export function render() {
|
||||
|
||||
function renderBoard() {
|
||||
if (!board?.columns?.length) {
|
||||
return `<div class="empty-state empty-compact"><div class="empty-icon">📋</div><div class="empty-title">${escHtml(t('engine.hermesKanbanEmpty'))}</div></div>`
|
||||
return `<div class="empty-state empty-compact"><div class="empty-icon">${svgIcon('clipboard-list', { size: 32 })}</div><div class="empty-title">${escHtml(t('engine.hermesKanbanEmpty'))}</div></div>`
|
||||
}
|
||||
return `
|
||||
<div class="hm-kanban-board">
|
||||
@@ -107,7 +108,7 @@ export function render() {
|
||||
<div class="hm-kanban-task-meta">
|
||||
${priorityBadge}
|
||||
${assignee}
|
||||
${task.comment_count ? `<span class="hm-kanban-task-meta-item">💬 ${task.comment_count}</span>` : ''}
|
||||
${task.comment_count ? `<span class="hm-kanban-task-meta-item">${svgIcon('message-square', { size: 12 })} ${task.comment_count}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
@@ -11,16 +11,17 @@ import { t } from '../../../lib/i18n.js'
|
||||
import { api } from '../../../lib/tauri-api.js'
|
||||
import { toast } from '../../../components/toast.js'
|
||||
import { humanizeError } from '../../../lib/humanize-error.js'
|
||||
import { svgIcon } from '../lib/svg-icons.js'
|
||||
|
||||
// feature 分类配置(决定分组顺序 + 图标 + 文案)
|
||||
const CATEGORIES = [
|
||||
{ prefix: 'platform.', emoji: '💬', titleKey: 'hermesLazyDeps.catPlatform' },
|
||||
{ prefix: 'tts.', emoji: '🔊', titleKey: 'hermesLazyDeps.catTts' },
|
||||
{ prefix: 'stt.', emoji: '🎙️', titleKey: 'hermesLazyDeps.catStt' },
|
||||
{ prefix: 'search.', emoji: '🔍', titleKey: 'hermesLazyDeps.catSearch' },
|
||||
{ prefix: 'provider.', emoji: '🧠', titleKey: 'hermesLazyDeps.catProvider' },
|
||||
{ prefix: 'memory.', emoji: '🗂️', titleKey: 'hermesLazyDeps.catMemory' },
|
||||
{ prefix: 'image.', emoji: '🎨', titleKey: 'hermesLazyDeps.catImage' },
|
||||
{ prefix: 'platform.', icon: 'message-square', titleKey: 'hermesLazyDeps.catPlatform' },
|
||||
{ prefix: 'tts.', icon: 'volume', titleKey: 'hermesLazyDeps.catTts' },
|
||||
{ prefix: 'stt.', icon: 'mic', titleKey: 'hermesLazyDeps.catStt' },
|
||||
{ prefix: 'search.', icon: 'search', titleKey: 'hermesLazyDeps.catSearch' },
|
||||
{ prefix: 'provider.', icon: 'shield', titleKey: 'hermesLazyDeps.catProvider' },
|
||||
{ prefix: 'memory.', icon: 'inbox', titleKey: 'hermesLazyDeps.catMemory' },
|
||||
{ prefix: 'image.', icon: 'image', titleKey: 'hermesLazyDeps.catImage' },
|
||||
]
|
||||
|
||||
const DESC_OVERRIDE_KEY = 'hermesLazyDeps.descOverride' // i18n.key 下的 feature → 描述
|
||||
@@ -28,7 +29,7 @@ const DESC_OVERRIDE_KEY = 'hermesLazyDeps.descOverride' // i18n.key 下的 feat
|
||||
// 把 feature 按分类分组
|
||||
function groupByCategory(features) {
|
||||
const groups = CATEGORIES.map(c => ({ ...c, items: [] }))
|
||||
const other = { prefix: '', emoji: '🧩', titleKey: 'hermesLazyDeps.catOther', items: [] }
|
||||
const other = { prefix: '', icon: 'file', titleKey: 'hermesLazyDeps.catOther', items: [] }
|
||||
for (const f of features) {
|
||||
const cat = groups.find(g => f.feature.startsWith(g.prefix))
|
||||
if (cat) cat.items.push(f)
|
||||
@@ -83,7 +84,7 @@ async function loadAndRender(page) {
|
||||
const features = featuresResp.features || []
|
||||
if (!features.length) {
|
||||
content.innerHTML = `<div class="empty-state empty-compact">
|
||||
<div class="empty-icon">📦</div>
|
||||
<div class="empty-icon">${svgIcon('inbox', { size: 32 })}</div>
|
||||
<div class="empty-title">${escapeHtml(t('hermesLazyDeps.emptyTitle'))}</div>
|
||||
</div>`
|
||||
return
|
||||
@@ -113,7 +114,7 @@ function renderGroup(group, status) {
|
||||
return `
|
||||
<div class="config-section">
|
||||
<div class="config-section-title">
|
||||
<span style="font-size:18px;line-height:1">${group.emoji}</span>
|
||||
<span style="display:inline-flex;align-items:center;color:var(--accent);margin-right:8px">${svgIcon(group.icon, { size: 18 })}</span>
|
||||
${escapeHtml(t(group.titleKey))}
|
||||
</div>
|
||||
<div class="lazy-deps-grid">
|
||||
@@ -130,7 +131,7 @@ function renderItem(f, st) {
|
||||
const specsTitle = (f.specs || []).join('\n')
|
||||
const featureLabel = featureDisplayName(f.feature)
|
||||
const stateBadge = satisfied
|
||||
? `<span class="lazy-deps-badge ok">✓ ${escapeHtml(t('hermesLazyDeps.installed'))}</span>`
|
||||
? `<span class="lazy-deps-badge ok">${svgIcon('check', { size: 11 })} ${escapeHtml(t('hermesLazyDeps.installed'))}</span>`
|
||||
: (known
|
||||
? `<span class="lazy-deps-badge warn">${escapeHtml(t('hermesLazyDeps.notInstalled'))}</span>`
|
||||
: `<span class="lazy-deps-badge unknown">?</span>`)
|
||||
|
||||
@@ -117,7 +117,7 @@ export function render() {
|
||||
levelFilter !== 'ALL' ? levelFilter : null,
|
||||
)
|
||||
} catch (e) {
|
||||
entries = [{ raw: `⚠️ ${t('engine.logsLoadFailed')}: ${e.message || e}` }]
|
||||
entries = [{ raw: `[ERROR] ${t('engine.logsLoadFailed')}: ${e.message || e}`, level: 'ERROR' }]
|
||||
}
|
||||
loading = false
|
||||
draw()
|
||||
|
||||
@@ -15,6 +15,7 @@ import { api } from '../../../lib/tauri-api.js'
|
||||
import { toast } from '../../../components/toast.js'
|
||||
import { showModal, showContentModal } from '../../../components/modal.js'
|
||||
import { humanizeError } from '../../../lib/humanize-error.js'
|
||||
import { svgIcon } from '../lib/svg-icons.js'
|
||||
|
||||
const OAUTH_BASE = '/api/providers/oauth'
|
||||
|
||||
@@ -27,7 +28,7 @@ function renderInlineError(err) {
|
||||
const h = humanizeError(err, t('engine.hermesOAuthTitle'))
|
||||
return `
|
||||
<div class="page-inline-error">
|
||||
<div class="page-inline-error-icon">⚠️</div>
|
||||
<div class="page-inline-error-icon">${svgIcon('alert-triangle', { size: 20 })}</div>
|
||||
<div class="page-inline-error-body">
|
||||
<div class="page-inline-error-message">${escHtml(h.message)}</div>
|
||||
${h.hint ? `<div class="page-inline-error-hint">${escHtml(h.hint)}</div>` : ''}
|
||||
@@ -62,7 +63,7 @@ export function render() {
|
||||
${error ? renderInlineError(error) : ''}
|
||||
${(!loading && !error && !providers.length) ? `
|
||||
<div class="empty-state empty-compact">
|
||||
<div class="empty-icon">🔐</div>
|
||||
<div class="empty-icon">${svgIcon('lock', { size: 32 })}</div>
|
||||
<div class="empty-title">${escHtml(t('engine.hermesOAuthEmpty'))}</div>
|
||||
</div>` : ''}
|
||||
${(!loading && providers.length) ? `
|
||||
|
||||
@@ -16,6 +16,7 @@ import { toast } from '../../../components/toast.js'
|
||||
import { showConfirm, showModal } from '../../../components/modal.js'
|
||||
import { humanizeError } from '../../../lib/humanize-error.js'
|
||||
import { getChatStore } from '../lib/chat-store.js'
|
||||
import { svgIcon } from '../lib/svg-icons.js'
|
||||
|
||||
const chatStore = getChatStore()
|
||||
|
||||
@@ -28,7 +29,7 @@ function renderInlineError(err) {
|
||||
const h = humanizeError(err, t('engine.hermesProfilesTitle'))
|
||||
return `
|
||||
<div class="page-inline-error">
|
||||
<div class="page-inline-error-icon">⚠️</div>
|
||||
<div class="page-inline-error-icon">${svgIcon('alert-triangle', { size: 20 })}</div>
|
||||
<div class="page-inline-error-body">
|
||||
<div class="page-inline-error-message">${escHtml(h.message)}</div>
|
||||
${h.hint ? `<div class="page-inline-error-hint">${escHtml(h.hint)}</div>` : ''}
|
||||
@@ -64,7 +65,7 @@ export function render() {
|
||||
${error ? renderInlineError(error) : ''}
|
||||
${(!loading && !error && !profiles.length) ? `
|
||||
<div class="empty-state empty-compact">
|
||||
<div class="empty-icon">📁</div>
|
||||
<div class="empty-icon">${svgIcon('folder', { size: 32 })}</div>
|
||||
<div class="empty-title">${escHtml(t('engine.hermesProfilesEmpty'))}</div>
|
||||
</div>` : ''}
|
||||
${(!loading && profiles.length) ? `
|
||||
|
||||
Reference in New Issue
Block a user