fix(frontend): 同步 main 并修复 phase2 关闭审阅意见
This commit is contained in:
@@ -8,6 +8,7 @@ import * as workspaceService from '@/services/workspaceService'
|
||||
import * as pluginService from '@/services/pluginService'
|
||||
import type { PluginCommand, PluginCommandEffect } from '@/contracts'
|
||||
import { usePluginStore } from '@/stores/plugin'
|
||||
import { t } from '@/i18n'
|
||||
|
||||
const router = useRouter()
|
||||
const editorStore = useEditorStore()
|
||||
@@ -25,17 +26,17 @@ const selectionSnapshot = ref<string | null>(null)
|
||||
interface Command { id: string; label: string; hint: string; run: () => void | Promise<void> }
|
||||
|
||||
const builtinCommands = computed<Command[]>(() => [
|
||||
{ id: 'workspace', label: '打开工作区', hint: '导航', run: () => router.push('/workspace') },
|
||||
{ id: 'search', label: '全局搜索', hint: '导航', run: () => router.push('/search') },
|
||||
{ id: 'chat', label: '打开 AI 对话', hint: '导航', run: () => router.push('/chat') },
|
||||
{ id: 'agent', label: '创建智能体运行', hint: '导航', run: () => router.push('/agent/runs') },
|
||||
{ id: 'themes', label: '主题管理', hint: '导航', run: () => router.push('/themes') },
|
||||
{ id: 'settings', label: '打开设置', hint: '导航', run: () => router.push('/settings') },
|
||||
{ id: 'tasks', label: '任务列表', hint: '导航', run: () => router.push('/tasks') },
|
||||
{ id: 'mode', label: `切换为${editorStore.mode === 'source' ? '写作' : '源码'}模式`, hint: '编辑器', run: () => editorStore.toggleMode() },
|
||||
{ id: 'save', label: '保存当前笔记', hint: '编辑器', run: () => editorStore.save() },
|
||||
{ id: 'theme', label: `切换为${themeStore.isDark ? '浅色' : '深色'}主题`, hint: '外观', run: () => themeStore.toggleTheme() },
|
||||
{ id: 'new-note', label: '创建笔记', hint: '工作区', run: createNote },
|
||||
{ id: 'themes', label: t('主题管理', 'Manage themes'), hint: t('导航', 'Navigation'), run: () => router.push('/themes') },
|
||||
{ id: 'tasks', label: t('任务列表', 'Tasks'), hint: t('导航', 'Navigation'), run: () => router.push('/tasks') },
|
||||
{ id: 'workspace', label: t('打开工作区', 'Open workspace'), hint: t('导航', 'Navigation'), run: () => router.push('/workspace') },
|
||||
{ id: 'search', label: t('全局搜索', 'Global search'), hint: t('导航', 'Navigation'), run: () => router.push('/search') },
|
||||
{ id: 'chat', label: t('打开 AI 对话', 'Open AI chat'), hint: t('导航', 'Navigation'), run: () => router.push('/chat') },
|
||||
{ id: 'agent', label: t('创建智能体运行', 'Create agent run'), hint: t('导航', 'Navigation'), run: () => router.push('/agent/runs') },
|
||||
{ id: 'settings', label: t('打开设置', 'Open settings'), hint: t('导航', 'Navigation'), run: () => router.push('/settings') },
|
||||
{ id: 'mode', label: editorStore.mode === 'source' ? t('切换为写作模式', 'Switch to writing mode') : t('切换为源码模式', 'Switch to source mode'), hint: t('编辑器', 'Editor'), run: () => editorStore.toggleMode() },
|
||||
{ id: 'save', label: t('保存当前笔记', 'Save current note'), hint: t('编辑器', 'Editor'), run: () => editorStore.save() },
|
||||
{ id: 'theme', label: themeStore.isDark ? t('切换为浅色主题', 'Switch to light theme') : t('切换为深色主题', 'Switch to dark theme'), hint: t('外观', 'Appearance'), run: () => themeStore.toggleTheme() },
|
||||
{ id: 'new-note', label: t('创建笔记', 'Create note'), hint: t('工作区', 'Workspace'), run: createNote },
|
||||
])
|
||||
|
||||
const commands = computed<Command[]>(() => [
|
||||
@@ -81,12 +82,12 @@ async function execute(command: Command | undefined) {
|
||||
try {
|
||||
await command.run()
|
||||
} catch (error) {
|
||||
commandNotice.value = error instanceof Error ? error.message : '命令执行失败'
|
||||
commandNotice.value = error instanceof Error ? error.message : t('命令执行失败', 'Command failed')
|
||||
}
|
||||
}
|
||||
|
||||
async function createNote() {
|
||||
const rawName = window.prompt('笔记名称')?.trim()
|
||||
const rawName = window.prompt(t('笔记名称', 'Note name'))?.trim()
|
||||
if (!rawName) return
|
||||
const name = rawName.endsWith('.md') ? rawName : `${rawName}.md`
|
||||
const file = await workspaceService.createFile('/', name, `# ${rawName}\n\n`)
|
||||
@@ -100,7 +101,7 @@ async function loadPluginCommands() {
|
||||
try {
|
||||
pluginCommands.value = await pluginService.listPluginCommands('command_palette')
|
||||
} catch (error) {
|
||||
commandError.value = error instanceof Error ? error.message : 'Plugin 命令加载失败'
|
||||
commandError.value = error instanceof Error ? error.message : t('Plugin 命令加载失败', 'Failed to load plugin commands')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +113,7 @@ async function executePluginCommand(command: PluginCommand) {
|
||||
if (hasRequiredArguments(command)) {
|
||||
pluginStore.selectPlugin(command.plugin_id)
|
||||
await router.push('/extensions/plugins')
|
||||
commandNotice.value = '请在 Plugin 详情页填写参数后执行“' + command.title + '”。'
|
||||
commandNotice.value = `${t('请在 Plugin 详情页填写参数后执行', 'Enter parameters on the Plugin details page, then run')} “${command.title}”.`
|
||||
return
|
||||
}
|
||||
const result = await pluginService.executePluginCommand(command.command_id, {}, {
|
||||
@@ -138,11 +139,11 @@ async function applyPluginEffect(effect: PluginCommandEffect) {
|
||||
if (effect.type === 'refresh') {
|
||||
if (effect.payload.scope === 'plugins') await pluginStore.loadPlugins()
|
||||
if (effect.payload.scope === 'commands') await loadPluginCommands()
|
||||
commandNotice.value = '相关数据已刷新。'
|
||||
commandNotice.value = t('相关数据已刷新。', 'Related data refreshed.')
|
||||
return
|
||||
}
|
||||
if (effect.type === 'job') { commandNotice.value = '后台任务已创建:' + effect.payload.job_id; return }
|
||||
commandNotice.value = 'Plugin 命令执行完成。'
|
||||
if (effect.type === 'job') { commandNotice.value = t('后台任务已创建:', 'Background job created: ') + effect.payload.job_id; return }
|
||||
commandNotice.value = t('Plugin 命令执行完成。', 'Plugin command completed.')
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
@@ -160,20 +161,20 @@ onBeforeUnmount(() => window.removeEventListener('keydown', handleKeydown))
|
||||
|
||||
<template>
|
||||
<div v-if="commandNotice" class="command-toast" role="status">
|
||||
<span>{{ commandNotice }}</span><button aria-label="关闭通知" @click="commandNotice = ''">×</button>
|
||||
<span>{{ commandNotice }}</span><button :aria-label="t('关闭通知', 'Close notification')" @click="commandNotice = ''">×</button>
|
||||
</div>
|
||||
<Teleport to="body">
|
||||
<div v-if="open" class="command-backdrop" @click.self="hide">
|
||||
<section class="command-palette" role="dialog" aria-modal="true" aria-label="命令面板">
|
||||
<input ref="input" v-model="query" class="command-input" placeholder="输入命令…" @keydown.enter.prevent="execute(filteredCommands[0])" />
|
||||
<section class="command-palette" role="dialog" aria-modal="true" :aria-label="t('命令面板', 'Command palette')">
|
||||
<input ref="input" v-model="query" class="command-input" :placeholder="t('输入命令…', 'Enter a command…')" @keydown.enter.prevent="execute(filteredCommands[0])" />
|
||||
<p v-if="commandError" class="command-error">{{ commandError }}</p>
|
||||
<div class="command-list">
|
||||
<button v-for="command in filteredCommands" :key="command.id" type="button" @click="execute(command)">
|
||||
<span>{{ command.label }}</span><small>{{ command.hint }}</small>
|
||||
</button>
|
||||
<p v-if="!filteredCommands.length">没有匹配的命令</p>
|
||||
<p v-if="!filteredCommands.length">{{ t('没有匹配的命令', 'No matching commands') }}</p>
|
||||
</div>
|
||||
<footer><span>Enter 执行</span><span>Esc 关闭</span></footer>
|
||||
<footer><span>Enter · {{ t('执行', 'Run') }}</span><span>Esc · {{ t('关闭', 'Close') }}</span></footer>
|
||||
</section>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import FilePicker from './FilePicker.vue'
|
||||
|
||||
describe('FilePicker', () => {
|
||||
it('keeps the native file input accessible and reports the selected file', async () => {
|
||||
const wrapper = mount(FilePicker, {
|
||||
props: { file: null, label: '选择文件', emptyLabel: '尚未选择文件', accept: '.json' },
|
||||
})
|
||||
const input = wrapper.get('input[type="file"]')
|
||||
const file = new File(['{}'], 'rules.json', { type: 'application/json' })
|
||||
Object.defineProperty(input.element, 'files', { value: [file], configurable: true })
|
||||
|
||||
await input.trigger('change')
|
||||
|
||||
expect(wrapper.emitted('select')).toEqual([[file]])
|
||||
expect(wrapper.get('label').attributes('for')).toBe(input.attributes('id'))
|
||||
expect(wrapper.text()).toContain('尚未选择文件')
|
||||
|
||||
await wrapper.setProps({ file })
|
||||
expect(wrapper.text()).toContain('rules.json')
|
||||
})
|
||||
|
||||
it('emits null when the native selection is cleared', async () => {
|
||||
const wrapper = mount(FilePicker, {
|
||||
props: { file: null, label: '选择文件', emptyLabel: '尚未选择文件' },
|
||||
})
|
||||
const input = wrapper.get('input[type="file"]')
|
||||
Object.defineProperty(input.element, 'files', { value: [], configurable: true })
|
||||
|
||||
await input.trigger('change')
|
||||
|
||||
expect(wrapper.emitted('select')).toEqual([[null]])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
import { useId } from 'vue'
|
||||
import { Upload } from '@element-plus/icons-vue'
|
||||
|
||||
defineProps<{
|
||||
file: File | null
|
||||
label: string
|
||||
emptyLabel: string
|
||||
accept?: string
|
||||
disabled?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{ select: [file: File | null] }>()
|
||||
const inputId = useId()
|
||||
|
||||
function selectFile(event: Event) {
|
||||
emit('select', (event.target as HTMLInputElement).files?.[0] ?? null)
|
||||
}
|
||||
|
||||
function allowReselect(event: MouseEvent) {
|
||||
;(event.currentTarget as HTMLInputElement).value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="file-picker" :class="{ disabled }">
|
||||
<input
|
||||
:id="inputId"
|
||||
class="file-picker-input"
|
||||
type="file"
|
||||
:accept="accept"
|
||||
:disabled="disabled"
|
||||
@click="allowReselect"
|
||||
@change="selectFile"
|
||||
/>
|
||||
<label class="file-picker-trigger" :for="inputId">
|
||||
<Upload aria-hidden="true" />
|
||||
<span>{{ label }}</span>
|
||||
</label>
|
||||
<span class="file-picker-name" :class="{ empty: !file }" :title="file?.name || emptyLabel">
|
||||
{{ file?.name || emptyLabel }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.file-picker { display: flex; min-width: 0; align-items: center; gap: var(--space-sm); }
|
||||
.file-picker-input { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }
|
||||
.file-picker-trigger { display: inline-flex; min-height: 36px; flex: 0 0 auto; align-items: center; gap: var(--space-sm); padding: 0 var(--space-md); border: 1px solid var(--color-border-default); border-radius: var(--radius-md); background: var(--color-surface-primary); font-weight: 600; cursor: pointer; transition: border-color var(--motion-fast), background-color var(--motion-fast), color var(--motion-fast), box-shadow var(--motion-fast), transform var(--motion-fast); }
|
||||
.file-picker-trigger svg { width: 16px; height: 16px; }
|
||||
.file-picker-trigger:hover { border-color: var(--color-accent-secondary); background: var(--color-background-hover); color: var(--color-accent-primary); transform: translateY(-1px); }
|
||||
.file-picker-input:focus-visible + .file-picker-trigger { outline: 2px solid var(--color-border-focus); outline-offset: 2px; }
|
||||
.file-picker-name { min-width: 0; overflow: hidden; color: var(--color-text-secondary); text-overflow: ellipsis; white-space: nowrap; user-select: text; }
|
||||
.file-picker-name.empty { color: var(--color-text-tertiary); }
|
||||
.disabled { opacity: .55; }
|
||||
.disabled .file-picker-trigger { cursor: not-allowed; transform: none; }
|
||||
@media (max-width: 560px) { .file-picker { align-items: stretch; flex-direction: column; } .file-picker-trigger { justify-content: center; } }
|
||||
</style>
|
||||
@@ -3,24 +3,25 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
import { computed, ref } from 'vue'
|
||||
import { ArrowLeftBold, ArrowRightBold, Brush, ChatDotRound, CircleCheck, Connection, Cpu, FolderOpened, Lightning, Monitor, Search, Setting } from '@element-plus/icons-vue'
|
||||
import AppIcon from './AppIcon.vue'
|
||||
import { t } from '@/i18n'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const expanded = ref(localStorage.getItem('primary-sidebar-expanded') === 'true')
|
||||
|
||||
const navItems = [
|
||||
{ name: 'workspace', icon: FolderOpened, label: '工作区' },
|
||||
{ name: 'search', icon: Search, label: '搜索' },
|
||||
{ name: 'chat', icon: ChatDotRound, label: 'AI 对话' },
|
||||
{ name: 'agent', icon: Cpu, label: '智能体' },
|
||||
{ name: 'tasks', icon: CircleCheck, label: '任务' },
|
||||
{ name: 'media', icon: Monitor, label: '音视频' },
|
||||
const navItems = computed(() => [
|
||||
{ name: 'workspace', icon: FolderOpened, label: t('工作区', 'Workspace') },
|
||||
{ name: 'search', icon: Search, label: t('搜索', 'Search') },
|
||||
{ name: 'chat', icon: ChatDotRound, label: t('AI 对话', 'AI Chat') },
|
||||
{ name: 'agent', icon: Cpu, label: t('智能体', 'Agent') },
|
||||
{ name: 'tasks', icon: CircleCheck, label: t('任务', 'Tasks') },
|
||||
{ name: 'media', icon: Monitor, label: t('音视频', 'Media') },
|
||||
{ name: 'skills', icon: Lightning, label: 'Skill' },
|
||||
{ name: 'plugins', icon: Connection, label: 'Plugin' },
|
||||
{ name: 'mcp-servers', icon: Monitor, label: 'MCP' },
|
||||
{ name: 'themes', icon: Brush, label: '主题' },
|
||||
{ name: 'settings', icon: Setting, label: '设置' },
|
||||
]
|
||||
{ name: 'themes', icon: Brush, label: t('主题', 'Themes') },
|
||||
{ name: 'settings', icon: Setting, label: t('设置', 'Settings') },
|
||||
])
|
||||
|
||||
const currentName = computed(() => {
|
||||
return route.name as string
|
||||
@@ -52,9 +53,9 @@ function toggleExpanded() {
|
||||
</div>
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<button class="nav-item collapse-button" type="button" :title="expanded ? '收起导航' : '展开导航'" @click="toggleExpanded">
|
||||
<button class="nav-item collapse-button" type="button" :title="expanded ? t('收起导航', 'Collapse navigation') : t('展开导航', 'Expand navigation')" @click="toggleExpanded">
|
||||
<AppIcon class="nav-icon" :icon="expanded ? ArrowLeftBold : ArrowRightBold" />
|
||||
<span class="nav-label">{{ expanded ? '收起' : '展开' }}</span>
|
||||
<span class="nav-label">{{ expanded ? t('收起', 'Collapse') : t('展开', 'Expand') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -7,6 +7,7 @@ import SearchFiltersPanel from '@/features/search/SearchFiltersPanel.vue'
|
||||
import TaskFiltersPanel from '@/features/tasks/TaskFiltersPanel.vue'
|
||||
import ExtensionListPanel from '@/components/common/ExtensionListPanel.vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { t } from '@/i18n'
|
||||
|
||||
const props = defineProps<{
|
||||
component: string | null
|
||||
@@ -17,12 +18,12 @@ const routeName = computed(() => route.name as string)
|
||||
|
||||
const sidebarTitle = computed(() => {
|
||||
const titles: Record<string, string> = {
|
||||
'file-tree': '文件',
|
||||
'conversation-list': '对话',
|
||||
'run-list': '智能体运行',
|
||||
'search-filters': '搜索筛选',
|
||||
'task-filters': '任务筛选',
|
||||
'extension-list': '扩展',
|
||||
'file-tree': t('文件', 'Files'),
|
||||
'conversation-list': t('对话', 'Conversations'),
|
||||
'run-list': t('智能体运行', 'Agent Runs'),
|
||||
'search-filters': t('搜索筛选', 'Search Filters'),
|
||||
'task-filters': t('任务筛选', 'Task Filters'),
|
||||
'extension-list': t('扩展', 'Extensions'),
|
||||
}
|
||||
return titles[props.component || ''] || ''
|
||||
})
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useSettingsStore } from '@/stores/settings'
|
||||
import { useProviderStore } from '@/stores/provider'
|
||||
import { useAgentStore } from '@/stores/agent'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { t } from '@/i18n'
|
||||
|
||||
const editorStore = useEditorStore()
|
||||
const settingsStore = useSettingsStore()
|
||||
@@ -15,12 +16,12 @@ const route = useRoute()
|
||||
const saveStatusText = computed(() => {
|
||||
const map: Record<string, string> = {
|
||||
idle: '',
|
||||
dirty: '未保存',
|
||||
saving: '保存中...',
|
||||
saved: '已保存',
|
||||
save_failed: '保存失败',
|
||||
external_changed: '外部已更新',
|
||||
conflict: '存在冲突',
|
||||
dirty: t('未保存', 'Unsaved'),
|
||||
saving: t('保存中...', 'Saving...'),
|
||||
saved: t('已保存', 'Saved'),
|
||||
save_failed: t('保存失败', 'Save failed'),
|
||||
external_changed: t('外部已更新', 'Changed externally'),
|
||||
conflict: t('存在冲突', 'Conflict'),
|
||||
}
|
||||
return map[editorStore.saveStatus] || ''
|
||||
})
|
||||
@@ -39,16 +40,16 @@ const saveStatusColor = computed(() => {
|
||||
|
||||
const indexStatusText = computed(() => {
|
||||
const s = settingsStore.indexStatus.status
|
||||
return s === 'unknown' ? '索引状态未获取' : s === 'idle' ? '索引就绪' : s === 'indexing' ? `索引中 (${settingsStore.indexStatus.pending_jobs})` : '索引错误'
|
||||
return s === 'unknown' ? t('索引状态未获取', 'Index status unavailable') : s === 'idle' ? t('索引就绪', 'Index ready') : s === 'indexing' ? `${t('索引中', 'Indexing')} (${settingsStore.indexStatus.pending_jobs})` : t('索引错误', 'Index error')
|
||||
})
|
||||
|
||||
const aiCoreStatusText = computed(() => {
|
||||
const map: Record<string, string> = {
|
||||
unknown: 'AI Core 状态未获取',
|
||||
starting: 'AI Core 启动中',
|
||||
running: 'AI Core 运行中',
|
||||
stopped: 'AI Core 已停止',
|
||||
error: 'AI Core 错误',
|
||||
unknown: t('AI Core 状态未获取', 'AI Core status unavailable'),
|
||||
starting: t('AI Core 启动中', 'AI Core starting'),
|
||||
running: t('AI Core 运行中', 'AI Core running'),
|
||||
stopped: t('AI Core 已停止', 'AI Core stopped'),
|
||||
error: t('AI Core 错误', 'AI Core error'),
|
||||
}
|
||||
return map[settingsStore.aiCoreStatus] || ''
|
||||
})
|
||||
@@ -85,7 +86,7 @@ const showEditorInfo = computed(() => route.name === 'workspace')
|
||||
</span>
|
||||
<span v-if="agentStore.isRunning" class="status-item agent-status">
|
||||
<span class="spinner" />
|
||||
智能体运行中
|
||||
{{ t('智能体运行中', 'Agent running') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="statusbar-right">
|
||||
@@ -93,10 +94,10 @@ const showEditorInfo = computed(() => route.name === 'workspace')
|
||||
{{ defaultProvider.name }} · {{ defaultProvider.default_model }}
|
||||
</span>
|
||||
<span v-if="showEditorInfo" class="status-item">
|
||||
{{ editorStore.lineCount }} 行
|
||||
{{ editorStore.lineCount }} {{ t('行', 'lines') }}
|
||||
</span>
|
||||
<span v-if="showEditorInfo" class="status-item">
|
||||
{{ editorStore.wordCount }} 字
|
||||
{{ editorStore.wordCount }} {{ t('字', 'words') }}
|
||||
</span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useEditorStore } from '@/stores/editor'
|
||||
import { useThemeStore } from '@/stores/theme'
|
||||
import { Moon, Sunny } from '@element-plus/icons-vue'
|
||||
import AppIcon from './AppIcon.vue'
|
||||
import { t } from '@/i18n'
|
||||
|
||||
const route = useRoute()
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
@@ -15,15 +16,15 @@ const themeStore = useThemeStore()
|
||||
const pageTitle = computed(() => {
|
||||
const name = route.name as string
|
||||
const titles: Record<string, string> = {
|
||||
workspace: '工作区',
|
||||
search: '搜索',
|
||||
chat: 'AI 对话',
|
||||
agent: '智能体执行轨迹',
|
||||
tasks: '任务',
|
||||
skills: 'Skill 管理',
|
||||
plugins: 'Plugin 与 MCP',
|
||||
themes: '主题管理',
|
||||
settings: '设置',
|
||||
workspace: t('工作区', 'Workspace'),
|
||||
search: t('搜索', 'Search'),
|
||||
chat: t('AI 对话', 'AI Chat'),
|
||||
agent: t('智能体执行轨迹', 'Agent Trace'),
|
||||
tasks: t('任务', 'Tasks'),
|
||||
skills: t('Skill 管理', 'Skill Management'),
|
||||
plugins: t('Plugin 与 MCP', 'Plugins and MCP'),
|
||||
themes: t('主题管理', 'Theme Management'),
|
||||
settings: t('设置', 'Settings'),
|
||||
}
|
||||
return titles[name] || 'NotesAgent'
|
||||
})
|
||||
@@ -53,7 +54,7 @@ const isDirty = computed(() => editorStore.saveStatus === 'dirty' || editorStore
|
||||
<span class="app-name">NotesAgent</span>
|
||||
</div>
|
||||
<div class="titlebar-right">
|
||||
<button class="icon-btn" @click="themeStore.toggleTheme()" :title="themeStore.isDark ? '切换浅色主题' : '切换深色主题'">
|
||||
<button class="icon-btn" @click="themeStore.toggleTheme()" :title="themeStore.isDark ? t('切换浅色主题', 'Switch to light theme') : t('切换深色主题', 'Switch to dark theme')">
|
||||
<AppIcon :icon="themeStore.isDark ? Sunny : Moon" :size="16" />
|
||||
</button>
|
||||
<div class="window-controls">
|
||||
|
||||
Reference in New Issue
Block a user