From 6efe2130b8e72566dd420615d4c0d7e8ac34860f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=B9=8F=E5=AE=87?= <9664676+pengyuthon@user.noreply.gitee.com> Date: Mon, 3 Aug 2026 11:07:03 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A8=A1=E5=9D=97=E8=BF=81=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yuthon_school/__manifest__.py | 3 + yuthon_school/data/demo.xml | 203 +++++++++++++++++++++ yuthon_school/models/__init__.py | 4 + yuthon_school/models/course.py | 111 +++++++++++ yuthon_school/models/exam.py | 71 +++++++ yuthon_school/models/forum_post.py | 65 +++++++ yuthon_school/models/syllabus.py | 46 +++++ yuthon_school/security/ir.model.access.csv | 4 + yuthon_school/static/description/icon1.png | Bin 0 -> 2884 bytes yuthon_school/views/course_views.xml | 148 +++++++++++++++ yuthon_school/views/exam_views.xml | 84 +++++++++ yuthon_school/views/forum_post_views.xml | 91 +++++++++ yuthon_school/views/menus.xml | 58 +++--- 13 files changed, 856 insertions(+), 32 deletions(-) create mode 100644 yuthon_school/models/course.py create mode 100644 yuthon_school/models/exam.py create mode 100644 yuthon_school/models/forum_post.py create mode 100644 yuthon_school/models/syllabus.py create mode 100644 yuthon_school/static/description/icon1.png create mode 100644 yuthon_school/views/course_views.xml create mode 100644 yuthon_school/views/exam_views.xml create mode 100644 yuthon_school/views/forum_post_views.xml diff --git a/yuthon_school/__manifest__.py b/yuthon_school/__manifest__.py index e42ef5c..d755765 100644 --- a/yuthon_school/__manifest__.py +++ b/yuthon_school/__manifest__.py @@ -23,6 +23,9 @@ 'views/progress_views.xml', 'views/elective_views.xml', 'views/progress_wizard_views.xml', + 'views/course_views.xml', + 'views/exam_views.xml', + 'views/forum_post_views.xml', 'views/menus.xml', ], 'demo': [ diff --git a/yuthon_school/data/demo.xml b/yuthon_school/data/demo.xml index 6f88dba..c9cafbb 100644 --- a/yuthon_school/data/demo.xml +++ b/yuthon_school/data/demo.xml @@ -304,4 +304,207 @@ 2026-07-28 08:00:00 + + + + Odoo 18 前端 OWL 实战 + + intermediate + published + 面向前端开发者的 OWL 组件化开发教程,覆盖组件、事件、状态与渲染。 + + + + OWL 组件基础 + + 10 + 2.5 + 组件定义、props、状态 + Component、useState、props + + + + 事件与渲染 + + 20 + 3.0 + 事件绑定、条件渲染、列表渲染 + t-on、t-if、t-foreach + + + + + + 模块结构详解 + + + 12:30 + beginner + demo_lesson_01.mp4 + AAAA + 讲解 Odoo 模块的目录结构与清单文件。 + __manifest__.py、data 加载顺序 + + + + 模型与字段类型 + + + 18:05 + intermediate + demo_lesson_02.mp4 + AAAA + Char/Integer/Selection 与关系字段。 + + + + OWL 组件基础 + + + 15:40 + intermediate + demo_lesson_03.mp4 + AAAA + OWL 组件定义与状态管理。 + + + + + + Odoo 18 全栈开发工程师 + ODOO-101 + + + beginner + ongoing + 零基础到独立开发 Odoo 18 模块,覆盖模型、视图、权限、控制器。 + + + + + + Odoo 前端 OWL 专项 + ODOO-201 + + + intermediate + recruiting + 深入 OWL 框架,掌握组件化前端开发。 + + + + + + + + 模块开发与权限设计 + 第 1-2 周 + + + 掌握模块清单、模型定义与 ACL 权限机制。 + 通过动手实现一个完整模块,理解 Odoo 的开发全流程。

]]>
+
+ + + 视图与前端交互 + 第 3-4 周 + + + 熟练使用 list/form/search/kanban 四种视图。 + + + + OWL 组件与状态管理 + 第 1-2 周 + + + 能用 OWL 编写组件化前端页面。 + + + + + + 模块开发单元测验 + + + quiz + 2026-07-21 + 92 + done + 概念清晰,注意 compute 字段的 store 用法。 + + + + 模块开发单元测验 + + + quiz + 2026-07-21 + 55 + done + 关系字段部分需要加强。 + + + + OWL 期中练习 + + + practice + 2026-07-28 + 88 + done + + + + OWL 期中练习 + + + practice + 2026-07-28 + 0 + pending + + + + + + Many2many 的 (6,0,[ids]) 怎么理解? + question + 陈小明 + student + + 在 demo 数据里看到 [(6,0,[ids])],这个 6 和 0 分别代表什么?

]]>
+ 3 +
+ + + 回复:Many2many 的 (6,0,[ids]) + + discuss + 张老师 + teacher + 6 表示「替换」,0 是保留位,[ids] 是要关联的记录 id 列表。整体含义是把当前记录的 M2M 关系替换为给定列表。

]]>
+ 5 +
+ + + 分享:compute 字段一定要加 store 吗? + share + 林小芳 + student + + 不是必须。不加 store 每次读取都实时计算;加 store 会落库并随依赖字段变化重算,适合列表排序/搜索场景。

]]>
+ 7 +
+ + + OWL 里 t-on 和原生 onclick 有什么区别? + question + 郑小伟 + student + + 用 t-on 绑定事件和直接写 onclick 有什么不同?

]]>
+ 1 +
+ diff --git a/yuthon_school/models/__init__.py b/yuthon_school/models/__init__.py index 25596e2..f5edd7c 100644 --- a/yuthon_school/models/__init__.py +++ b/yuthon_school/models/__init__.py @@ -4,3 +4,7 @@ from . import textbook from . import organization from . import progress from . import elective +from . import course +from . import syllabus +from . import exam +from . import forum_post diff --git a/yuthon_school/models/course.py b/yuthon_school/models/course.py new file mode 100644 index 0000000..d9712f1 --- /dev/null +++ b/yuthon_school/models/course.py @@ -0,0 +1,111 @@ +from odoo import fields, models, api + + +class Course(models.Model): + _name = 'yuthon.school.course' + _description = '课程' + _order = 'sequence, name' + + name = fields.Char( + string='课程名称', + required=True, + help='如:Odoo 18 全栈开发工程师认证课程', + ) + code = fields.Char( + string='课程编号', + help='如:ODOO-101', + ) + sequence = fields.Integer( + string='排序', + default=10, + ) + category_id = fields.Many2one( + 'yuthon.school.category', + string='学习分类', + ondelete='restrict', + ) + teacher_id = fields.Many2one( + 'yuthon.school.teacher', + string='授课教师', + ) + difficulty = fields.Selection( + [('beginner', '入门'), ('intermediate', '进阶'), ('advanced', '高级')], + string='难度等级', + default='beginner', + ) + state = fields.Selection( + [('draft', '草稿'), ('recruiting', '招生中'), + ('ongoing', '进行中'), ('finished', '已结课')], + string='状态', + default='draft', + ) + description = fields.Text( + string='课程简介', + help='课程目标、适合人群、考核方式等', + ) + textbook_ids = fields.Many2many( + 'yuthon.school.textbook', + 'yuthon_course_textbook_rel', + 'course_id', 'textbook_id', + string='关联教材', + help='本课程涵盖的教材(Many2many 演示)', + ) + syllabus_ids = fields.One2many( + 'yuthon.school.syllabus', + 'course_id', + string='课程大纲', + ) + student_ids = fields.Many2many( + 'yuthon.school.student', + 'yuthon_course_student_rel', + 'course_id', 'student_id', + string='选课学生', + help='选修本课程的学生(Many2many 演示)', + ) + total_hours = fields.Float( + string='总学时', + compute='_compute_total_hours', + store=True, + help='自动汇总关联教材各章学时', + ) + enrollment_count = fields.Integer( + string='选课人数', + compute='_compute_counts', + store=True, + ) + syllabus_count = fields.Integer( + string='大纲条目', + compute='_compute_counts', + store=True, + ) + active = fields.Boolean( + string='启用', + default=True, + ) + + @api.depends('textbook_ids.chapter_ids.hours') + def _compute_total_hours(self): + for record in self: + record.total_hours = sum( + chapter.hours + for textbook in record.textbook_ids + for chapter in textbook.chapter_ids + ) + + @api.depends('student_ids', 'syllabus_ids') + def _compute_counts(self): + for record in self: + record.enrollment_count = len(record.student_ids) + record.syllabus_count = len(record.syllabus_ids) + + def action_recruit(self): + """开始招生""" + self.write({'state': 'recruiting'}) + + def action_start(self): + """开课""" + self.write({'state': 'ongoing'}) + + def action_finish(self): + """结课""" + self.write({'state': 'finished'}) diff --git a/yuthon_school/models/exam.py b/yuthon_school/models/exam.py new file mode 100644 index 0000000..7c001b0 --- /dev/null +++ b/yuthon_school/models/exam.py @@ -0,0 +1,71 @@ +from odoo import fields, models, api + + +class Exam(models.Model): + _name = 'yuthon.school.exam' + _description = '训练考核' + _order = 'exam_date desc, id' + + name = fields.Char( + string='考核名称', + required=True, + help='如:Odoo 18 模块开发单元测验', + ) + course_id = fields.Many2one( + 'yuthon.school.course', + string='课程', + ondelete='restrict', + ) + student_id = fields.Many2one( + 'yuthon.school.student', + string='学生', + required=True, + ondelete='cascade', + ) + exam_type = fields.Selection( + [('quiz', '测验'), ('practice', '练习'), + ('midterm', '期中'), ('final', '期末')], + string='考核类型', + default='quiz', + ) + exam_date = fields.Date( + string='考核日期', + ) + score = fields.Float( + string='得分', + default=0.0, + ) + total_score = fields.Float( + string='满分', + default=100.0, + ) + pass_score = fields.Float( + string='及格线', + default=60.0, + ) + state = fields.Selection( + [('pending', '待考'), ('done', '已考')], + string='状态', + default='pending', + ) + is_pass = fields.Boolean( + string='是否及格', + compute='_compute_is_pass', + store=True, + ) + note = fields.Text( + string='评语', + ) + active = fields.Boolean( + string='启用', + default=True, + ) + + @api.depends('score', 'pass_score') + def _compute_is_pass(self): + for record in self: + record.is_pass = bool(record.score >= record.pass_score) + + def action_mark_done(self): + """标记已考""" + self.write({'state': 'done'}) diff --git a/yuthon_school/models/forum_post.py b/yuthon_school/models/forum_post.py new file mode 100644 index 0000000..2731d95 --- /dev/null +++ b/yuthon_school/models/forum_post.py @@ -0,0 +1,65 @@ +from odoo import fields, models, api + + +class ForumPost(models.Model): + _name = 'yuthon.school.forum_post' + _description = '学习交流' + _order = 'create_date desc, id' + + name = fields.Char( + string='主题', + required=True, + help='如:Many2many 的 (6,0,[ids]) 怎么理解?', + ) + content = fields.Html( + string='内容', + sanitize=True, + ) + author_name = fields.Char( + string='作者', + help='发帖人姓名(教学演示用,避免关联复杂权限)', + ) + author_type = fields.Selection( + [('student', '学生'), ('teacher', '老师')], + string='作者类型', + default='student', + ) + course_id = fields.Many2one( + 'yuthon.school.course', + string='关联课程', + ondelete='restrict', + ) + post_type = fields.Selection( + [('question', '提问'), ('share', '分享'), ('discuss', '讨论')], + string='类型', + default='question', + ) + like_count = fields.Integer( + string='点赞数', + default=0, + ) + parent_id = fields.Many2one( + 'yuthon.school.forum_post', + string='回复对象', + ondelete='cascade', + help='非空表示这是某帖的回复(实现楼中楼)', + ) + reply_ids = fields.One2many( + 'yuthon.school.forum_post', + 'parent_id', + string='回复', + ) + reply_count = fields.Integer( + string='回复数', + compute='_compute_reply_count', + store=True, + ) + active = fields.Boolean( + string='启用', + default=True, + ) + + @api.depends('reply_ids') + def _compute_reply_count(self): + for record in self: + record.reply_count = len(record.reply_ids) diff --git a/yuthon_school/models/syllabus.py b/yuthon_school/models/syllabus.py new file mode 100644 index 0000000..2f92489 --- /dev/null +++ b/yuthon_school/models/syllabus.py @@ -0,0 +1,46 @@ +from odoo import fields, models, api + + +class Syllabus(models.Model): + _name = 'yuthon.school.syllabus' + _description = '课程大纲' + _order = 'sequence, id' + + name = fields.Char( + string='大纲条目', + required=True, + help='如:模块开发与权限设计', + ) + sequence = fields.Integer( + string='排序', + default=10, + ) + week = fields.Char( + string='周次', + help='如:第 1 周', + ) + course_id = fields.Many2one( + 'yuthon.school.course', + string='所属课程', + required=True, + ondelete='cascade', + ) + chapter_ids = fields.Many2many( + 'yuthon.school.chapter', + 'yuthon_syllabus_chapter_rel', + 'syllabus_id', 'chapter_id', + string='覆盖章节', + help='本条目对应的教材章节(Many2many 演示)', + ) + objective = fields.Text( + string='教学目标', + help='学完本条目应掌握的能力', + ) + content = fields.Html( + string='大纲内容', + sanitize=True, + ) + active = fields.Boolean( + string='启用', + default=True, + ) diff --git a/yuthon_school/security/ir.model.access.csv b/yuthon_school/security/ir.model.access.csv index ab882ff..60d55be 100644 --- a/yuthon_school/security/ir.model.access.csv +++ b/yuthon_school/security/ir.model.access.csv @@ -10,3 +10,7 @@ access_yuthon_school_student_user,yuthon.school.student.user,model_yuthon_school access_yuthon_school_progress_user,yuthon.school.progress.user,model_yuthon_school_progress,base.group_user,1,1,1,1 access_yuthon_school_elective_user,yuthon.school.elective.user,model_yuthon_school_elective,base.group_user,1,1,1,1 access_yuthon_school_progress_wizard_user,yuthon.school.progress.wizard.user,model_yuthon_school_progress_wizard,base.group_user,1,1,1,1 +access_yuthon_school_course_user,yuthon.school.course.user,model_yuthon_school_course,base.group_user,1,1,1,1 +access_yuthon_school_syllabus_user,yuthon.school.syllabus.user,model_yuthon_school_syllabus,base.group_user,1,1,1,1 +access_yuthon_school_exam_user,yuthon.school.exam.user,model_yuthon_school_exam,base.group_user,1,1,1,1 +access_yuthon_school_forum_post_user,yuthon.school.forum_post.user,model_yuthon_school_forum_post,base.group_user,1,1,1,1 diff --git a/yuthon_school/static/description/icon1.png b/yuthon_school/static/description/icon1.png new file mode 100644 index 0000000000000000000000000000000000000000..d524c03c523707d41b157d3a778c785ce490dc48 GIT binary patch literal 2884 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 literal 0 HcmV?d00001 diff --git a/yuthon_school/views/course_views.xml b/yuthon_school/views/course_views.xml new file mode 100644 index 0000000..4737ed2 --- /dev/null +++ b/yuthon_school/views/course_views.xml @@ -0,0 +1,148 @@ + + + + + yuthon.school.course.list + yuthon.school.course + + + + + + + + + + + + + + + + + + yuthon.school.course.form + yuthon.school.course + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + +
+ + + + + + + + + +
+ + + + + +
+
+
+ +
+
+ + + + yuthon.school.course.search + yuthon.school.course + + + + + + + + + + + + + + + 课程管理 + yuthon.school.course + list,form + + +

+ 暂无课程,点击「新建」创建一门课程(关联教材、大纲与选课学生)。 +

+
+
+
diff --git a/yuthon_school/views/exam_views.xml b/yuthon_school/views/exam_views.xml new file mode 100644 index 0000000..37455b0 --- /dev/null +++ b/yuthon_school/views/exam_views.xml @@ -0,0 +1,84 @@ + + + + + yuthon.school.exam.list + yuthon.school.exam + + + + + + + + + + + + + + + + + + yuthon.school.exam.form + yuthon.school.exam + +
+
+
+ + + + + + + + + + + + + + + + + + +
+
+
+ + + + yuthon.school.exam.search + yuthon.school.exam + + + + + + + + + + + + + + + + 训练考核 + yuthon.school.exam + list,form + + +

+ 暂无考核记录,点击「新建」登记一次测验、练习或考试成绩。 +

+
+
+
diff --git a/yuthon_school/views/forum_post_views.xml b/yuthon_school/views/forum_post_views.xml new file mode 100644 index 0000000..76154da --- /dev/null +++ b/yuthon_school/views/forum_post_views.xml @@ -0,0 +1,91 @@ + + + + + yuthon.school.forum_post.list + yuthon.school.forum_post + + + + + + + + + + + + + + + + + yuthon.school.forum_post.form + yuthon.school.forum_post + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + + + yuthon.school.forum_post.search + yuthon.school.forum_post + + + + + + + + + + + + + 学习交流 + yuthon.school.forum_post + list,form + + +

+ 还没有交流帖,点击「新建」发起一个提问、分享或讨论。 +

+
+
+
diff --git a/yuthon_school/views/menus.xml b/yuthon_school/views/menus.xml index 90e5cfe..64a0cfc 100644 --- a/yuthon_school/views/menus.xml +++ b/yuthon_school/views/menus.xml @@ -1,46 +1,41 @@ + - - + - - - - - - - - - - + + + + + + -