论坛(五)帖子编辑与话题

📅 2026/7/12 21:05:57
论坛(五)帖子编辑与话题
论坛五帖子编辑与话题本文是「AtomCode 驱动全栈开发」系列的第 16 篇。我们将深入分析论坛项目的帖子编辑页面、话题选择联动、Markdown 编辑器集成完整展示 PostEdit 视图、路由配置、Topic 模型及前后端联调的全过程。目录1. 引言2. 知识要点3. 完整代码3.1 路由配置3.2 Topic 模型3.3 PostEdit 视图3.4 MarkdownEditor 组件3.5 话题列表 API3.6 前端话题获取逻辑4. 逐段/逐行讲解4.1 路由守卫未登录不可发帖4.2 Topic 模型设计4.3 PostEdit 视图剖析4.4 MarkdownEditor 组件双向绑定4.5 话题获取与 select 联动5. AtomCode 实战6. 运行与验证7. 常见错误与排查8. 小结 下一篇预告1. 引言在论坛系统中「发布帖子」是核心功能之一。用户需要填写标题选择话题技术讨论、生活分享、项目展示、问答求助用Markdown编写正文实时预览渲染效果提交后跳转到详情页本博客将完整呈现从路由配置 → 数据库模型 → 前端组件 → 话题联动的全链路实现。2. 知识要点知识点说明Vue Router 嵌套路由 路由守卫requiresAuthmeta 字段 beforeEach拦截Flask Blueprint 路由/api/topicsGET 接口SQLAlchemy Model 关系Topic→Post一对多Vue 组件 v-model 通信自定义组件通过:valueinput实现双向绑定marked 库浏览器端 Markdown → HTML 渲染Axios 请求fetchTopics在created钩子中触发3. 完整代码3.1 路由配置文件/root/projects/forum/static/js/router.js/** * Vue Router — 路由配置 */window.routernewVueRouter({mode:history,base:/,routes:[{path:/,component:Home,name:home},{path:/login,component:Login,name:login},{path:/register,component:Register,name:register},{path:/posts/new,component:PostEdit,name:post-new,meta:{requiresAuth:true}},{path:/posts/:id,component:PostDetail,name:post-detail},],});// 路由守卫需要登录的页面跳转到 /loginrouter.beforeEach((to,from,next){if(to.metato.meta.requiresAuth!store.getters.isLoggedIn){next({name:login,query:{redirect:to.fullPath}});}else{next();}});3.2 Topic 模型文件/root/projects/forum/app/models.pyclassTopic(db.Model):__tablename__topicsiddb.Column(db.Integer,primary_keyTrue)namedb.Column(db.String(100),nullableFalse)slugdb.Column(db.String(100),uniqueTrue,nullableFalse,indexTrue)postsdb.relationship(Post,backreftopic,lazydynamic)defto_dict(self):return{id:self.id,name:self.name,slug:self.slug,}3.3 PostEdit 视图文件/root/projects/forum/static/js/views/PostEdit.js/** * PostEdit — 发布/编辑帖子 */window.PostEdit{data(){return{title:,body:,topicId:,error:,submitting:false,};},computed:{topics(){returnthis.$store.getters.topics;},},created(){if(this.topics.length0){this.$store.dispatch(fetchTopics);}},methods:{asyncsubmit(){if(!this.title.trim()){this.error请输入标题;return;}if(!this.body.trim()){this.error请输入内容;return;}if(!this.topicId){this.error请选择话题;return;}this.submittingtrue;this.error;try{constresawaitthis.$store.dispatch(createPost,{title:this.title.trim(),body:this.body,topic_id:this.topicId,});this.$router.push(/posts/res.post.id);}catch(e){this.error(e.responsee.response.datae.response.data.error)||发布失败;}finally{this.submittingfalse;}},},template:div stylemargin-top: 20px; div classcard div classcard-header发布新帖子/div div classcard-body div classalert alert-error v-iferror{{ error }}/div div classform-group label标题/label input classform-control v-modeltitle placeholder帖子标题 /div div classform-group label话题/label select classform-control v-modeltopicId option value-- 选择话题 --/option option v-fort in topics :keyt.id :valuet.id{{ t.name }}/option /select /div div classform-group label内容Markdown 格式/label markdown-editor v-modelbody/markdown-editor /div button classbtn btn-primary clicksubmit :disabledsubmitting {{ submitting ? 发布中... : 发布帖子 }} /button router-link to/ classbtn btn-outline stylemargin-left: 8px;取消/router-link /div /div /div,};3.4 MarkdownEditor 组件文件/root/projects/forum/static/js/components/MarkdownEditor.js/** * MarkdownEditor — Markdown 编辑器 实时预览 * emits: input (content) * 或者直接用 v-model通过 :value input 实现 */Vue.component(markdown-editor,{props:{value:{type:String,default:},placeholder:{type:String,default:使用 Markdown 书写内容...},minHeight:{type:String,default:300px},},computed:{previewHtml(){if(!this.value)returnp stylecolor:#888;预览区域/p;try{returnmarked.parse(this.value);}catch(e){returnp stylecolor:red;Markdown 解析出错/p;}},},methods:{onInput(e){this.$emit(input,e.target.value);},},template:div classmarkdown-editor div classeditor-pane textarea :valuevalue inputonInput :placeholderplaceholder :style{ minHeight: minHeight } /textarea /div div classpreview-pane div classpost-body v-htmlpreviewHtml/div /div /div,});3.5 话题列表 API文件/root/projects/forum/app/routes.pyapi_bp.route(/topics,methods[GET])deflist_topics():topicsTopic.query.all()returnjsonify({topics:[t.to_dict()fortintopics]})3.6 前端话题获取逻辑文件/root/projects/forum/static/js/store.js节选actions:{// ─── 话题 ──────────────────────────────────────asyncfetchTopics({commit}){constresawaitaxios.get(/api/topics);commit(SET_TOPICS,res.data.topics);returnres.data.topics;},// ...}4. 逐段/逐行讲解4.1 路由守卫未登录不可发帖在router.js中我们为/posts/new路径添加了meta: { requiresAuth: true }{path:/posts/new,component:PostEdit,name:post-new,meta:{requiresAuth:true}},然后通过beforeEach全局守卫检查router.beforeEach((to,from,next){if(to.metato.meta.requiresAuth!store.getters.isLoggedIn){next({name:login,query:{redirect:to.fullPath}});}else{next();}});关键逻辑to.meta.requiresAuth为true且用户未登录时强制跳转到/login通过query: { redirect: to.fullPath }传递原始路径登录后自动跳回这样即使手动在地址栏输入/posts/new未登录用户也会被拦截4.2 Topic 模型设计Topic模型非常简单只有三个字段字段类型说明idInteger, PK主键nameString(100)话题显示名如技术讨论slugString(100), unique, indexedURL 友好的标识如techslug字段的设计意义重大在 URL 参数中使用?topictech而非?topic_id1更语义化后续做话题筛选时前端直接传slug即可无需暴露数据库 IDto_dict()方法同时返回name和slug前端可灵活使用4.3 PostEdit 视图剖析data 中定义了四个关键状态字段类型用途titleString帖子标题双向绑定到 inputbodyStringMarkdown 正文双向绑定到markdown-editortopicIdString选中的话题 ID双向绑定到 selecterrorString表单校验错误信息submittingBoolean防止重复提交computed 中的 topicstopics(){returnthis.$store.getters.topics;},从 Vuex store 中读取话题列表。如果fetchTopics还没触发过store 中 topics 为空数组。created 钩子created(){if(this.topics.length0){this.$store.dispatch(fetchTopics);}},只在 topics 为空时才请求避免重复请求。如果其他页面如 Home 的 PostList已经加载过话题这里直接复用缓存。submit 方法三步校验标题不能为空正文不能为空必须选择话题校验通过后调this.$store.dispatch(createPost, ...)成功后this.$router.push(/posts/ res.post.id)跳转到详情页。4.4 MarkdownEditor 组件双向绑定markdown-editor是一个自定义组件通过v-model协议实现双向绑定markdown-editorv-modelbody/markdown-editorVue 的v-model对自定义组件等价于markdown-editor:valuebodyinputbody $event/markdown-editor因此在组件内部props 接收value父组件传递的当前值input事件发射this.$emit(input, e.target.value)将新值传回父组件computedpreviewHtml使用marked.parse()将 Markdown 转为 HTML通过v-html渲染到预览区布局左右分栏桌面端或上下堆叠移动端左编辑右预览实时可见。4.5 话题获取与 select 联动selectclassform-controlv-modeltopicIdoptionvalue-- 选择话题 --/optionoptionv-fort in topics:keyt.id:valuet.id{{ t.name }}/option/selectv-modeltopicId双向绑定选中的话题 IDv-for遍历topics数组:value绑定t.id显示t.name默认topicId为空字符串对应value的占位选项5. AtomCode 实战以下提示词可直接用于驱动 AtomCode 生成类似功能。提示词生成帖子编辑页面请基于 Flask Vue 2 生成一个帖子编辑页面 PostEdit 组件 1. 组件包含标题输入框、话题下拉选择框、Markdown 编辑器带实时预览 2. 话题数据从 Vuex store 的 topics 中获取如果为空则 dispatch fetchTopics 3. 表单校验标题、正文、话题都不能为空 4. 提交成功后跳转到 /posts/id 详情页 5. Markdown 编辑器使用 marked 库解析左右分栏布局 6. 路由配置 /posts/newrequiresAuth: true 7. 路由守卫拦截未登录用户跳转到 /login提示词生成 Topic 模型请生成一个 SQLAlchemy 的 Topic 模型 - 字段id, name, slug唯一索引 - 一对多关系Topic - Post - to_dict() 方法返回 id, name, slug - 同时生成 /api/topics 的 GET 接口返回所有话题列表6. 运行与验证启动后端cd/root/projects/forum python run.py预期输出* Serving Flask app app * Debug mode: on * Running on http://0.0.0.0:5000测试话题 APIcurlhttp://localhost:5000/api/topics预期输出{topics:[{id:1,name:技术讨论,slug:tech},{id:2,name:生活分享,slug:life},{id:3,name:项目展示,slug:projects},{id:4,name:问答求助,slug:qa}]}浏览器验证打开http://localhost:5000/点击「发布帖子」→ 被重定向到/login用testuser / 123456登录再次点击「发布帖子」→ 进入/posts/new填写标题、选择话题、写入 Markdown → 右侧实时预览点击「发布帖子」→ 跳转到详情页7. 常见错误与排查7.1 发布帖子按钮被重定向到登录页现象无论是否登录点击「发布帖子」都跳转到登录页。原因Navbar 中「发布帖子」链接没有判断登录状态直接渲染了router-link to/posts/new。解决在 Navbar 中用v-ifisLoggedIn包裹templatev-ifisLoggedInlirouter-linkto/posts/new发布帖子/router-link/li/template7.2 话题下拉列表为空现象进入发布页面后select 下拉没有选项。原因fetchTopics没有在created中触发或者后端/api/topics返回了错误。排查步骤打开浏览器开发者工具 → Network 标签刷新发布页查看是否有/api/topics请求如果没有检查PostEdit.created()中调用了fetchTopics如果有但返回空数组检查后端数据库是否 seed 了话题数据# 检查数据库话题cd/root/projects/forum python-cfrom app import create_app; from app.models import Topic; appcreate_app(); app.app_context().push(); print([t.name for t in Topic.query.all()])7.3 Markdown 预览显示原始文本现象预览区直接显示# 标题而非渲染后的 HTML。原因marked库未加载或marked.parse调用失败。排查检查浏览器控制台是否有marked is not defined错误。确认index.html中正确引入了 marked CDNscriptsrchttps://cdn.jsdelivr.net/npm/marked5.1.1/marked.min.js/script7.4 提交后无反应或跳转失败现象点击「发布帖子」按钮后页面没有跳转也没有错误提示。原因submitting状态卡在true因为createPost抛出了异常但catch中未正确处理。排查检查store.js中的createPostaction 是否正确导入了axios以及后端/api/postsPOST 接口是否工作正常。8. 小结 下一篇预告本篇我们完成了模块核心实现路由配置/posts/newrequiresAuth守卫Topic 模型id / name / slug 三字段一对多关系PostEdit 视图标题 话题选择 Markdown 编辑器MarkdownEditor左右分栏v-model双向绑定实时预览话题接口/api/topicsGET返回所有话题下一篇论坛六发布帖子与 Markdown 将深入分析后台发布接口的完整实现、前端发布逻辑、Markdown 后端渲染方案以及 XSS 风险与 bleach 净化建议。本文由 AtomCode 驱动生成代码基于真实项目/root/projects/forum。