Files
NotesAgentic/frontend/src/features/vault/VaultEntry.vue
T

475 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, onMounted } from 'vue'
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()
const themeStore = useThemeStore()
const settingsStore = useSettingsStore()
const isLoading = ref(false)
const showCreateDialog = ref(false)
const newVaultName = ref('')
const newVaultPath = ref('')
const aiCoreStatus = ref<'checking' | 'running' | 'stopped'>('checking')
onMounted(async () => {
await Promise.all([workspaceStore.loadRecentVaults(), settingsStore.loadDiagnostics()])
const lastVaultPath = localStorage.getItem('last-vault-path')
if (settingsStore.restoreLastVault && lastVaultPath) {
await openVault(lastVaultPath)
return
}
setTimeout(() => {
aiCoreStatus.value = settingsStore.aiCoreStatus === 'running' ? 'running' : 'stopped'
}, 800)
})
async function openVault(path: string) {
isLoading.value = true
try {
await workspaceStore.openVault(path)
router.push('/workspace')
} finally {
isLoading.value = false
}
}
async function openFolderPicker() {
// In Tauri this would use the native dialog
// For web dev, simulate
const path = prompt('请输入 Vault 路径(开发模式)', '/Users/demo/Documents/MyVault')
if (path) {
await openVault(path)
}
}
async function createVault() {
if (!newVaultName.value || !newVaultPath.value) return
isLoading.value = true
try {
await workspaceStore.createVault(newVaultPath.value, newVaultName.value)
router.push('/workspace')
} finally {
isLoading.value = false
showCreateDialog.value = false
}
}
</script>
<template>
<div class="vault-entry">
<div class="bg-decoration" />
<div class="entry-container">
<div class="brand-section">
<div class="logo"><AppIcon :icon="Document" :size="56" /></div>
<h1 class="app-title">NotesAgent</h1>
<p class="app-subtitle">本地优先的 AI 笔记软件</p>
</div>
<div class="vault-card">
<h2 class="card-title">选择知识库</h2>
<p class="card-desc">选择一个本地 Vault 开始你的知识之旅</p>
<div v-if="workspaceStore.recentVaults.length" class="recent-vaults">
<div class="section-label">最近打开</div>
<div class="vault-list">
<button
v-for="vault in workspaceStore.recentVaults"
:key="vault.path"
class="vault-item"
@click="openVault(vault.path)"
:disabled="isLoading"
>
<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>
<AppIcon class="vault-arrow" :icon="ArrowRight" :size="16" />
</button>
</div>
</div>
<div class="actions">
<button class="btn btn-primary" @click="openFolderPicker" :disabled="isLoading">
<AppIcon :icon="FolderOpened" /> 打开本地 Vault
</button>
<button class="btn btn-secondary" @click="showCreateDialog = true" :disabled="isLoading">
<AppIcon :icon="Plus" /> 创建新 Vault
</button>
</div>
<div class="ai-core-status">
<span class="status-dot" :class="aiCoreStatus" />
<span v-if="aiCoreStatus === 'checking'">正在检查 AI Core 状态...</span>
<span v-else-if="aiCoreStatus === 'running'" class="status-running">AI Core 运行正常</span>
<span v-else class="status-stopped">AI Core 未启动编辑功能仍可用</span>
</div>
</div>
<div class="footer-info">
<span>v0.1.0</span>
<button class="theme-toggle" @click="themeStore.toggleTheme()">
<AppIcon :icon="themeStore.isDark ? Sunny : Moon" :size="15" />
{{ themeStore.isDark ? '浅色' : '深色' }}
</button>
</div>
</div>
<!-- Create Vault Dialog -->
<div v-if="showCreateDialog" class="dialog-overlay" @click.self="showCreateDialog = false">
<div class="dialog">
<h3>创建新 Vault</h3>
<div class="form-group">
<label>Vault 名称</label>
<input v-model="newVaultName" type="text" placeholder="我的知识库" />
</div>
<div class="form-group">
<label>存储路径</label>
<input v-model="newVaultPath" type="text" placeholder="/path/to/vault" />
</div>
<div class="dialog-actions">
<button class="btn btn-secondary" @click="showCreateDialog = false">取消</button>
<button class="btn btn-primary" @click="createVault" :disabled="!newVaultName || !newVaultPath">创建</button>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.vault-entry {
height: 100vh;
width: 100vw;
display: flex;
align-items: center;
justify-content: center;
background: var(--color-background-primary);
position: relative;
overflow: hidden;
}
.bg-decoration {
position: absolute;
inset: 0;
background:
radial-gradient(circle at 20% 30%, var(--color-accent-soft) 0%, transparent 50%),
radial-gradient(circle at 80% 70%, var(--color-info-soft) 0%, transparent 50%);
opacity: 0.62;
}
.entry-container {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 32px;
max-width: 480px;
width: 90%;
animation: entry-in var(--motion-slow) both;
}
.brand-section {
text-align: center;
}
.logo {
display: inline-grid;
place-items: center;
width: 84px;
height: 84px;
margin-bottom: 14px;
border: 1px solid color-mix(in srgb, var(--color-accent-primary) 18%, transparent);
border-radius: 24px;
background: var(--color-surface-primary);
color: var(--color-accent-primary);
box-shadow: var(--shadow-lg);
}
.app-title {
font-size: 32px;
font-weight: 700;
color: var(--color-text-primary);
margin-bottom: 8px;
background: linear-gradient(135deg, var(--color-accent-primary), var(--color-info));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.app-subtitle {
font-size: 15px;
color: var(--color-text-secondary);
}
.vault-card {
width: 100%;
background: var(--color-surface-primary);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-xl);
padding: var(--space-2xl);
box-shadow: var(--shadow-xl);
}
.card-title {
font-size: 20px;
font-weight: 600;
margin-bottom: 4px;
color: var(--color-text-primary);
}
.card-desc {
font-size: 14px;
color: var(--color-text-secondary);
margin-bottom: var(--space-xl);
}
.section-label {
font-size: 12px;
color: var(--color-text-tertiary);
margin-bottom: var(--space-sm);
font-weight: 500;
}
.vault-list {
display: flex;
flex-direction: column;
gap: var(--space-xs);
margin-bottom: var(--space-xl);
}
.vault-item {
display: flex;
align-items: center;
gap: var(--space-md);
width: 100%;
padding: var(--space-md);
background: var(--color-background-secondary);
border: 1px solid transparent;
border-radius: var(--radius-md);
cursor: pointer;
text-align: left;
transition: background-color var(--motion-fast), border-color var(--motion-fast), box-shadow var(--motion-fast), transform var(--motion-fast);
&:hover {
background: var(--color-accent-soft);
border-color: var(--color-accent-secondary);
box-shadow: var(--shadow-sm);
transform: translateY(-1px);
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
.vault-icon {
font-size: 20px;
flex-shrink: 0;
}
.vault-info {
flex: 1;
min-width: 0;
}
.vault-name {
font-weight: 500;
color: var(--color-text-primary);
font-size: 14px;
}
.vault-path {
font-size: 12px;
color: var(--color-text-tertiary);
margin-top: 2px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.vault-arrow {
color: var(--color-text-tertiary);
font-size: 20px;
transition: color var(--motion-fast), transform var(--motion-fast);
}
.vault-item:hover .vault-arrow { color: var(--color-accent-primary); transform: translateX(3px); }
.actions {
display: flex;
flex-direction: column;
gap: var(--space-sm);
margin-bottom: var(--space-lg);
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-sm);
padding: 10px 16px;
border-radius: var(--radius-md);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background-color var(--motion-fast), border-color var(--motion-fast), box-shadow var(--motion-fast), transform var(--motion-fast);
border: 1px solid transparent;
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
&.btn-primary {
background: var(--color-accent-primary);
color: var(--color-text-inverse);
&:hover:not(:disabled) {
background: var(--color-accent-primary-hover);
transform: translateY(-1px);
box-shadow: 0 7px 18px color-mix(in srgb, var(--color-accent-primary) 25%, transparent);
}
}
&.btn-secondary {
background: var(--color-surface-secondary);
color: var(--color-text-primary);
border-color: var(--color-border-default);
&:hover:not(:disabled) {
background: var(--color-background-hover);
}
}
}
.ai-core-status {
display: flex;
align-items: center;
gap: var(--space-sm);
font-size: 13px;
color: var(--color-text-secondary);
padding-top: var(--space-md);
border-top: 1px solid var(--color-border-subtle);
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--color-text-tertiary);
&.checking {
animation: pulse 1.5s infinite;
background: var(--color-warning);
}
&.running { background: var(--color-success); }
&.stopped { background: var(--color-text-tertiary); }
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.status-running { color: var(--color-success); }
.status-stopped { color: var(--color-text-tertiary); }
.footer-info {
display: flex;
align-items: center;
gap: var(--space-lg);
font-size: 12px;
color: var(--color-text-tertiary);
}
.theme-toggle {
display: inline-flex;
align-items: center;
gap: var(--space-xs);
color: var(--color-text-secondary);
background: none;
border: none;
cursor: pointer;
padding: 4px 8px;
border-radius: var(--radius-sm);
&:hover {
background: var(--color-background-hover);
}
}
.dialog-overlay {
position: fixed;
inset: 0;
background: var(--color-background-overlay);
display: flex;
align-items: center;
justify-content: center;
z-index: var(--z-modal);
animation: dialog-backdrop-in var(--motion-fast) both;
}
.dialog {
background: var(--color-surface-primary);
border-radius: var(--radius-lg);
padding: var(--space-xl);
width: 90%;
max-width: 400px;
box-shadow: var(--shadow-xl);
animation: dialog-in var(--motion-normal) both;
}
@keyframes entry-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
@keyframes dialog-backdrop-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes dialog-in { from { opacity: 0; transform: translateY(8px) scale(.985); } to { opacity: 1; transform: translateY(0) scale(1); } }
.dialog h3 {
margin: 0 0 var(--space-lg) 0;
font-size: 18px;
}
.form-group {
margin-bottom: var(--space-md);
label {
display: block;
font-size: 13px;
color: var(--color-text-secondary);
margin-bottom: var(--space-xs);
}
input {
width: 100%;
padding: 8px 12px;
background: var(--color-background-secondary);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-md);
font-size: 14px;
color: var(--color-text-primary);
outline: none;
transition: border-color var(--motion-fast);
&:focus {
border-color: var(--color-border-focus);
}
}
}
.dialog-actions {
display: flex;
justify-content: flex-end;
gap: var(--space-sm);
margin-top: var(--space-lg);
}
</style>