feat(frontend): 统一界面图标体系

This commit is contained in:
2026-08-30 09:57:04 +08:00
parent bba3041867
commit c33610a295
14 changed files with 321 additions and 36 deletions
@@ -1,4 +1,6 @@
<script setup lang="ts">
import { Connection } from '@element-plus/icons-vue'
import AppIcon from '@/components/common/AppIcon.vue'
import { onMounted, ref } from 'vue'
import { usePluginStore } from '@/stores/plugin'
@@ -23,7 +25,7 @@ async function uninstall(id: string, name: string) { if (!confirm(`卸载“${na
<div v-if="pluginStore.selectedPlugin.last_error" class="error-banner last-error">{{ pluginStore.selectedPlugin.last_error }}</div>
<div v-if="pluginStore.selectedPlugin.dependent_skills?.length" class="notice-banner last-error">依赖此插件的 Skill{{ pluginStore.selectedPlugin.dependent_skills.join('') }}</div>
</div>
<div v-else class="feature-grid"><article v-for="plugin in pluginStore.plugins" :key="plugin.plugin_id" class="item-card extension-card" @click="pluginStore.selectPlugin(plugin.plugin_id)"><div class="extension-title"><span class="icon">{{ plugin.icon || '🧩' }}</span><div><strong>{{ plugin.name }}</strong><p>v{{ plugin.version }}</p></div><span class="badge" :class="{ success: plugin.status === 'ready', error: plugin.status === 'error', warning: plugin.status === 'permission_required' }">{{ plugin.status }}</span></div><p class="muted">{{ plugin.description }}</p><p class="subtle">{{ plugin.permissions.length }} 项权限 · {{ plugin.contributions.length }} Contribution</p></article></div>
<div v-else class="feature-grid"><article v-for="plugin in pluginStore.plugins" :key="plugin.plugin_id" class="item-card extension-card" @click="pluginStore.selectPlugin(plugin.plugin_id)"><div class="extension-title"><AppIcon :icon="Connection" :size="22" /><div><strong>{{ plugin.name }}</strong><p>v{{ plugin.version }}</p></div><span class="badge" :class="{ success: plugin.status === 'ready', error: plugin.status === 'error', warning: plugin.status === 'permission_required' }">{{ plugin.status }}</span></div><p class="muted">{{ plugin.description }}</p><p class="subtle">{{ plugin.permissions.length }} 项权限 · {{ plugin.contributions.length }} Contribution</p></article></div>
</section>
</template>
+3 -1
View File
@@ -1,4 +1,6 @@
<script setup lang="ts">
import { Lightning } from '@element-plus/icons-vue'
import AppIcon from '@/components/common/AppIcon.vue'
import { onMounted, ref } from 'vue'
import { useSkillStore } from '@/stores/skill'
@@ -30,7 +32,7 @@ async function uninstall(skillId: string, name: string) {
<div class="detail-grid"><div><h3>工具</h3><div class="tag-list"><span v-for="tool in skillStore.selectedSkill.tools" :key="tool" class="badge info">{{ tool }}</span></div></div><div><h3>权限</h3><div class="tag-list"><span v-for="permission in skillStore.selectedSkill.permissions" :key="permission" class="badge warning">{{ permission }}</span></div></div><div><h3>检索配置</h3><pre>{{ JSON.stringify(skillStore.selectedSkill.retrieval_config, null, 2) }}</pre></div><div><h3>模型能力</h3><div class="tag-list"><span v-for="cap in skillStore.selectedSkill.model_requirements?.capabilities" :key="cap" class="badge">{{ cap }}</span></div></div></div>
<div v-if="skillStore.selectedSkill.missing_dependencies?.length" class="error-banner dependencies">缺失依赖{{ skillStore.selectedSkill.missing_dependencies.join('') }}</div>
</div>
<div v-else class="feature-grid"><article v-for="skill in skillStore.skills" :key="skill.skill_id" class="item-card extension-card" @click="skillStore.selectSkill(skill.skill_id)"><div class="extension-title"><span class="icon">{{ skill.icon || '⚡' }}</span><div><strong>{{ skill.name }}</strong><p>v{{ skill.version }}</p></div><span class="badge" :class="{ success: skill.status === 'ready', warning: skill.status === 'dependency_missing' }">{{ skill.status }}</span></div><p class="muted">{{ skill.description }}</p><div class="tag-list"><span v-for="permission in skill.permissions.slice(0, 3)" :key="permission" class="badge">{{ permission }}</span></div></article></div>
<div v-else class="feature-grid"><article v-for="skill in skillStore.skills" :key="skill.skill_id" class="item-card extension-card" @click="skillStore.selectSkill(skill.skill_id)"><div class="extension-title"><AppIcon :icon="Lightning" :size="22" /><div><strong>{{ skill.name }}</strong><p>v{{ skill.version }}</p></div><span class="badge" :class="{ success: skill.status === 'ready', warning: skill.status === 'dependency_missing' }">{{ skill.status }}</span></div><p class="muted">{{ skill.description }}</p><div class="tag-list"><span v-for="permission in skill.permissions.slice(0, 3)" :key="permission" class="badge">{{ permission }}</span></div></article></div>
</section>
</template>
+12 -6
View File
@@ -4,6 +4,8 @@ import { useRouter } from 'vue-router'
import { useWorkspaceStore } from '@/stores/workspace'
import { useThemeStore } from '@/stores/theme'
import { useSettingsStore } from '@/stores/settings'
import { ArrowRight, Document, Folder, FolderOpened, Moon, Plus, Sunny } from '@element-plus/icons-vue'
import AppIcon from '@/components/common/AppIcon.vue'
const router = useRouter()
const workspaceStore = useWorkspaceStore()
@@ -65,7 +67,7 @@ async function createVault() {
<div class="bg-decoration" />
<div class="entry-container">
<div class="brand-section">
<div class="logo">📝</div>
<div class="logo"><AppIcon :icon="Document" :size="56" /></div>
<h1 class="app-title">知笔知己</h1>
<p class="app-subtitle">本地优先的 AI 笔记软件</p>
</div>
@@ -84,22 +86,22 @@ async function createVault() {
@click="openVault(vault.path)"
:disabled="isLoading"
>
<span class="vault-icon">📁</span>
<AppIcon class="vault-icon" :icon="Folder" :size="20" />
<div class="vault-info">
<div class="vault-name">{{ vault.name }}</div>
<div class="vault-path">{{ vault.path }}</div>
</div>
<span class="vault-arrow"></span>
<AppIcon class="vault-arrow" :icon="ArrowRight" :size="16" />
</button>
</div>
</div>
<div class="actions">
<button class="btn btn-primary" @click="openFolderPicker" :disabled="isLoading">
<span>📂</span> 打开本地 Vault
<AppIcon :icon="FolderOpened" /> 打开本地 Vault
</button>
<button class="btn btn-secondary" @click="showCreateDialog = true" :disabled="isLoading">
<span></span> 创建新 Vault
<AppIcon :icon="Plus" /> 创建新 Vault
</button>
</div>
@@ -114,7 +116,8 @@ async function createVault() {
<div class="footer-info">
<span>v0.1.0</span>
<button class="theme-toggle" @click="themeStore.toggleTheme()">
{{ themeStore.isDark ? ' 浅色' : '🌙 深色' }}
<AppIcon :icon="themeStore.isDark ? Sunny : Moon" :size="15" />
{{ themeStore.isDark ? '浅色' : '深色' }}
</button>
</div>
</div>
@@ -374,6 +377,9 @@ async function createVault() {
}
.theme-toggle {
display: inline-flex;
align-items: center;
gap: var(--space-xs);
color: var(--color-text-secondary);
background: none;
border: none;
@@ -1,5 +1,7 @@
<script setup lang="ts">
import type { FileNode } from '@/contracts'
import { Document, Folder, FolderOpened } from '@element-plus/icons-vue'
import AppIcon from '@/components/common/AppIcon.vue'
defineProps<{ node: FileNode; activePath: string | null }>()
const emit = defineEmits<{
@@ -12,7 +14,7 @@ const emit = defineEmits<{
<div>
<div class="tree-node" :class="{ active: node.path === activePath }"
@click="emit('open', node)" @contextmenu="emit('contextMenu', $event, node)">
<span>{{ node.type === 'folder' ? (node.is_open ? '📂' : '📁') : '📄' }}</span>
<AppIcon :icon="node.type === 'folder' ? (node.is_open ? FolderOpened : Folder) : Document" :size="16" />
<span class="name">{{ node.name }}</span>
<span v-if="node.is_dirty"></span>
</div>
@@ -6,6 +6,8 @@ import * as workspaceService from '@/services/workspaceService'
import { useEditorStore } from '@/stores/editor'
import { useWorkspaceStore } from '@/stores/workspace'
import FileTreeNode from './FileTreeNode.vue'
import { DocumentAdd, FolderAdd } from '@element-plus/icons-vue'
import AppIcon from '@/components/common/AppIcon.vue'
const workspaceStore = useWorkspaceStore()
const editorStore = useEditorStore()
@@ -90,8 +92,8 @@ async function deleteTarget() {
<template>
<section class="file-tree-panel" @click="closeContextMenu">
<div class="toolbar">
<button type="button" title="新建笔记" @click.stop="beginCreate('file')">📄</button>
<button type="button" title="新建文件夹" @click.stop="beginCreate('folder')">📁</button>
<button type="button" title="新建笔记" aria-label="新建笔记" @click.stop="beginCreate('file')"><AppIcon :icon="DocumentAdd" /></button>
<button type="button" title="新建文件夹" aria-label="新建文件夹" @click.stop="beginCreate('folder')"><AppIcon :icon="FolderAdd" /></button>
</div>
<form v-if="newItemType" class="new-item" @submit.prevent="createItem">
<input v-model="newItemName" :placeholder="newItemType === 'file' ? '笔记名称' : '文件夹名称'" autofocus />
@@ -4,6 +4,8 @@ import { useWorkspaceStore } from '@/stores/workspace'
import { useEditorStore } from '@/stores/editor'
import EditorHeader from '@/features/editor/EditorHeader.vue'
import EditorPane from '@/features/editor/EditorPane.vue'
import { EditPen } from '@element-plus/icons-vue'
import AppIcon from '@/components/common/AppIcon.vue'
const workspaceStore = useWorkspaceStore()
const editorStore = useEditorStore()
@@ -28,7 +30,7 @@ onMounted(() => {
</template>
<div v-else class="empty-workspace">
<div class="empty-content">
<div class="empty-icon">📝</div>
<AppIcon class="empty-icon" :icon="EditPen" :size="48" />
<h2>开始写作</h2>
<p>从左侧文件树选择笔记或创建新的笔记</p>
</div>