新增: 初始化 DevFlow 项目仓库

Tauri 2 + Vue 3 + Vite 6 桌面应用,Rust workspace 含 13 个 crate
(df-ai / df-storage / df-workflow / df-core / df-execute 等)。
核心能力:AI 聊天 agentic 循环(工具调用+人工审批)、工作流引擎、
任务/想法/项目/阶段管理、可追溯性,及配套前端组件。
This commit is contained in:
2026-06-12 01:31:05 +08:00
commit 98393b4908
178 changed files with 27859 additions and 0 deletions

440
src/App.vue Normal file
View File

@@ -0,0 +1,440 @@
<template>
<!-- 分离窗口纯内容不渲染主布局 -->
<div v-if="isDetached" class="df-detached-root">
<router-view v-slot="{ Component }">
<component :is="Component" />
</router-view>
</div>
<!-- 主窗口完整布局 -->
<div v-else class="df-shell">
<!-- Sidebar -->
<aside class="df-sidebar">
<!-- Logo -->
<div class="sidebar-brand">
<div class="brand-mark">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" fill="url(#brand-grad)" />
<defs><linearGradient id="brand-grad" x1="3" y1="2" x2="21" y2="22">
<stop stop-color="#7b6ff0"/><stop offset="1" stop-color="#5eaff0"/>
</linearGradient></defs>
</svg>
</div>
<span class="brand-text">DevFlow</span>
<span class="brand-version">v0.1</span>
</div>
<!-- Navigation -->
<nav class="sidebar-nav">
<div class="nav-group">
<span class="nav-group-label">{{ $t('nav.workspace') }}</span>
<router-link
v-for="item in primaryNav" :key="item.path"
:to="item.path"
class="nav-link"
active-class="nav-link--active"
>
<span class="nav-link-icon" v-html="item.svg"></span>
<span class="nav-link-text">{{ $t(item.label) }}</span>
<span v-if="item.badge" class="nav-link-badge">{{ item.badge }}</span>
</router-link>
</div>
<div class="nav-divider"></div>
<div class="nav-group">
<span class="nav-group-label">{{ $t('nav.traceability') }}</span>
<router-link
v-for="item in secondaryNav" :key="item.path"
:to="item.path"
class="nav-link"
active-class="nav-link--active"
>
<span class="nav-link-icon" v-html="item.svg"></span>
<span class="nav-link-text">{{ $t(item.label) }}</span>
</router-link>
</div>
</nav>
<!-- Footer -->
<div class="sidebar-footer">
<div class="sidebar-status">
<span class="status-dot"></span>
<span class="status-text">{{ $t('nav.systemReady') }}</span>
</div>
<!-- AI 面板切换按钮 -->
<button class="nav-link ai-toggle-btn" :class="{ 'ai-toggle-btn--active': aiStore.state.panelOpen }" @click="aiStore.togglePanel()">
<span class="nav-link-icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a7 7 0 014 12.74V17a1 1 0 01-1 1H9a1 1 0 01-1-1v-2.26A7 7 0 0112 2z"/><line x1="9" y1="21" x2="15" y2="21"/></svg>
</span>
<span class="nav-link-text">{{ $t('nav.aiPanel') }}</span>
<span class="ai-toggle-shortcut">I</span>
</button>
<router-link to="/settings" class="nav-link" active-class="nav-link--active">
<span class="nav-link-icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
</span>
<span class="nav-link-text">{{ $t('nav.settings') }}</span>
</router-link>
</div>
</aside>
<!-- Main (页面内容) -->
<main class="df-main" v-show="!aiStore.state.maximized">
<router-view v-slot="{ Component }">
<transition name="page" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</main>
<!-- AI Chat Panel -->
<transition name="ai-slide">
<div v-if="aiStore.state.panelOpen && !aiStore.state.detached" class="df-ai-panel" :style="panelStyle">
<div v-show="!aiStore.state.maximized" class="ai-resize-handle" :class="{ active: resizing }" @mousedown="startResize"></div>
<AiChat />
</div>
</transition>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
import { useRoute } from 'vue-router'
import { useAiStore } from './stores/ai'
import AiChat from './components/AiChat.vue'
const route = useRoute()
const isDetached = computed(() => route.path === '/ai-detached')
const aiStore = useAiStore()
// 点击菜单自动退出最大化 → 切为分栏模式
watch(() => route.path, (path) => {
if (!['/', '/ai-home', '/ai-detached'].includes(path) && aiStore.state.maximized) {
aiStore.state.maximized = false
}
})
// ── AI 面板拖拽调宽 ──
const panelWidth = ref(parseInt(localStorage.getItem('df-ai-width') || '500', 10))
const resizing = ref(false)
// maximized=全宽(flex:1填充) | sidebar=固定宽
const panelStyle = computed(() => {
if (aiStore.state.maximized) return { flex: '1', minWidth: '0' }
return {
width: panelWidth.value + 'px',
minWidth: panelWidth.value + 'px',
maxWidth: panelWidth.value + 'px',
}
})
function startResize(e: MouseEvent) {
e.preventDefault()
resizing.value = true
const startX = e.clientX
const startWidth = panelWidth.value
document.body.style.cursor = 'col-resize'
document.body.style.userSelect = 'none'
function onMouseMove(ev: MouseEvent) {
const delta = startX - ev.clientX // 向左拖 = 增宽
const newWidth = Math.max(320, Math.min(800, startWidth + delta))
panelWidth.value = newWidth
}
function onMouseUp() {
resizing.value = false
document.body.style.cursor = ''
document.body.style.userSelect = ''
localStorage.setItem('df-ai-width', String(panelWidth.value))
document.removeEventListener('mousemove', onMouseMove)
document.removeEventListener('mouseup', onMouseUp)
}
document.addEventListener('mousemove', onMouseMove)
document.addEventListener('mouseup', onMouseUp)
}
// 主题初始化
function initTheme() {
const saved = localStorage.getItem('df-theme') || 'dark'
if (saved === 'light') {
document.documentElement.setAttribute('data-theme', 'light')
} else if (saved === 'system') {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
if (!prefersDark) document.documentElement.setAttribute('data-theme', 'light')
}
}
// Ctrl+I / Cmd+I 切换 AI 面板
function handleKeydown(e: KeyboardEvent) {
if ((e.ctrlKey || e.metaKey) && e.key === 'i') {
e.preventDefault()
aiStore.togglePanel()
}
}
onMounted(() => {
initTheme()
window.addEventListener('keydown', handleKeydown)
})
onUnmounted(() => {
window.removeEventListener('keydown', handleKeydown)
})
const primaryNav = [
{ path: '/dashboard', label: 'nav.overview', badge: null,
svg: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>' },
{ path: '/ideas', label: 'nav.ideas', badge: null,
svg: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18h6M10 22h4M12 2a7 7 0 014 12.74V17a1 1 0 01-1 1h-6a1 1 0 01-1-1v-2.26A7 7 0 0112 2z"/></svg>' },
{ path: '/projects', label: 'nav.projects', badge: null,
svg: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/></svg>' },
{ path: '/tasks', label: 'nav.tasks', badge: null,
svg: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>' },
]
const secondaryNav = [
{ path: '/knowledge', label: 'nav.knowledge',
svg: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 016.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 014 19.5v-15A2.5 2.5 0 016.5 2z"/></svg>' },
{ path: '/decisions', label: 'nav.decisions',
svg: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>' },
]
</script>
<style scoped>
.df-detached-root { width: 100vw; height: 100vh; overflow: hidden; background: var(--df-bg); }
.df-shell { display: flex; height: 100vh; width: 100vw; overflow: hidden; }
/* ═══ Sidebar ═══ */
.df-sidebar {
width: 192px;
min-width: 192px;
background: var(--df-sidebar-bg);
border-right: 0.5px solid var(--df-border);
display: flex;
flex-direction: column;
position: relative;
user-select: none;
}
/* — Logo — */
.sidebar-brand {
display: flex;
align-items: center;
gap: 8px;
padding: 14px 16px 12px;
border-bottom: 0.5px solid var(--df-border);
}
.brand-mark {
width: 28px; height: 28px;
display: flex; align-items: center; justify-content: center;
background: var(--df-accent-bg);
border: 0.5px solid var(--df-border);
}
.brand-text {
font-size: 15px;
font-weight: 500;
letter-spacing: -0.3px;
color: var(--df-text);
}
.brand-version {
font-family: var(--df-font-mono);
font-size: 10px;
color: var(--df-text-dim);
background: rgba(255,255,255,0.04);
padding: 1px 6px;
border-radius: var(--df-radius-xs);
margin-left: auto;
}
/* — Nav — */
.sidebar-nav { flex: 1; overflow-y: auto; padding: 8px 8px; }
.nav-group { margin-bottom: 2px; }
.nav-group-label {
display: block;
padding: 8px 10px 4px;
font-size: 9px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 1.2px;
color: var(--df-text-dim);
}
.nav-divider {
height: 0.5px;
background: var(--df-border);
margin: 6px 12px;
}
.nav-link {
display: flex;
align-items: center;
gap: 8px;
padding: 5px 10px;
margin: 1px 0;
border-radius: var(--df-radius-sm);
color: var(--df-text-secondary);
text-decoration: none;
font-size: 12px;
font-weight: 450;
transition: all 0.18s var(--df-ease);
position: relative;
}
.nav-link:hover {
background: var(--df-sidebar-hover);
color: var(--df-text);
}
.nav-link--active {
background: var(--df-sidebar-active);
color: var(--df-accent);
font-weight: 500;
}
.nav-link--active::before {
content: '';
position: absolute;
left: -10px;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 16px;
background: var(--df-accent);
border-radius: 0 3px 3px 0;
}
.nav-link-icon {
display: flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
opacity: 0.7;
flex-shrink: 0;
}
.nav-link--active .nav-link-icon { opacity: 1; }
.nav-link-text { flex: 1; }
.nav-link-badge {
font-family: var(--df-font-mono);
font-size: 10px;
font-weight: 500;
padding: 1px 6px;
border-radius: var(--df-radius);
min-width: 18px;
text-align: center;
background: var(--df-accent);
color: #fff;
line-height: 1.4;
}
/* — AI Toggle Button — */
.ai-toggle-btn {
cursor: pointer;
border: none;
background: transparent;
width: 100%;
font: inherit;
text-align: left;
appearance: none;
-webkit-appearance: none;
}
.ai-toggle-btn--active {
color: var(--df-accent);
background: var(--df-sidebar-active);
}
.ai-toggle-shortcut {
font-family: var(--df-font-mono);
font-size: 9px;
color: var(--df-text-dim);
background: rgba(255,255,255,0.04);
padding: 1px 5px;
border-radius: var(--df-radius-sm);
}
/* — Footer — */
.sidebar-footer {
padding: 6px 8px;
border-top: 0.5px solid var(--df-border);
}
.sidebar-status {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
margin-bottom: 1px;
}
.status-dot {
width: 6px; height: 6px;
border-radius: 50%;
background: var(--df-success);
animation: pulseGlow 3s ease-in-out infinite;
}
.status-text {
font-family: var(--df-font-mono);
font-size: 10px;
color: var(--df-text-dim);
letter-spacing: 0.3px;
}
/* — Main — */
.df-main {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
background: var(--df-bg);
min-width: 0;
}
/* — AI Panel (全宽) — */
.df-ai-panel {
position: relative;
height: 100vh;
overflow: hidden;
}
/* — AI Resize Handle — */
.ai-resize-handle {
position: absolute;
left: 0; top: 0; bottom: 0;
width: 4px;
cursor: col-resize;
z-index: 10;
transition: background 0.15s;
}
.ai-resize-handle:hover,
.ai-resize-handle.active {
background: var(--df-accent);
opacity: 0.4;
}
/* — AI Slide Transition — */
.ai-slide-enter-active { transition: opacity 0.2s var(--df-ease); }
.ai-slide-leave-active { transition: opacity 0.15s var(--df-ease); }
.ai-slide-enter-from, .ai-slide-leave-to {
opacity: 0;
}
/* ═══ Responsive ═══ */
@media (max-width: 768px) {
.df-sidebar { width: 56px; min-width: 56px; }
.brand-text, .brand-version, .nav-group-label, .nav-link-text,
.nav-link-badge, .sidebar-status, .status-text, .ai-toggle-shortcut { display: none; }
.sidebar-brand { padding: 12px 10px 10px; justify-content: center; }
.brand-mark { width: 32px; height: 32px; }
.nav-link { justify-content: center; padding: 8px 0; }
.nav-divider { margin: 4px 8px; }
.nav-link--active::before { display: none; }
.df-ai-panel { width: 100vw; min-width: 100vw; max-width: 100vw; position: absolute; right: 0; top: 0; z-index: 100; }
}
/* — Page Transition — */
.page-enter-active { transition: opacity 0.2s var(--df-ease), transform 0.2s var(--df-ease); }
.page-leave-active { transition: opacity 0.12s var(--df-ease), transform 0.12s var(--df-ease); }
.page-enter-from { opacity: 0; transform: translateY(6px); }
.page-leave-to { opacity: 0; transform: translateY(-4px); }
</style>