提问背景 Question:
那么以下代码,针对 description我要如何处理。
// 2. 百科详情页 Handler
// 路由配置:r.GET("/baike/article/:id", ai.Baike_detail)
func Baike_detail(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
articleIDStr := ps.ByName("id")
articleID, err := strconv.Atoi(articleIDStr)
if err != nil {
http.NotFound(w, r)
return
}
// 1. 查询文章主体
var a Article
var contentRaw string
var createdAtInt int64
row := fun.DB.QueryRow(
"SELECT id, question, title, description, content, click_count, like_count, created_at FROM cms_article WHERE id = ? AND status = 1",
articleID,
)
err = row.Scan(&a.ID, &a.Question, &a.Title, &a.Description, &contentRaw, &a.ClickCount, &a.LikeCount, &createdAtInt)
if err != nil {
if err == sql.ErrNoRows {
http.NotFound(w, r)
} else {
http.Error(w, "数据库错误: "+err.Error(), http.StatusInternalServerError)
}
return
}
//a.Content = template.HTML(contentRaw) // 转换为非转义 HTML,直接渲染
a.Content = ConvertMarkdown(contentRaw) //解析Markdown并转义template.HTML
a.CreatedAt = time.Unix(createdAtInt, 0).Format("2006-01-02 15:04")
// 2. 异步增加浏览量(避免阻塞主渲染)
go func(id int) {
_, _ = fun.DB.Exec("UPDATE cms_article SET click_count = click_count + 1 WHERE id = ?", id)
}(articleID)
// 3. 查询当前文章的 Tags
var tags []Tag
tagRows, err := fun.DB.Query(`
SELECT t.id, t.name
FROM cms_tag t
INNER JOIN cms_article_tag at ON t.id = at.tag_id
WHERE at.article_id = ?`,
articleID,
)
if err == nil {
for tagRows.Next() {
var t Tag
if err := tagRows.Scan(&t.ID, &t.Name); err == nil {
tags = append(tags, t)
}
}
tagRows.Close()
}
data := BaikeDetailData{
Article: a,
Tags: tags,
}
// 4. 渲染模板
path := filepath.Join(fun.AppPath, "/templates/ai/baike_detail.html")
mb, err := fun.TManager.Get(path)
if err != nil {
http.Error(w, "详情模板渲染失败: "+err.Error(), http.StatusInternalServerError)
return
}
err = mb.Execute(w, data)
if err != nil {
fun.WriteLog("渲染百科详情页模板报错: %s", err)
}
}
/*
func ConvertMarkdown(mdContent string) template.HTML {
var buf bytes.Buffer
if err := goldmark.Convert([]byte(mdContent), &buf); err != nil {
return template.HTML(mdContent) // 如果解析失败,回退返回原始文本
}
return template.HTML(buf.String())
}
*/
func ConvertMarkdown(mdContent string) template.HTML {
// 1. 将字面量的 "" (反斜杠+n) 替换为真正的换行符 ""
mdContent = strings.ReplaceAll(mdContent, "\\n", "\n")
// 2. 兼容性处理:将字面量的 "\\r" 替换为真正的 "\r"
mdContent = strings.ReplaceAll(mdContent, "\\r", "\n")
// 3. 兼容性处理:将 Windows 换行符统一替换为 Linux 换行符(可选,防患未然)
mdContent = strings.ReplaceAll(mdContent, "\r", "\n")
var buf bytes.Buffer
if err := goldmark.Convert([]byte(mdContent), &buf); err != nil {
return template.HTML(mdContent) // 如果解析失败,回退返回原始文本
}
return template.HTML(buf.String())
}
Go百科详情提问背景处理
针对 description(提问背景),为了既能优雅地展示代码与排版(支持 Markdown),又能防止长内容/多代码撑爆页面,最稳妥、体验最好的处理方案是:
- 后端:使用您现有的
ConvertMarkdown方法,将description解析为template.HTML,确保代码块和格式正常渲染。 - 前端:不要在后端强行截断字符串(因为截断 HTML/Markdown 会导致标签未闭合、代码块错乱)。应当**在前端使用 CSS/JS 实现「超出高度折叠 + 点击展开」**的效果。
具体改造方案如下:
一、 后端代码调整
首先,确保您的 Article 结构体或传递给模板的数据结构中,Description(或者新增一个字段)能够接收 template.HTML 类型,避免前端模板渲染时被转义。
1. 修改结构体定义
建议在 Article 结构体中将 Description 定义为 template.HTML,或者在渲染结构体中新增一个 DescriptionHTML 字段:
type Article struct {
ID int
Question string
Title string
Description template.HTML // 修改为 template.HTML 类型
Content template.HTML
ClickCount int
LikeCount int
CreatedAt string
}
2. 在 Handler 中解析 Markdown
在 Baike_detail 函数中,读取数据库后进行解析转换:
func Baike_detail(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
// ... 前面查询数据的代码保持不变 ...
var a Article
var contentRaw string
var descriptionRaw string // 用临时变量接收数据库中的原始 String
var createdAtInt int64
row := fun.DB.QueryRow(
"SELECT id, question, title, description, content, click_count, like_count, created_at FROM cms_article WHERE id = ? AND status = 1",
articleID,
)
err = row.Scan(&a.ID, &a.Question, &a.Title, &descriptionRaw, &contentRaw, &a.ClickCount, &a.LikeCount, &createdAtInt)
// ... 错误处理 ...
// 核心修改点:解析提问背景和正文
a.Description = ConvertMarkdown(descriptionRaw) // 解析提问背景的 Markdown
a.Content = ConvertMarkdown(contentRaw) // 解析正文
a.CreatedAt = time.Unix(createdAtInt, 0).Format("2006-01-02 15:04")
// ... 后续逻辑保持不变 ...
}
二、 前端 HTML/CSS 控制(防止内容过长)
在 baike_detail.html 中,对提问背景区域包裹一层容器,利用 CSS 限制最大显示高度,并提供“展开/收起”按钮。这样即使提问背景里有一百行代码,也不会影响首屏体验。
1. HTML 结构
<div class="question-bg-container">
<div class="question-bg-title">提问背景:</div>
<!-- 限制高度的包裹盒子 -->
<div class="question-bg-content" id="qBgContent">
{{.Article.Description}}
</div>
<!-- 展开收起按钮 -->
<div class="toggle-btn" id="toggleBtn" style="display: none;">展开全部 ↓</div>
</div>
2. CSS 样式(关键:限高、遮罩渐变)
.question-bg-container {
background-color: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 6px;
padding: 15px;
margin-bottom: 20px;
position: relative;
}
.question-bg-title {
font-weight: bold;
color: #495057;
margin-bottom: 8px;
}
.question-bg-content {
max-height: 180px; /* 默认最大高度,超出折叠,可放下约5-8行代码 */
overflow: hidden;
transition: max-height 0.3s ease;
position: relative;
}
/* 当内容超出时,底部的半透明遮罩,显得更平滑 */
.question-bg-content.collapsed::after {
content: "";
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 50px;
background: linear-gradient(transparent, #f8f9fa);
pointer-events: none;
}
.toggle-btn {
text-align: center;
color: #007bff;
cursor: pointer;
margin-top: 10px;
font-size: 14px;
user-select: none;
}
3. JS 逻辑(动态判断是否需要折叠)
document.addEventListener("DOMContentLoaded", function() {
const content = document.getElementById("qBgContent");
const btn = document.getElementById("toggleBtn");
// 设定一个阈值(与CSS中max-height一致)
const maxHeight = 180;
// 如果实际内容高度大于限制高度,则显示“展开”按钮,并应用遮罩
if (content.scrollHeight > maxHeight) {
content.classList.add("collapsed");
btn.style.display = "block";
}
btn.addEventListener("click", function() {
if (content.classList.contains("collapsed")) {
// 展开
content.style.maxHeight = content.scrollHeight + "px";
content.classList.remove("collapsed");
btn.innerText = "收起全部 ↑";
} else {
// 收起
content.style.maxHeight = maxHeight + "px";
content.classList.add("collapsed");
btn.innerText = "展开全部 ↓";
}
});
});
总结
- 需要 Markdown 解析吗? 需要,因为提问背景里经常会有行内代码或代码块,解析后排版更好看。
- 长度如何控制? 不要用后端
substr截断,改用 前端 CSSmax-height+ JSscrollHeight判断进行折叠。这种做法最稳妥,不会破坏代码块 HTML 闭合标签,且兼容性最好。