新增: 初始化 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:
338
src/views/Knowledge.vue
Normal file
338
src/views/Knowledge.vue
Normal file
@@ -0,0 +1,338 @@
|
||||
<template>
|
||||
<div class="knowledge">
|
||||
<!-- 页面头部 -->
|
||||
<header class="page-header">
|
||||
<h1>📚 知识库</h1>
|
||||
<div class="header-actions">
|
||||
<button class="btn btn-primary" @click="addKnowledge">+ 新增知识</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<div class="search-bar">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
class="search-input"
|
||||
placeholder="搜索知识标题、标签、内容..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 分类标签页 -->
|
||||
<div class="category-tabs">
|
||||
<button
|
||||
v-for="cat in categories"
|
||||
:key="cat.key"
|
||||
class="tab-btn"
|
||||
:class="{ 'tab-active': activeCategory === cat.key }"
|
||||
@click="activeCategory = cat.key"
|
||||
>
|
||||
<span class="tab-icon">{{ cat.icon }}</span>
|
||||
<span class="tab-label">{{ cat.label }}</span>
|
||||
<span class="tab-count">{{ getCategoryCount(cat.key) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 知识卡片列表 -->
|
||||
<div class="knowledge-grid">
|
||||
<div class="knowledge-card" v-for="item in filteredItems" :key="item.id">
|
||||
<div class="card-header">
|
||||
<span class="card-title">{{ item.title }}</span>
|
||||
<span class="card-score" :class="scoreClass(item.score)">{{ item.score }}分</span>
|
||||
</div>
|
||||
<div class="card-desc">{{ item.description }}</div>
|
||||
<div class="card-tags">
|
||||
<span class="tag" v-for="tag in item.tags" :key="tag">{{ tag }}</span>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<span class="card-reuse">🔄 复用 {{ item.reuseCount }} 次</span>
|
||||
<span class="card-category">{{ getCategoryLabel(item.category) }}</span>
|
||||
<span class="card-time">{{ item.updatedAt }}</span>
|
||||
<button class="btn btn-ghost" @click="deleteKnowledge(item.id)">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="filteredItems.length === 0" class="empty-state">
|
||||
<div class="empty-icon">📭</div>
|
||||
<div class="empty-text">没有找到匹配的知识条目</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
interface KnowledgeItem {
|
||||
id: number
|
||||
title: string
|
||||
description: string
|
||||
category: string
|
||||
tags: string[]
|
||||
reuseCount: number
|
||||
score: number
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
const categories = [
|
||||
{ key: 'all', label: '全部', icon: '📦' },
|
||||
{ key: 'review', label: '审查规则', icon: '🔍' },
|
||||
{ key: 'prompt', label: 'Prompt模板', icon: '💬' },
|
||||
{ key: 'pitfall', label: '踩坑经验', icon: '⚠️' },
|
||||
{ key: 'diagnosis', label: '诊断知识', icon: '🩺' },
|
||||
{ key: 'deploy', label: '部署经验', icon: '🚀' },
|
||||
]
|
||||
|
||||
const activeCategory = ref('all')
|
||||
const searchQuery = ref('')
|
||||
const knowledgeItems = ref<KnowledgeItem[]>(loadKnowledgeItems())
|
||||
|
||||
// 从 localStorage 加载知识数据
|
||||
function loadKnowledgeItems(): KnowledgeItem[] {
|
||||
const saved = localStorage.getItem('devflow-knowledge')
|
||||
if (saved) {
|
||||
try {
|
||||
return JSON.parse(saved)
|
||||
} catch {
|
||||
// 解析失败,使用默认数据
|
||||
}
|
||||
}
|
||||
return getDefaultData()
|
||||
}
|
||||
|
||||
// 获取默认数据
|
||||
function getDefaultData(): KnowledgeItem[] {
|
||||
return [
|
||||
// 审查规则
|
||||
{ id: 1, title: 'SQL 注入防护检查', description: '所有 SQL 拼接必须使用参数化查询,禁止字符串拼接用户输入', category: 'review', tags: ['安全', 'SQL', 'Go'], reuseCount: 23, score: 95, updatedAt: '3 天前' },
|
||||
{ id: 2, title: '错误处理规范', description: '禁止吞掉 error,必须向上传播或记录日志', category: 'review', tags: ['Go', '规范', '错误处理'], reuseCount: 18, score: 88, updatedAt: '1 周前' },
|
||||
{ id: 3, title: 'API 响应格式一致性', description: '统一使用 { code, message, data } 结构, HTTP 状态码语义正确', category: 'review', tags: ['API', '规范'], reuseCount: 15, score: 82, updatedAt: '5 天前' },
|
||||
|
||||
// Prompt 模板
|
||||
{ id: 4, title: 'SQL 查询生成 Prompt', description: '根据自然语言生成 SQL,包含表结构上下文和示例输出格式', category: 'prompt', tags: ['SQL', 'LLM', '模板'], reuseCount: 142, score: 91, updatedAt: '昨天' },
|
||||
{ id: 5, title: '代码审查 Prompt', description: 'AI 代码审查专用 Prompt,涵盖安全、性能、可维护性维度', category: 'prompt', tags: ['代码审查', 'LLM'], reuseCount: 87, score: 85, updatedAt: '3 天前' },
|
||||
{ id: 6, title: 'API 文档生成 Prompt', description: '从 Handler 代码自动生成 API 文档的 Prompt 模板', category: 'prompt', tags: ['文档', 'LLM', '自动生成'], reuseCount: 34, score: 78, updatedAt: '1 周前' },
|
||||
|
||||
// 踩坑经验
|
||||
{ id: 7, title: 'Go context 传递陷阱', description: 'goroutine 中必须传递 context 而非创建新的,否则超时控制失效', category: 'pitfall', tags: ['Go', '并发', 'context'], reuseCount: 12, score: 92, updatedAt: '2 天前' },
|
||||
{ id: 8, title: 'Vue 3 作用域坑', description: 'v-for 内 ref 绑定不会自动响应式,需使用数组形式', category: 'pitfall', tags: ['Vue', '前端', '响应式'], reuseCount: 8, score: 75, updatedAt: '1 周前' },
|
||||
{ id: 9, title: 'Docker 网络模式选择', description: 'host 模式在 Mac/Win 上无效,必须用端口映射', category: 'pitfall', tags: ['Docker', '网络'], reuseCount: 6, score: 70, updatedAt: '2 周前' },
|
||||
|
||||
// 诊断知识
|
||||
{ id: 10, title: 'MySQL 慢查询诊断流程', description: 'EXPLAIN → 索引检查 → 慢查询日志分析 → 优化建议', category: 'diagnosis', tags: ['MySQL', '性能', '诊断'], reuseCount: 31, score: 89, updatedAt: '4 天前' },
|
||||
{ id: 11, title: 'Go 内存泄漏排查', description: 'pprof heap 分析 → goroutine 泄漏检查 → GC 调优', category: 'diagnosis', tags: ['Go', '内存', 'pprof'], reuseCount: 9, score: 83, updatedAt: '1 周前' },
|
||||
{ id: 12, title: '前端白屏诊断', description: 'Console 错误 → 网络请求 → 路由配置 → 构建产物检查', category: 'diagnosis', tags: ['前端', '调试', 'Vue'], reuseCount: 14, score: 80, updatedAt: '5 天前' },
|
||||
|
||||
// 部署经验
|
||||
{ id: 13, title: 'Go 二进制热更新', description: 'kill + mv + nohup 启动,无需 systemd 的轻量部署方案', category: 'deploy', tags: ['Go', '部署', 'Linux'], reuseCount: 19, score: 86, updatedAt: '昨天' },
|
||||
{ id: 14, title: 'SCP 上传最佳实践', description: '使用 ssh-proxy upload 代替 scp,支持配置化管理', category: 'deploy', tags: ['SCP', '部署', '工具'], reuseCount: 11, score: 72, updatedAt: '3 天前' },
|
||||
{ id: 15, title: 'Nginx 反向代理配置', description: 'u-ask 的 Nginx 配置模板,含 WebSocket 支持和 gzip', category: 'deploy', tags: ['Nginx', '配置', '反向代理'], reuseCount: 7, score: 77, updatedAt: '1 周前' },
|
||||
]
|
||||
}
|
||||
|
||||
// 保存到 localStorage
|
||||
function saveToLocalStorage() {
|
||||
localStorage.setItem('devflow-knowledge', JSON.stringify(knowledgeItems.value))
|
||||
}
|
||||
|
||||
// 添加知识
|
||||
function addKnowledge() {
|
||||
// TODO: 实现添加知识的对话框逻辑
|
||||
console.log('添加知识')
|
||||
}
|
||||
|
||||
// 删除知识
|
||||
function deleteKnowledge(id: number) {
|
||||
const index = knowledgeItems.value.findIndex(item => item.id === id)
|
||||
if (index !== -1) {
|
||||
knowledgeItems.value.splice(index, 1)
|
||||
saveToLocalStorage()
|
||||
}
|
||||
}
|
||||
|
||||
const filteredItems = computed(() => {
|
||||
let items = knowledgeItems.value
|
||||
if (activeCategory.value !== 'all') {
|
||||
items = items.filter(i => i.category === activeCategory.value)
|
||||
}
|
||||
if (searchQuery.value.trim()) {
|
||||
const q = searchQuery.value.toLowerCase()
|
||||
items = items.filter(i =>
|
||||
i.title.toLowerCase().includes(q) ||
|
||||
i.tags.some(t => t.toLowerCase().includes(q)) ||
|
||||
i.description.toLowerCase().includes(q)
|
||||
)
|
||||
}
|
||||
return items
|
||||
})
|
||||
|
||||
function getCategoryCount(key: string): number {
|
||||
if (key === 'all') return knowledgeItems.value.length
|
||||
return knowledgeItems.value.filter(i => i.category === key).length
|
||||
}
|
||||
|
||||
function getCategoryLabel(key: string): string {
|
||||
const cat = categories.find(c => c.key === key)
|
||||
return cat ? cat.label : key
|
||||
}
|
||||
|
||||
function scoreClass(score: number): string {
|
||||
if (score >= 90) return 'score-high'
|
||||
if (score >= 75) return 'score-mid'
|
||||
return 'score-low'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.knowledge { padding: 16px 20px 20px; }
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--df-gap-page);
|
||||
}
|
||||
.page-header h1 { font-size: 24px; font-weight: 500; color: var(--df-text); }
|
||||
.header-actions { display: flex; gap: 8px; }
|
||||
|
||||
/* ===== 按钮 ===== */
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: var(--df-radius-sm);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.btn-primary { background: var(--df-accent); color: #fff; }
|
||||
.btn-primary:hover { background: var(--df-accent-hover); }
|
||||
.btn-accent { background: rgba(100,255,218,0.15); color: var(--df-success); border: 0.5px solid rgba(100,255,218,0.3); }
|
||||
.btn-accent:hover { background: rgba(100,255,218,0.25); }
|
||||
.btn-ghost { background: transparent; color: var(--df-text-secondary); border: 0.5px solid var(--df-border); }
|
||||
.btn-ghost:hover { background: var(--df-bg-card); color: var(--df-text); }
|
||||
|
||||
/* ===== 搜索栏 ===== */
|
||||
.search-bar { margin-bottom: var(--df-gap-page); }
|
||||
.search-input {
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
padding: 10px 16px;
|
||||
background: var(--df-bg-card);
|
||||
border: 0.5px solid var(--df-border);
|
||||
border-radius: var(--df-radius);
|
||||
color: var(--df-text);
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
.search-input::placeholder { color: var(--df-text-dim); }
|
||||
.search-input:focus { border-color: var(--df-accent); }
|
||||
|
||||
/* ===== 分类标签 ===== */
|
||||
.category-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: var(--df-gap-page);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.tab-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 16px;
|
||||
border: 0.5px solid var(--df-border);
|
||||
border-radius: var(--df-radius);
|
||||
background: transparent;
|
||||
color: var(--df-text-secondary);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.tab-btn:hover { background: var(--df-bg-card); color: var(--df-text); }
|
||||
.tab-active {
|
||||
background: var(--df-accent) !important;
|
||||
color: #fff !important;
|
||||
border-color: var(--df-accent) !important;
|
||||
}
|
||||
.tab-icon { font-size: 14px; }
|
||||
.tab-count {
|
||||
font-size: 11px;
|
||||
background: rgba(255,255,255,0.15);
|
||||
padding: 1px 6px;
|
||||
border-radius: var(--df-radius);
|
||||
min-width: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
.tab-active .tab-count { background: rgba(255,255,255,0.25); }
|
||||
|
||||
/* ===== 知识卡片网格 ===== */
|
||||
.knowledge-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
|
||||
gap: var(--df-gap-grid);
|
||||
}
|
||||
.knowledge-card {
|
||||
background: var(--df-bg-card);
|
||||
border: 0.5px solid var(--df-border);
|
||||
border-radius: var(--df-radius-lg);
|
||||
padding: var(--df-pad-panel);
|
||||
transition: border-color 0.15s, transform 0.15s;
|
||||
}
|
||||
.knowledge-card:hover {
|
||||
border-color: var(--df-accent);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.card-title { font-size: 14px; font-weight: 500; color: var(--df-text); }
|
||||
.card-score {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--df-radius-xs);
|
||||
}
|
||||
.score-high { background: rgba(100,255,218,0.15); color: var(--df-success); }
|
||||
.score-mid { background: rgba(255,217,61,0.15); color: var(--df-warning); }
|
||||
.score-low { background: rgba(255,107,107,0.15); color: var(--df-danger); }
|
||||
|
||||
.card-desc {
|
||||
font-size: 13px;
|
||||
color: var(--df-text-secondary);
|
||||
line-height: 1.5;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.card-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
|
||||
.tag {
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--df-radius-xs);
|
||||
background: rgba(108,99,255,0.1);
|
||||
color: var(--df-accent);
|
||||
}
|
||||
.card-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 11px;
|
||||
color: var(--df-text-dim);
|
||||
}
|
||||
.card-reuse { color: var(--df-info); }
|
||||
.card-category {
|
||||
background: rgba(90,99,128,0.2);
|
||||
padding: 2px 6px;
|
||||
border-radius: var(--df-radius-sm);
|
||||
}
|
||||
|
||||
/* ===== 空状态 ===== */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 0;
|
||||
}
|
||||
.empty-icon { font-size: 48px; margin-bottom: 12px; }
|
||||
.empty-text { font-size: 14px; color: var(--df-text-dim); }
|
||||
</style>
|
||||
Reference in New Issue
Block a user