From e98be48f2c4aa71b4c5aef3f26c2d75c4e14d34a 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: Fri, 7 Aug 2026 18:45:07 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E6=95=99=E8=82=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- college/__manifest__.py | 11 + college/models/chapter.py | 199 ++++++++++++++++++ college/security/groups.xml | 12 ++ college/security/ir.model.access.csv | 6 + college/static/src/scss/college_theme.scss | 149 +++++++++++++ .../static/src/scss/college_theme_dark.scss | 191 +++++++++++++++++ college/views/chapter_views.xml | 74 +++++++ college/views/course_views.xml | 58 +++-- college/views/homework_views.xml | 18 +- college/views/progress_views.xml | 20 +- college/views/training_views.xml | 16 +- 11 files changed, 697 insertions(+), 57 deletions(-) create mode 100644 college/static/src/scss/college_theme.scss create mode 100644 college/static/src/scss/college_theme_dark.scss diff --git a/college/__manifest__.py b/college/__manifest__.py index e47bcbb..9081f3c 100644 --- a/college/__manifest__.py +++ b/college/__manifest__.py @@ -45,6 +45,17 @@ 'views/about_page_views.xml', 'views/menus.xml', ], + 'assets': { + 'web.assets_backend': [ + 'college/static/src/scss/college_theme.scss', + ], + 'web.assets_web_dark': [ + 'college/static/src/scss/college_theme_dark.scss', + ], + 'web.assets_backend_lazy_dark': [ + 'college/static/src/scss/college_theme_dark.scss', + ], + }, 'application': True, 'installable': True, 'license': 'LGPL-3', diff --git a/college/models/chapter.py b/college/models/chapter.py index 59a94d3..0a2bee6 100644 --- a/college/models/chapter.py +++ b/college/models/chapter.py @@ -1,3 +1,5 @@ +import re + from odoo import api, fields, models @@ -53,6 +55,191 @@ class CourseChapterAttachment(models.Model): return super().write(vals) +class CourseChapterExercise(models.Model): + """章节训练习题——老师按章节布置的随堂简答题。 + + 一个章节可挂多道;学生可以自由作答(不计分、不强制), + 学员的回答存于 `yuthon.school.course.chapter.exercise.answer`。 + """ + + _name = 'yuthon.school.course.chapter.exercise' + _description = '章节训练习题' + _order = 'chapter_id, sequence, id' + _rec_name = 'exercise_label' + + chapter_id = fields.Many2one( + 'yuthon.school.course.chapter', string='章节', + required=True, ondelete='cascade', index=True) + sequence = fields.Integer(string='排序', default=10) + question = fields.Html(string='题目', sanitize=True, required=True) + analysis = fields.Html( + string='参考解析', sanitize=True, + help='给学员的参考答案/思路(仅展示,不参与评分)') + + exercise_label = fields.Char( + string='显示名', compute='_compute_exercise_label', + help='内嵌列表与表单标题用:第 N 题') + + answer_ids = fields.One2many( + 'yuthon.school.course.chapter.exercise.answer', 'exercise_id', + string='学员作答') + answer_count = fields.Integer( + string='作答人数', compute='_compute_answer_count', store=True) + course_id = fields.Many2one( + 'yuthon.school.course', string='课程', + related='chapter_id.course_id', store=True, index=True) + + # 当前学员的回答(depends_context('uid') 跟随当前登录用户切换) + my_answer_content = fields.Html( + string='我的回答(全文)', + compute='_compute_my_answer', depends_context='uid') + my_answer_preview = fields.Char( + string='我的回答', + compute='_compute_my_answer_preview', depends_context='uid', + help='去除 Html 标签后的纯文本预览,便于在列表里紧凑显示') + + @api.depends('sequence') + def _compute_exercise_label(self): + for rec in self: + rec.exercise_label = f'第 {rec.sequence} 题' + + @api.depends('answer_ids') + def _compute_answer_count(self): + for rec in self: + rec.answer_count = len(rec.answer_ids) + + @api.depends_context('uid') + def _compute_my_answer(self): + """当前登录学员对本习题的回答(未作答则为 False)。""" + Answer = self.env['yuthon.school.course.chapter.exercise.answer'] + student = self.env['yuthon.school.student'].sudo().search( + [('user_id', '=', self.env.uid)], limit=1) + answers = {} + if student: + for a in Answer.search([ + ('student_id', '=', student.id), + ('exercise_id', 'in', self.ids), + ]): + answers[a.exercise_id.id] = a + for rec in self: + ans = answers.get(rec.id) + rec.my_answer_content = ans.content if ans else False + + @api.depends_context('uid') + def _compute_my_answer_preview(self): + """去除 Html 标签后的纯文本预览(80 字以内)。""" + for rec in self: + content = rec.my_answer_content or '' + text = re.sub(r'<[^>]+>', ' ', content) + text = re.sub(r'\s+', ' ', text).strip() + if not text: + rec.my_answer_preview = '' + elif len(text) > 80: + rec.my_answer_preview = text[:80] + '…' + else: + rec.my_answer_preview = text + + def action_answer(self): + """学生端:弹窗回答本题。 + + 若当前用户已对本题作答过,则打开已有作答;否则新建一条空作答。 + """ + self.ensure_one() + Answer = self.env['yuthon.school.course.chapter.exercise.answer'] + student = self.env['yuthon.school.student'].sudo().search( + [('user_id', '=', self.env.uid)], limit=1) + if not student: + return {'type': 'ir.actions.act_window_close'} + answer = Answer.search([ + ('exercise_id', '=', self.id), + ('student_id', '=', student.id), + ], limit=1) + if not answer: + answer = Answer.create({ + 'exercise_id': self.id, + 'student_id': student.id, + }) + return { + 'type': 'ir.actions.act_window', + 'name': '作答', + 'res_model': 'yuthon.school.course.chapter.exercise.answer', + 'res_id': answer.id, + 'view_mode': 'form', + 'view_id': self.env.ref( + 'college.yuthon_school_course_chapter_exercise_answer_form').id, + 'views': [(self.env.ref( + 'college.yuthon_school_course_chapter_exercise_answer_form').id, + 'form')], + 'target': 'new', + } + + def action_view_answers(self): + """老师端:弹窗查看本题所有学员作答。 + + 弹出只读列表(学员 / 回答 / 作答时间),不可编辑(不参与评分)。 + """ + self.ensure_one() + return { + 'type': 'ir.actions.act_window', + 'name': f'学员作答 · {self.exercise_label}', + 'res_model': 'yuthon.school.course.chapter.exercise.answer', + 'view_mode': 'list,form', + 'domain': [('exercise_id', '=', self.id)], + 'views': [ + (self.env.ref( + 'college.yuthon_school_course_chapter_exercise_answer_view_tree_teacher').id, + 'list'), + (False, 'form'), + ], + 'target': 'new', + 'context': {'default_exercise_id': self.id}, + } + + +class CourseChapterExerciseAnswer(models.Model): + """学员对章节训练习题的回答(不计分、自由提交、可修改)。""" + + _name = 'yuthon.school.course.chapter.exercise.answer' + _description = '习题作答' + _order = 'exercise_id, student_id, id' + _rec_name = 'exercise_id' + + exercise_id = fields.Many2one( + 'yuthon.school.course.chapter.exercise', string='习题', + required=True, ondelete='cascade', index=True) + student_id = fields.Many2one( + 'yuthon.school.student', string='学员', + required=True, ondelete='cascade', index=True) + user_id = fields.Many2one( + 'res.users', string='账号', + related='student_id.user_id', store=True, index=True) + content = fields.Html( + string='我的回答', sanitize=True, + help='学员的作答内容,可随时修改(不计分)') + submit_time = fields.Datetime( + string='最后作答时间', default=fields.Datetime.now) + + chapter_id = fields.Many2one( + 'yuthon.school.course.chapter', string='章节', + related='exercise_id.chapter_id', store=True, index=True) + course_id = fields.Many2one( + 'yuthon.school.course', string='课程', + related='exercise_id.course_id', store=True, index=True) + question_display = fields.Html( + string='题目', related='exercise_id.question', readonly=True) + + @api.model_create_multi + def create(self, vals_list): + vals_list = [dict(v, submit_time=fields.Datetime.now()) + for v in vals_list] + return super().create(vals_list) + + def write(self, vals): + if 'content' in vals: + vals['submit_time'] = fields.Datetime.now() + return super().write(vals) + + class CourseChapter(models.Model): """课程章节——老师端在课程内容里维护,学生端作为学习目录。 @@ -81,6 +268,18 @@ class CourseChapter(models.Model): attachment_ids = fields.One2many( 'yuthon.school.course.chapter.attachment', 'chapter_id', string='附件') + # 训练习题:一个章节可挂多道简答题(学生可自由作答,不计分) + exercise_ids = fields.One2many( + 'yuthon.school.course.chapter.exercise', 'chapter_id', + string='训练习题') + exercise_count = fields.Integer( + string='习题数', compute='_compute_exercise_count', store=True) + + @api.depends('exercise_ids') + def _compute_exercise_count(self): + for chapter in self: + chapter.exercise_count = len(chapter.exercise_ids) + # 随堂笔记:老师按章节记录的富文本笔记(与章节编辑表单共用同一字段) notes = fields.Html(string='随堂笔记', sanitize=True) diff --git a/college/security/groups.xml b/college/security/groups.xml index 0992e75..77ecb3b 100644 --- a/college/security/groups.xml +++ b/college/security/groups.xml @@ -15,4 +15,16 @@ 可查看自己选课的课程、提交作业、查看自己的学习进展,不能创建课程。 + + + + 习题作答:学生仅能访问自己的回答 + + + [('user_id', '=', user.id)] + + + + + diff --git a/college/security/ir.model.access.csv b/college/security/ir.model.access.csv index c5de095..f435d03 100644 --- a/college/security/ir.model.access.csv +++ b/college/security/ir.model.access.csv @@ -30,6 +30,12 @@ access_yuthon_school_course_chapter_student,yuthon.school.course.chapter.student access_yuthon_school_course_chapter_attachment_admin,yuthon.school.course.chapter.attachment.admin,model_yuthon_school_course_chapter_attachment,base.group_system,1,1,1,1 access_yuthon_school_course_chapter_attachment_teacher,yuthon.school.course.chapter.attachment.teacher,model_yuthon_school_course_chapter_attachment,college.group_teacher,1,1,1,1 access_yuthon_school_course_chapter_attachment_student,yuthon.school.course.chapter.attachment.student,model_yuthon_school_course_chapter_attachment,college.group_student,1,0,0,0 +access_yuthon_school_course_chapter_exercise_admin,yuthon.school.course.chapter.exercise.admin,model_yuthon_school_course_chapter_exercise,base.group_system,1,1,1,1 +access_yuthon_school_course_chapter_exercise_teacher,yuthon.school.course.chapter.exercise.teacher,model_yuthon_school_course_chapter_exercise,college.group_teacher,1,1,1,1 +access_yuthon_school_course_chapter_exercise_student,yuthon.school.course.chapter.exercise.student,model_yuthon_school_course_chapter_exercise,college.group_student,1,0,0,0 +access_yuthon_school_course_chapter_exercise_answer_admin,yuthon.school.course.chapter.exercise.answer.admin,model_yuthon_school_course_chapter_exercise_answer,base.group_system,1,1,1,1 +access_yuthon_school_course_chapter_exercise_answer_teacher,yuthon.school.course.chapter.exercise.answer.teacher,model_yuthon_school_course_chapter_exercise_answer,college.group_teacher,1,0,0,0 +access_yuthon_school_course_chapter_exercise_answer_student,yuthon.school.course.chapter.exercise.answer.student,model_yuthon_school_course_chapter_exercise_answer,college.group_student,1,1,1,0 access_yuthon_school_training_admin,yuthon.school.training.admin,model_yuthon_school_training,base.group_system,1,1,1,1 access_yuthon_school_training_teacher,yuthon.school.training.teacher,model_yuthon_school_training,college.group_teacher,1,1,1,1 access_yuthon_school_training_student,yuthon.school.training.student,model_yuthon_school_training,college.group_student,1,1,1,1 diff --git a/college/static/src/scss/college_theme.scss b/college/static/src/scss/college_theme.scss new file mode 100644 index 0000000..70adb7c --- /dev/null +++ b/college/static/src/scss/college_theme.scss @@ -0,0 +1,149 @@ +// ============================================================ +// college 看板主题适配 +// 使用 Odoo SCSS 变量,自动跟随 udoo_om_ux 等主题的浅色/深色色板。 +// 需同时注册到 web.assets_backend(浅色)与 web.assets_web_dark +// / web.assets_backend_lazy_dark(深色)。 +// ============================================================ + +.college-course-kanban { + .college-kanban-card { + background-color: $o-view-background-color; + color: $o-main-text-color; + border: 1px solid rgba($o-main-text-color, 0.08); + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08); + border-radius: 8px; + overflow: hidden; + transition: transform 0.15s ease, box-shadow 0.15s ease; + + &:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12); + } + + .college-kanban-title { + font-size: 14px; + font-weight: 600; + margin-bottom: 6px; + color: $o-main-text-color; + } + + .college-kanban-meta { + font-size: 13px; + color: $o-gray-600; + + &.text-muted, + .text-muted { + color: $o-gray-600 !important; + } + + .fa { + color: $o-gray-500; + } + } + + .college-kanban-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 3px 10px; + border-radius: 12px; + font-size: 11px; + font-weight: 500; + } + } + + // ---------------- 老师端:蓝调专业风 ---------------- + &.college-kanban-teacher .college-kanban-card { + .college-kanban-badge-hours { + background: rgba($o-info, 0.15); + color: $o-main-text-color; + } + + .college-kanban-badge-students { + background: rgba($o-success, 0.15); + color: $o-main-text-color; + } + } + + // ---------------- 学生-我的课程:书本青蓝风 ---------------- + &.college-kanban-my .college-kanban-card { + .college-kanban-badge-type { + background: rgba($o-info, 0.85); + color: #ffffff; + } + + .college-kanban-badge-public { + background: rgba($o-success, 0.85); + color: #ffffff; + } + } + + // ---------------- 学生-课程学习:活力橙风 ---------------- + &.college-kanban-study .college-kanban-card { + .college-kanban-badge-type { + background: rgba($o-view-background-color, 0.92); + color: $o-danger; + } + + .college-kanban-badge-hours { + background: rgba($o-warning, 0.15); + color: $o-main-text-color; + } + + .college-kanban-badge-chapters { + background: rgba($o-info, 0.15); + color: $o-main-text-color; + } + } + + // ---------------- 学习进展:深森林绿 ---------------- + &.college-kanban-progress .college-kanban-card { + .college-kanban-title { + color: #ffffff; + } + + .college-kanban-badge-score { + background: rgba($o-view-background-color, 0.92); + color: $o-success; + } + + .college-kanban-progress-rate { + color: $o-main-text-color; + font-weight: 600; + } + } + + // ---------------- 我的实训:紫罗兰 ---------------- + &.college-kanban-training .college-kanban-card { + .college-kanban-title { + color: #ffffff; + } + + .college-kanban-badge-purple { + background: rgba($o-info, 0.15); + color: $o-main-text-color; + } + + .college-kanban-badge-pink { + background: rgba($o-danger, 0.15); + color: $o-main-text-color; + } + } + + // ---------------- 作业提交:珊瑚红 ---------------- + &.college-kanban-homework .college-kanban-card { + .college-kanban-title { + color: #ffffff; + } + + .college-kanban-badge-score { + background: rgba($o-view-background-color, 0.9); + color: $o-danger; + } + + .college-kanban-badge-state { + background: rgba($o-danger, 0.15); + color: $o-main-text-color; + } + } +} diff --git a/college/static/src/scss/college_theme_dark.scss b/college/static/src/scss/college_theme_dark.scss new file mode 100644 index 0000000..a79156c --- /dev/null +++ b/college/static/src/scss/college_theme_dark.scss @@ -0,0 +1,191 @@ +// ============================================================ +// college 看板「深色模式」专属样式 +// 本文件只注册在 web.assets_web_dark / web.assets_backend_lazy_dark, +// 这两个 bundle 仅当用户开启深色主题时才会加载,因此下面的颜色 +// 全部硬编码,不依赖任何主题 SCSS 变量(避免跨模块编译顺序导致 +// 变量未切换成深色值的问题)。 +// 深色页面底色约 #111827,卡片用略亮的 slate-800 (#1e293b) 形成层次。 +// ============================================================ + +// 顶部同级菜单段(如「我的课程 / 全部课程」)夜间白字加粗 +.nav-segment .nav-link { + color: #f1f5f9 !important; + font-weight: 600 !important; +} + +.nav-segment .nav-link.active { + color: #ffffff !important; + font-weight: 700 !important; +} + +.college-course-kanban { + .college-kanban-card { + background-color: #1e293b; + color: #f1f5f9; + border: 1px solid #334155; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35); + border-radius: 8px; + overflow: hidden; + transition: transform 0.15s ease, box-shadow 0.15s ease; + + &:hover { + transform: translateY(-2px); + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.45); + } + + .college-kanban-title { + font-size: 14px; + font-weight: 600; + margin-bottom: 6px; + color: #f8fafc !important; + } + + .college-kanban-body { + background-color: #1e293b; + } + + .college-kanban-meta { + font-size: 13px; + color: #94a3b8 !important; + + &.text-muted, + .text-muted { + color: #94a3b8 !important; + } + + .fa { + color: #64748b !important; + } + } + + .college-kanban-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 3px 10px; + border-radius: 12px; + font-size: 11px; + font-weight: 500; + } + } + + // ---------------- 老师端:蓝调专业风 ---------------- + &.college-kanban-teacher .college-kanban-card { + // 头部渐变稍压暗,避免与深色页面「糊」在一起,白字依旧清晰 + > div:first-child { + background: linear-gradient(135deg, #2563eb, #4f46e5) !important; + } + + .college-kanban-badge-hours { + background: rgba(96, 165, 250, 0.18); + color: #93c5fd !important; + } + + .college-kanban-badge-students { + background: rgba(52, 211, 153, 0.18); + color: #6ee7b7 !important; + } + } + + // ---------------- 学生-我的课程:书本青蓝风 ---------------- + &.college-kanban-my .college-kanban-card { + > div:first-child { + background: linear-gradient(135deg, #0ea5e9, #06b6d4) !important; + } + + .college-kanban-badge-type { + background: #38bdf8; + color: #0f172a !important; + } + + .college-kanban-badge-public { + background: #34d399; + color: #0f172a !important; + } + } + + // ---------------- 学生-课程学习:活力橙风 ---------------- + &.college-kanban-study .college-kanban-card { + > div:first-child { + background: linear-gradient(120deg, #f97316, #ef4444) !important; + } + + .college-kanban-badge-type { + background: rgba(251, 146, 60, 0.22); + color: #fdba74 !important; + } + + .college-kanban-badge-hours { + background: rgba(251, 191, 36, 0.18); + color: #fcd34d !important; + } + + .college-kanban-badge-chapters { + background: rgba(96, 165, 250, 0.18); + color: #93c5fd !important; + } + } + + // ---------------- 学习进展:深森林绿 ---------------- + &.college-kanban-progress .college-kanban-card { + > div:first-child { + background: linear-gradient(135deg, #2d6a4f, #40916c) !important; + } + + .college-kanban-title { + color: #f8fafc !important; + } + + .college-kanban-badge-score { + background: rgba(52, 211, 153, 0.2); + color: #6ee7b7 !important; + } + + .college-kanban-progress-rate { + color: #f1f5f9 !important; + font-weight: 600; + } + } + + // ---------------- 我的实训:紫罗兰 ---------------- + &.college-kanban-training .college-kanban-card { + > div:first-child { + background: linear-gradient(135deg, #7c3aed, #c026d3) !important; + } + + .college-kanban-title { + color: #f8fafc !important; + } + + .college-kanban-badge-purple { + background: rgba(167, 139, 250, 0.2); + color: #c4b5fd !important; + } + + .college-kanban-badge-pink { + background: rgba(244, 114, 182, 0.2); + color: #f9a8d4 !important; + } + } + + // ---------------- 作业提交:珊瑚红 ---------------- + &.college-kanban-homework .college-kanban-card { + > div:first-child { + background: linear-gradient(135deg, #e11d48, #f97316) !important; + } + + .college-kanban-title { + color: #f8fafc !important; + } + + .college-kanban-badge-score { + background: rgba(248, 113, 113, 0.2); + color: #fca5a5 !important; + } + + .college-kanban-badge-state { + background: rgba(248, 113, 113, 0.18); + color: #fca5a5 !important; + } + } +} diff --git a/college/views/chapter_views.xml b/college/views/chapter_views.xml index 7f1b113..0a74d5b 100644 --- a/college/views/chapter_views.xml +++ b/college/views/chapter_views.xml @@ -16,6 +16,19 @@ + + + + + + + +