refactor: 提取分配目标模态窗体为共享fragment,实现DRY
This commit is contained in:
+24
-77
@@ -128,61 +128,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 分配目标弹窗 -->
|
{% set modal_title = '分配目标' %}
|
||||||
<div class="modal fade" id="classAssignGoalModal" tabindex="-1">
|
{% include "fragments/assign_goal_modal.html" with context %}
|
||||||
<div class="modal-dialog modal-lg">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">分配目标 - <span id="classAssignGoalClassName"></span></h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">选择目标 *</label>
|
|
||||||
<select class="form-select" id="class-assign-goal-select"></select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">评估日期</label>
|
|
||||||
<div class="row g-2">
|
|
||||||
<div class="col-auto">
|
|
||||||
<select class="form-select" id="class-assign-assessment-days">
|
|
||||||
<option value="">指定天数</option>
|
|
||||||
<option value="15">15天后</option>
|
|
||||||
<option value="30">30天后</option>
|
|
||||||
<option value="60">60天后</option>
|
|
||||||
<option value="90" selected>90天后</option>
|
|
||||||
<option value="180">180天后</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-auto">
|
|
||||||
<input type="date" class="form-control" id="class-assign-assessment-date">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<a class="text-decoration-none" data-bs-toggle="collapse" href="#classAssignMoreSettings" role="button">
|
|
||||||
更多设置 ▼
|
|
||||||
</a>
|
|
||||||
<div class="collapse" id="classAssignMoreSettings">
|
|
||||||
<div class="card card-body mt-2">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">开始日期</label>
|
|
||||||
<input type="date" class="form-control" id="class-assign-start-date">
|
|
||||||
<small class="text-muted">默认立即开始</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer-with-top">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
|
||||||
<button type="button" class="btn btn-primary" id="confirm-class-assign-goal">分配</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
@@ -354,59 +301,59 @@ document.getElementById('addClassBtn').onclick = () => {
|
|||||||
|
|
||||||
// ========== 分配目标功能 ==========
|
// ========== 分配目标功能 ==========
|
||||||
|
|
||||||
let classAssignGoalModal;
|
let assignGoalModal;
|
||||||
|
|
||||||
document.getElementById('classAssignGoalModal').addEventListener('show.bs.modal', function () {
|
document.getElementById('assignGoalModal').addEventListener('show.bs.modal', function () {
|
||||||
loadClassGoalOptions();
|
loadClassGoalOptions();
|
||||||
});
|
});
|
||||||
|
|
||||||
function openAssignGoalModal(classId, className) {
|
function openAssignGoalModal(classId, className) {
|
||||||
currentClassId = classId;
|
currentClassId = classId;
|
||||||
document.getElementById('classAssignGoalClassName').textContent = className;
|
document.getElementById('assignGoalModalSubtitle').textContent = ' - ' + className;
|
||||||
// 重置表单
|
// 重置表单
|
||||||
document.getElementById('class-assign-assessment-days').value = '';
|
document.getElementById('assign-assessment-days').value = '';
|
||||||
document.getElementById('class-assign-assessment-date').value = '';
|
document.getElementById('assign-assessment-date').value = '';
|
||||||
document.getElementById('class-assign-start-date').value = '';
|
document.getElementById('assign-start-date').value = '';
|
||||||
classAssignGoalModal = new bootstrap.Modal(document.getElementById('classAssignGoalModal'));
|
assignGoalModal = new bootstrap.Modal(document.getElementById('assignGoalModal'));
|
||||||
classAssignGoalModal.show();
|
assignGoalModal.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadClassGoalOptions() {
|
async function loadClassGoalOptions() {
|
||||||
const res = await fetch('/api/goals');
|
const res = await fetch('/api/goals');
|
||||||
const goals = await res.json();
|
const goals = await res.json();
|
||||||
const select = document.getElementById('class-assign-goal-select');
|
const select = document.getElementById('assign-goal-select');
|
||||||
select.innerHTML = goals.map(g => `<option value="${g.id}">${escapeHtml(g.name)} [${g.level || '入门'} - ${g.category || '综合'}]</option>`).join('');
|
select.innerHTML = goals.map(g => `<option value="${g.id}">${escapeHtml(g.name)} [${g.level || '入门'} - ${g.category || '综合'}]</option>`).join('');
|
||||||
|
|
||||||
// 设置默认开始日期为今天,评估日期为90天后
|
// 设置默认开始日期为今天,评估日期为90天后
|
||||||
document.getElementById('class-assign-start-date').value = new Date().toISOString().split('T')[0];
|
document.getElementById('assign-start-date').value = new Date().toISOString().split('T')[0];
|
||||||
document.getElementById('class-assign-assessment-days').value = '90';
|
document.getElementById('assign-assessment-days').value = '90';
|
||||||
const d = new Date();
|
const d = new Date();
|
||||||
d.setDate(d.getDate() + 90);
|
d.setDate(d.getDate() + 90);
|
||||||
document.getElementById('class-assign-assessment-date').value = d.toISOString().split('T')[0];
|
document.getElementById('assign-assessment-date').value = d.toISOString().split('T')[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 评估日期联动
|
// 评估日期联动
|
||||||
document.getElementById('class-assign-assessment-days').addEventListener('change', function() {
|
document.getElementById('assign-assessment-days').addEventListener('change', function() {
|
||||||
const days = parseInt(this.value);
|
const days = parseInt(this.value);
|
||||||
if (days) {
|
if (days) {
|
||||||
const d = new Date();
|
const d = new Date();
|
||||||
d.setDate(d.getDate() + days);
|
d.setDate(d.getDate() + days);
|
||||||
document.getElementById('class-assign-assessment-date').value = d.toISOString().split('T')[0];
|
document.getElementById('assign-assessment-date').value = d.toISOString().split('T')[0];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('class-assign-assessment-date').addEventListener('change', function() {
|
document.getElementById('assign-assessment-date').addEventListener('change', function() {
|
||||||
if (this.value) {
|
if (this.value) {
|
||||||
document.getElementById('class-assign-assessment-days').value = '';
|
document.getElementById('assign-assessment-days').value = '';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 确认分配目标
|
// 确认分配目标
|
||||||
document.getElementById('confirm-class-assign-goal').addEventListener('click', async () => {
|
document.getElementById('confirm-assign-goal').addEventListener('click', async () => {
|
||||||
const goalId = document.getElementById('class-assign-goal-select').value;
|
const goalId = document.getElementById('assign-goal-select').value;
|
||||||
const assessmentDays = document.getElementById('class-assign-assessment-days').value;
|
const assessmentDays = document.getElementById('assign-assessment-days').value;
|
||||||
const assessmentDate = document.getElementById('class-assign-assessment-date').value;
|
const assessmentDate = document.getElementById('assign-assessment-date').value;
|
||||||
const startDate = document.getElementById('class-assign-start-date').value;
|
const startDate = document.getElementById('assign-start-date').value;
|
||||||
|
|
||||||
if (!goalId) { alert('请选择目标'); return; }
|
if (!goalId) { alert('请选择目标'); return; }
|
||||||
if (!assessmentDays && !assessmentDate) { alert('请选择评估方式'); return; }
|
if (!assessmentDays && !assessmentDate) { alert('请选择评估方式'); return; }
|
||||||
@@ -428,7 +375,7 @@ document.getElementById('confirm-class-assign-goal').addEventListener('click', a
|
|||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
classAssignGoalModal.hide();
|
assignGoalModal.hide();
|
||||||
alert(data.message + (data.skipped_count ? `(${data.skipped_count}个学员已分配此目标,跳过)` : ''));
|
alert(data.message + (data.skipped_count ? `(${data.skipped_count}个学员已分配此目标,跳过)` : ''));
|
||||||
} else {
|
} else {
|
||||||
const err = await res.json();
|
const err = await res.json();
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<!-- 分配目标 Modal (共享 Fragment) -->
|
||||||
|
<!--
|
||||||
|
通过 data 属性传递上下文:
|
||||||
|
- data-context: "student" 或 "class"
|
||||||
|
- data-target-id: 学员ID 或 班级ID
|
||||||
|
- data-modal-title: 模态窗体标题
|
||||||
|
-->
|
||||||
|
<div class="modal fade" id="assignGoalModal" tabindex="-1"
|
||||||
|
data-context="{{ context | default('student') }}"
|
||||||
|
data-target-id="{{ target_id | default('') }}"
|
||||||
|
data-modal-title="{{ modal_title | default('分配目标') }}">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">{{ modal_title | default('分配目标') }}<span id="assignGoalModalSubtitle"></span></h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">选择目标 *</label>
|
||||||
|
<select class="form-select" id="assign-goal-select"></select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">评估日期</label>
|
||||||
|
<div class="row g-2">
|
||||||
|
<div class="col-auto">
|
||||||
|
<select class="form-select" id="assign-assessment-days">
|
||||||
|
<option value="">指定天数</option>
|
||||||
|
<option value="15">15天后</option>
|
||||||
|
<option value="30">30天后</option>
|
||||||
|
<option value="60">60天后</option>
|
||||||
|
<option value="90" selected>90天后</option>
|
||||||
|
<option value="180">180天后</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<input type="date" class="form-control" id="assign-assessment-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<a class="text-decoration-none" data-bs-toggle="collapse" href="#assignMoreSettings" role="button">
|
||||||
|
更多设置 ▼
|
||||||
|
</a>
|
||||||
|
<div class="collapse" id="assignMoreSettings">
|
||||||
|
<div class="card card-body mt-2">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">开始日期</label>
|
||||||
|
<input type="date" class="form-control" id="assign-start-date">
|
||||||
|
<small class="text-muted">默认立即开始</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer-with-top">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||||
|
<button type="button" class="btn btn-primary" id="confirm-assign-goal">分配</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -252,61 +252,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 分配目标 Modal -->
|
{% include "fragments/assign_goal_modal.html" with context %}
|
||||||
<div class="modal fade" id="assignGoalModal" tabindex="-1">
|
|
||||||
<div class="modal-dialog modal-lg">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">分配目标</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">选择目标 *</label>
|
|
||||||
<select class="form-select" id="assign-goal-select"></select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">评估日期</label>
|
|
||||||
<div class="row g-2">
|
|
||||||
<div class="col-auto">
|
|
||||||
<select class="form-select" id="assign-assessment-days">
|
|
||||||
<option value="">指定天数</option>
|
|
||||||
<option value="15">15天后</option>
|
|
||||||
<option value="30">30天后</option>
|
|
||||||
<option value="60">60天后</option>
|
|
||||||
<option value="90" selected>90天后</option>
|
|
||||||
<option value="180">180天后</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-auto">
|
|
||||||
<input type="date" class="form-control" id="assign-assessment-date">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<a class="text-decoration-none" data-bs-toggle="collapse" href="#assignMoreSettings" role="button">
|
|
||||||
更多设置 ▼
|
|
||||||
</a>
|
|
||||||
<div class="collapse" id="assignMoreSettings">
|
|
||||||
<div class="card card-body mt-2">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">开始日期</label>
|
|
||||||
<input type="date" class="form-control" id="assign-start-date">
|
|
||||||
<small class="text-muted">默认立即开始</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer-with-top">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
|
||||||
<button type="button" class="btn btn-primary" id="confirm-assign-goal">分配</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 调整目标 Modal -->
|
<!-- 调整目标 Modal -->
|
||||||
<div class="modal fade" id="adjustGoalModal" tabindex="-1">
|
<div class="modal fade" id="adjustGoalModal" tabindex="-1">
|
||||||
|
|||||||
@@ -143,6 +143,54 @@ deploy: v1.2.0 生产环境部署
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 代码复用规范(铁律)
|
||||||
|
|
||||||
|
### DRY 原则(Don't Repeat Yourself)
|
||||||
|
|
||||||
|
> **复制粘贴代码是严重的 Code Smell。任何重复代码必须提取为可复用组件。**
|
||||||
|
|
||||||
|
### 前端模板复用
|
||||||
|
|
||||||
|
| 场景 | 复用方式 |
|
||||||
|
|------|---------|
|
||||||
|
| 多个页面共用 Modal | 提取为 `app/templates/fragments/` 下的 Fragment,用 `{% include %}` 引用 |
|
||||||
|
| 多个页面共用样式 | 在 `base.html` 或页面级别 `<style>` 中定义 |
|
||||||
|
| 多个页面共用 JavaScript 函数 | 提取到 `base.html` 的 `<script>` 或单独 JS 文件 |
|
||||||
|
|
||||||
|
### Fragment 提取规范
|
||||||
|
|
||||||
|
1. **创建 Fragment 文件**:`app/templates/fragments/{component_name}.html`
|
||||||
|
2. **通过 data 属性传递上下文**:
|
||||||
|
```html
|
||||||
|
<!-- 学员详情用 -->
|
||||||
|
<div id="assignGoalModal" data-student-id="{{ student.id }}">
|
||||||
|
|
||||||
|
<!-- 班级管理用 -->
|
||||||
|
<div id="assignGoalModal" data-class-id="{{ class.id }}">
|
||||||
|
```
|
||||||
|
3. **JavaScript 读取 data 属性决定行为**:
|
||||||
|
```javascript
|
||||||
|
const studentId = modalElement.dataset.studentId;
|
||||||
|
const classId = modalElement.dataset.classId;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 检查清单
|
||||||
|
|
||||||
|
新增代码前,先问:
|
||||||
|
- [ ] 这个 UI 组件是否在其他页面也存在?
|
||||||
|
- [ ] 这段 JavaScript 函数是否已有类似实现?
|
||||||
|
- [ ] 这个 HTML 结构是否可以直接复用?
|
||||||
|
|
||||||
|
如果答案是"是",**先提取复用组件,再使用**。
|
||||||
|
|
||||||
|
### 禁止的行为
|
||||||
|
|
||||||
|
❌ 直接复制粘贴另一个页面的 Modal HTML
|
||||||
|
❌ 复制粘贴另一个页面的 JavaScript 函数
|
||||||
|
❌ 在不同文件里写功能完全相同的函数,仅变量名不同
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 版本历史
|
## 版本历史
|
||||||
|
|
||||||
| 版本 | 日期 | 说明 |
|
| 版本 | 日期 | 说明 |
|
||||||
|
|||||||
Reference in New Issue
Block a user