Compare commits

..
3 Commits
Author SHA1 Message Date
wonder 8c5d540565 fix: 修复所有页面 Tab 导航下方冗余空白
- 将 body 布局从普通流改为 flex 纵向布局,navbar 和内容区自然排列
- navbar 移除 position:sticky,改为 flex-shrink:0 作为固定头部
- 新增 .main-content 工具类(flex:1; overflow:hidden),替代 calc(100vh-56px)+margin-top 方案
- builder/dashboard 页面使用 main-content 类占满剩余空间
- settings 页面 margin-top 从 80px 调整为 24px
2026-06-26 17:44:01 +08:00
wonder 27a2dd743d feat: 新增参考材料前端 UI
- 在左侧片段选择卡片下方添加参考材料卡片
- 支持动态添加/删除参考材料
- 每个材料包含可选标题和内容输入
- 标题为空时自动显示为「参考材料{n}」
- 材料内容实时预览到 Prompt
- 草稿自动保存/加载包含材料数据
- 保存会话时支持保存材料
2026-06-26 17:26:34 +08:00
wonder e49b744385 feat: 新增参考材料后端支持
- 新增 SessionMaterial 模型
- 新增 builder_session_materials 表自动迁移
- BuilderSession 增加 Materials 字段
- CreateBuilderSession/UpdateBuilderSession 支持保存材料
- GetBuilderSession 支持加载材料
- DeleteBuilderSession 支持级联删除材料
- AssemblePrompt 支持组装材料内容到 Prompt
2026-06-26 17:26:27 +08:00
7 changed files with 171 additions and 7 deletions
+1 -1
View File
@@ -8,7 +8,7 @@
<link rel="stylesheet" href="/static/common.css">
</head>
<body>
<div class="flex" style="height: calc(100vh - 56px); margin-top: 56px;">
<div class="flex main-content">
<!-- 左侧项目树 -->
<div class="w-64 border-r overflow-y-auto p-4" style="border-color: var(--border-color); background: var(--bg-primary);">
<div class="search-box mb-4">
+83 -2
View File
@@ -8,7 +8,7 @@
<link rel="stylesheet" href="/static/common.css">
</head>
<body>
<div class="flex" style="height: calc(100vh - 56px); margin-top: 56px;">
<div class="flex main-content">
<!-- 左侧面板 -->
<div class="w-1/3 border-r overflow-y-auto p-4 flex flex-col gap-4" style="border-color: var(--border-color); background: var(--bg-primary);">
<!-- 项目/会话 -->
@@ -73,6 +73,17 @@
<div class="card-body" id="snippets-container"></div>
</div>
<!-- 参考材料 -->
<div class="card">
<div class="card-header">
📦 参考材料
<button class="btn btn-sm" onclick="addMaterial()">+ 添加</button>
</div>
<div class="card-body" id="materials-container">
<div class="text-sm text-muted" id="materials-empty">暂无参考材料</div>
</div>
</div>
<!-- CLAUDE.md -->
<div class="card">
<div class="collapse-header" onclick="toggleCollapse(this)">
@@ -173,6 +184,7 @@
let allSnippets = [];
let selectedTags = {}; // tagId -> optionId
let selectedSnippets = []; // [{snippetId, sortOrder}]
let materials = []; // [{title, content}]
let suggestions = [];
// 初始化
@@ -380,6 +392,16 @@
}
}
// Materials
const validMaterials = materials.filter(m => m.content.trim());
if (validMaterials.length > 0) {
const materialParts = validMaterials.map((m, i) => {
const title = m.title.trim() || `参考材料${i + 1}`;
return `--- ${title} ---\n${m.content}`;
});
parts.push(`## 参考材料\n\n${materialParts.join('\n\n')}`);
}
// Suggestions
const adoptedSuggestions = suggestions.filter(s => s.adopted);
if (adoptedSuggestions.length > 0) {
@@ -441,6 +463,51 @@
document.getElementById('tag-search')?.addEventListener('input', debounce(() => renderTags(), 300));
document.getElementById('snippet-search')?.addEventListener('input', debounce(() => renderSnippets(), 300));
// 参考材料管理
function addMaterial() {
materials.push({ title: '', content: '' });
renderMaterials();
updatePreview();
}
function removeMaterial(index) {
materials.splice(index, 1);
renderMaterials();
updatePreview();
}
function renderMaterials() {
const container = document.getElementById('materials-container');
const emptyHint = document.getElementById('materials-empty');
if (materials.length === 0) {
container.innerHTML = '<div class="text-sm text-muted" id="materials-empty">暂无参考材料</div>';
return;
}
container.innerHTML = materials.map((m, i) => `
<div class="mb-3 p-3" style="background:var(--bg-tertiary);border-radius:8px">
<div class="flex items-center justify-between mb-2">
<input class="input" placeholder="参考材料${i + 1}" value="${escapeHtml(m.title)}"
oninput="updateMaterialTitle(${i}, this.value)" style="flex:1;margin-right:8px">
<button class="btn btn-sm" onclick="removeMaterial(${i})" style="color:var(--error-color)">✕</button>
</div>
<textarea class="textarea" placeholder="输入参考材料内容..." rows="3"
oninput="updateMaterialContent(${i}, this.value)">${escapeHtml(m.content)}</textarea>
</div>
`).join('');
}
function updateMaterialTitle(index, title) {
materials[index].title = title;
updatePreview();
}
function updateMaterialContent(index, content) {
materials[index].content = content;
updatePreview();
}
// 自定义输入变化时更新预览
document.getElementById('custom-content')?.addEventListener('input', debounce(() => updatePreview(), 200));
document.getElementById('project-name')?.addEventListener('input', debounce(() => updatePreview(), 200));
@@ -591,6 +658,14 @@
sort_order: s.sortOrder
}));
const materialsData = materials
.filter(m => m.content.trim())
.map((m, i) => ({
title: m.title.trim(),
content: m.content,
sort_order: i
}));
try {
await API.post('/api/builder/sessions', {
title,
@@ -598,7 +673,8 @@
final_prompt: finalPrompt,
claude_session_id: claudeSessionId,
tag_option_ids: tagOptionIds,
snippet_ids: snippetIds
snippet_ids: snippetIds,
materials: materialsData
});
closeSaveModal();
Toast.success('会话已保存');
@@ -616,6 +692,7 @@
claudeMD: document.getElementById('claude-md').value,
selectedTags: { ...selectedTags },
selectedSnippets: [...selectedSnippets],
materials: materials.map(m => ({ title: m.title, content: m.content })),
});
}, 1000);
@@ -633,6 +710,8 @@
document.getElementById('claude-md').value = draft.claudeMD || '';
selectedTags = draft.selectedTags || {};
selectedSnippets = draft.selectedSnippets || [];
materials = draft.materials || [];
renderMaterials();
}
function resetBuilder() {
@@ -644,11 +723,13 @@
document.getElementById('session-prompts').classList.add('hidden');
selectedTags = {};
selectedSnippets = [];
materials = [];
suggestions = [];
document.getElementById('suggestions-card').style.display = 'none';
Draft.clear('builder');
renderTags();
renderSnippets();
renderMaterials();
updatePreview();
Toast.success('已重置');
}
+1 -1
View File
@@ -8,7 +8,7 @@
<link rel="stylesheet" href="/static/common.css">
</head>
<body>
<div style="max-width:640px;margin:80px auto 0;padding:0 24px">
<div style="max-width:640px;margin:24px auto 0;padding:0 24px">
<h1 style="font-size:24px;font-weight:700;margin-bottom:24px">⚙️ 设置</h1>
<!-- LLM 配置 -->
+13 -2
View File
@@ -39,10 +39,16 @@
padding: 0;
}
html, body {
height: 100%;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: var(--bg-secondary);
color: var(--text-primary);
display: flex;
flex-direction: column;
min-height: 100vh;
}
@@ -55,12 +61,17 @@ body {
display: flex;
align-items: center;
justify-content: space-between;
position: sticky;
top: 0;
flex-shrink: 0;
z-index: 100;
box-shadow: var(--shadow-sm);
}
/* 主内容区 */
.main-content {
flex: 1;
overflow: hidden;
}
.nav-brand {
font-size: 18px;
font-weight: 700;
+10
View File
@@ -102,6 +102,16 @@ func AutoMigrate() error {
PRIMARY KEY (id),
KEY idx_session_id (builder_session_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
`CREATE TABLE IF NOT EXISTS builder_session_materials (
id bigint NOT NULL AUTO_INCREMENT,
builder_session_id bigint NOT NULL,
title varchar(255) NOT NULL DEFAULT '',
content text NOT NULL,
sort_order int NOT NULL DEFAULT 0,
PRIMARY KEY (id),
KEY idx_session_id (builder_session_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
}
for _, ddl := range tables {
+53 -1
View File
@@ -47,6 +47,11 @@ func CreateBuilderSession(w http.ResponseWriter, r *http.Request) {
SnippetID int64 `json:"snippet_id"`
SortOrder int `json:"sort_order"`
} `json:"snippet_ids"`
Materials []struct {
Title string `json:"title"`
Content string `json:"content"`
SortOrder int `json:"sort_order"`
} `json:"materials"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
fail(w, 400, "请求格式错误")
@@ -79,6 +84,12 @@ func CreateBuilderSession(w http.ResponseWriter, r *http.Request) {
sessionID, s.SnippetID, s.SortOrder)
}
// Insert materials
for _, m := range req.Materials {
db.DB.Exec("INSERT INTO builder_session_materials (builder_session_id, title, content, sort_order) VALUES (?, ?, ?, ?)",
sessionID, m.Title, m.Content, m.SortOrder)
}
success(w, map[string]int64{"id": sessionID})
}
@@ -121,12 +132,27 @@ func GetBuilderSession(w http.ResponseWriter, r *http.Request) {
snippetRows.Close()
}
// Load materials
materialRows, err := db.DB.Query("SELECT id, builder_session_id, title, content, sort_order FROM builder_session_materials WHERE builder_session_id=? ORDER BY sort_order", id)
if err == nil {
for materialRows.Next() {
var m models.SessionMaterial
if err := materialRows.Scan(&m.ID, &m.BuilderSessionID, &m.Title, &m.Content, &m.SortOrder); err == nil {
s.Materials = append(s.Materials, m)
}
}
materialRows.Close()
}
if s.Tags == nil {
s.Tags = []models.SessionTag{}
}
if s.Snippets == nil {
s.Snippets = []models.SessionSnippet{}
}
if s.Materials == nil {
s.Materials = []models.SessionMaterial{}
}
success(w, s)
}
@@ -151,6 +177,11 @@ func UpdateBuilderSession(w http.ResponseWriter, r *http.Request) {
SnippetID int64 `json:"snippet_id"`
SortOrder int `json:"sort_order"`
} `json:"snippet_ids"`
Materials []struct {
Title string `json:"title"`
Content string `json:"content"`
SortOrder int `json:"sort_order"`
} `json:"materials"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
fail(w, 400, "请求格式错误")
@@ -183,6 +214,13 @@ func UpdateBuilderSession(w http.ResponseWriter, r *http.Request) {
id, s.SnippetID, s.SortOrder)
}
// Replace materials
db.DB.Exec("DELETE FROM builder_session_materials WHERE builder_session_id=?", id)
for _, m := range req.Materials {
db.DB.Exec("INSERT INTO builder_session_materials (builder_session_id, title, content, sort_order) VALUES (?, ?, ?, ?)",
id, m.Title, m.Content, m.SortOrder)
}
success(w, nil)
}
@@ -195,12 +233,13 @@ func DeleteBuilderSession(w http.ResponseWriter, r *http.Request) {
db.DB.Exec("DELETE FROM builder_session_tags WHERE builder_session_id=?", id)
db.DB.Exec("DELETE FROM builder_session_snippets WHERE builder_session_id=?", id)
db.DB.Exec("DELETE FROM builder_session_materials WHERE builder_session_id=?", id)
db.DB.Exec("DELETE FROM builder_sessions WHERE id=?", id)
success(w, nil)
}
// AssemblePrompt 组装最终 Prompt
func AssemblePrompt(projectName string, customContent string, tagOptions []models.TagOption, snippets []models.Snippet, suggestions []string) string {
func AssemblePrompt(projectName string, customContent string, tagOptions []models.TagOption, snippets []models.Snippet, suggestions []string, materials []models.SessionMaterial) string {
var parts []string
// Project context
@@ -231,6 +270,19 @@ func AssemblePrompt(projectName string, customContent string, tagOptions []model
parts = append(parts, fmt.Sprintf("## 参考片段\n\n%s", strings.Join(snippetParts, "\n\n")))
}
// Materials
if len(materials) > 0 {
var materialParts []string
for i, m := range materials {
title := m.Title
if title == "" {
title = fmt.Sprintf("参考材料%d", i+1)
}
materialParts = append(materialParts, fmt.Sprintf("--- %s ---\n%s", title, m.Content))
}
parts = append(parts, fmt.Sprintf("## 参考材料\n\n%s", strings.Join(materialParts, "\n\n")))
}
// Suggestions
if len(suggestions) > 0 {
parts = append(parts, fmt.Sprintf("## 补充建议\n\n%s", strings.Join(suggestions, "\n")))
+10
View File
@@ -43,6 +43,7 @@ type BuilderSession struct {
UpdatedAt time.Time `json:"updated_at"`
Tags []SessionTag `json:"tags,omitempty"`
Snippets []SessionSnippet `json:"snippets,omitempty"`
Materials []SessionMaterial `json:"materials,omitempty"`
}
// SessionTag 构建会话标签关联
@@ -61,6 +62,15 @@ type SessionSnippet struct {
SortOrder int `json:"sort_order"`
}
// SessionMaterial 构建会话参考材料
type SessionMaterial struct {
ID int64 `json:"id"`
BuilderSessionID int64 `json:"builder_session_id"`
Title string `json:"title"`
Content string `json:"content"`
SortOrder int `json:"sort_order"`
}
// Prompt prompts 表记录(只读)
type Prompt struct {
ID int64 `json:"id"`