Upload 67 files
Browse files- app/__pycache__/models.cpython-313.pyc +0 -0
- app/models.py +2 -0
- app/routes/__pycache__/admin.cpython-313.pyc +0 -0
- app/routes/__pycache__/user.cpython-313.pyc +0 -0
- app/routes/admin.py +40 -2
- app/routes/user.py +16 -3
- app/services/__pycache__/bootstrap.cpython-313.pyc +0 -0
- app/services/bootstrap.py +2 -0
- app/templates/admin_activities.html +20 -2
- app/templates/admin_activity_edit.html +16 -1
- app/templates/admin_dashboard.html +3 -1
app/__pycache__/models.cpython-313.pyc
CHANGED
|
Binary files a/app/__pycache__/models.cpython-313.pyc and b/app/__pycache__/models.cpython-313.pyc differ
|
|
|
app/models.py
CHANGED
|
@@ -72,6 +72,7 @@ class Activity(TimestampMixin, Base):
|
|
| 72 |
description: Mapped[str] = mapped_column(Text, default="")
|
| 73 |
start_at: Mapped[datetime] = mapped_column(DateTime)
|
| 74 |
deadline_at: Mapped[datetime] = mapped_column(DateTime)
|
|
|
|
| 75 |
leaderboard_visible: Mapped[bool] = mapped_column(Boolean, default=True)
|
| 76 |
clue_interval_minutes: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
| 77 |
created_by_id: Mapped[int] = mapped_column(ForeignKey("admins.id"))
|
|
@@ -150,3 +151,4 @@ class Submission(TimestampMixin, Base):
|
|
| 150 |
back_populates="assigned_submissions",
|
| 151 |
foreign_keys=[assigned_admin_id],
|
| 152 |
)
|
|
|
|
|
|
| 72 |
description: Mapped[str] = mapped_column(Text, default="")
|
| 73 |
start_at: Mapped[datetime] = mapped_column(DateTime)
|
| 74 |
deadline_at: Mapped[datetime] = mapped_column(DateTime)
|
| 75 |
+
is_visible: Mapped[bool] = mapped_column(Boolean, default=True)
|
| 76 |
leaderboard_visible: Mapped[bool] = mapped_column(Boolean, default=True)
|
| 77 |
clue_interval_minutes: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
| 78 |
created_by_id: Mapped[int] = mapped_column(ForeignKey("admins.id"))
|
|
|
|
| 151 |
back_populates="assigned_submissions",
|
| 152 |
foreign_keys=[assigned_admin_id],
|
| 153 |
)
|
| 154 |
+
|
app/routes/__pycache__/admin.cpython-313.pyc
CHANGED
|
Binary files a/app/routes/__pycache__/admin.cpython-313.pyc and b/app/routes/__pycache__/admin.cpython-313.pyc differ
|
|
|
app/routes/__pycache__/user.cpython-313.pyc
CHANGED
|
Binary files a/app/routes/__pycache__/user.cpython-313.pyc and b/app/routes/__pycache__/user.cpython-313.pyc differ
|
|
|
app/routes/admin.py
CHANGED
|
@@ -91,6 +91,7 @@ def parse_activity_fields(form) -> dict:
|
|
| 91 |
start_raw = str(form.get("start_at", "")).strip()
|
| 92 |
deadline_raw = str(form.get("deadline_at", "")).strip()
|
| 93 |
clue_interval_raw = str(form.get("clue_interval_minutes", "")).strip()
|
|
|
|
| 94 |
leaderboard_visible = form.get("leaderboard_visible") == "on"
|
| 95 |
|
| 96 |
if not title or not start_raw or not deadline_raw:
|
|
@@ -116,6 +117,7 @@ def parse_activity_fields(form) -> dict:
|
|
| 116 |
"start_at": start_at,
|
| 117 |
"deadline_at": deadline_at,
|
| 118 |
"clue_interval_minutes": clue_interval_minutes,
|
|
|
|
| 119 |
"leaderboard_visible": leaderboard_visible,
|
| 120 |
}
|
| 121 |
|
|
@@ -191,6 +193,14 @@ def cleanup_task_files(task: Task) -> None:
|
|
| 191 |
delete_file_if_exists(task.clue_image_path, settings.task_media_root)
|
| 192 |
|
| 193 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
def normalize_path_key(file_path: str | Path) -> str:
|
| 195 |
return str(Path(file_path).resolve())
|
| 196 |
|
|
@@ -1087,8 +1097,31 @@ async def delete_task(task_id: int, request: Request, db: Session = Depends(get_
|
|
| 1087 |
add_flash(request, "success", "任务已删除,剩余任务顺序和线索发布时间已自动更新。")
|
| 1088 |
return redirect(f"/admin/activities/{activity_id}/edit")
|
| 1089 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1090 |
@router.post("/admin/activities/{activity_id}/visibility")
|
| 1091 |
-
async def
|
| 1092 |
admin = require_admin(request, db)
|
| 1093 |
if not admin:
|
| 1094 |
return redirect("/admin")
|
|
@@ -1098,10 +1131,11 @@ async def update_leaderboard_visibility(activity_id: int, request: Request, db:
|
|
| 1098 |
raise HTTPException(status_code=404, detail="活动不存在")
|
| 1099 |
|
| 1100 |
form = await request.form()
|
|
|
|
| 1101 |
activity.leaderboard_visible = form.get("leaderboard_visible") == "on"
|
| 1102 |
db.add(activity)
|
| 1103 |
db.commit()
|
| 1104 |
-
add_flash(request, "success", f"已更新 {activity.title} 的
|
| 1105 |
return redirect("/admin/activities")
|
| 1106 |
|
| 1107 |
@router.get("/admin/reviews")
|
|
@@ -1339,3 +1373,7 @@ async def delete_managed_image(request: Request, db: Session = Depends(get_db)):
|
|
| 1339 |
|
| 1340 |
|
| 1341 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
start_raw = str(form.get("start_at", "")).strip()
|
| 92 |
deadline_raw = str(form.get("deadline_at", "")).strip()
|
| 93 |
clue_interval_raw = str(form.get("clue_interval_minutes", "")).strip()
|
| 94 |
+
is_visible = form.get("is_visible") == "on"
|
| 95 |
leaderboard_visible = form.get("leaderboard_visible") == "on"
|
| 96 |
|
| 97 |
if not title or not start_raw or not deadline_raw:
|
|
|
|
| 117 |
"start_at": start_at,
|
| 118 |
"deadline_at": deadline_at,
|
| 119 |
"clue_interval_minutes": clue_interval_minutes,
|
| 120 |
+
"is_visible": is_visible,
|
| 121 |
"leaderboard_visible": leaderboard_visible,
|
| 122 |
}
|
| 123 |
|
|
|
|
| 193 |
delete_file_if_exists(task.clue_image_path, settings.task_media_root)
|
| 194 |
|
| 195 |
|
| 196 |
+
def cleanup_activity_files(activity: Activity) -> None:
|
| 197 |
+
all_submissions: list[Submission] = []
|
| 198 |
+
for task in activity.tasks:
|
| 199 |
+
all_submissions.extend(task.submissions)
|
| 200 |
+
cleanup_task_files(task)
|
| 201 |
+
cleanup_submission_files(all_submissions)
|
| 202 |
+
|
| 203 |
+
|
| 204 |
def normalize_path_key(file_path: str | Path) -> str:
|
| 205 |
return str(Path(file_path).resolve())
|
| 206 |
|
|
|
|
| 1097 |
add_flash(request, "success", "任务已删除,剩余任务顺序和线索发布时间已自动更新。")
|
| 1098 |
return redirect(f"/admin/activities/{activity_id}/edit")
|
| 1099 |
|
| 1100 |
+
@router.post("/admin/activities/{activity_id}/delete")
|
| 1101 |
+
async def delete_activity(activity_id: int, request: Request, db: Session = Depends(get_db)):
|
| 1102 |
+
admin = require_admin(request, db)
|
| 1103 |
+
if not admin:
|
| 1104 |
+
return redirect("/admin")
|
| 1105 |
+
|
| 1106 |
+
activity = (
|
| 1107 |
+
db.query(Activity)
|
| 1108 |
+
.options(joinedload(Activity.tasks).joinedload(Task.submissions))
|
| 1109 |
+
.filter(Activity.id == activity_id)
|
| 1110 |
+
.first()
|
| 1111 |
+
)
|
| 1112 |
+
if not activity:
|
| 1113 |
+
raise HTTPException(status_code=404, detail="活动不存在")
|
| 1114 |
+
|
| 1115 |
+
activity_title = activity.title
|
| 1116 |
+
cleanup_activity_files(activity)
|
| 1117 |
+
db.delete(activity)
|
| 1118 |
+
db.commit()
|
| 1119 |
+
add_flash(request, "success", f"活动 {activity_title} 已删除,相关任务与本地提交图片也已清理。")
|
| 1120 |
+
return redirect("/admin/activities")
|
| 1121 |
+
|
| 1122 |
+
|
| 1123 |
@router.post("/admin/activities/{activity_id}/visibility")
|
| 1124 |
+
async def update_activity_visibility(activity_id: int, request: Request, db: Session = Depends(get_db)):
|
| 1125 |
admin = require_admin(request, db)
|
| 1126 |
if not admin:
|
| 1127 |
return redirect("/admin")
|
|
|
|
| 1131 |
raise HTTPException(status_code=404, detail="活动不存在")
|
| 1132 |
|
| 1133 |
form = await request.form()
|
| 1134 |
+
activity.is_visible = form.get("is_visible") == "on"
|
| 1135 |
activity.leaderboard_visible = form.get("leaderboard_visible") == "on"
|
| 1136 |
db.add(activity)
|
| 1137 |
db.commit()
|
| 1138 |
+
add_flash(request, "success", f"已更新 {activity.title} 的活动可见性和排行榜设置。")
|
| 1139 |
return redirect("/admin/activities")
|
| 1140 |
|
| 1141 |
@router.get("/admin/reviews")
|
|
|
|
| 1373 |
|
| 1374 |
|
| 1375 |
|
| 1376 |
+
|
| 1377 |
+
|
| 1378 |
+
|
| 1379 |
+
|
app/routes/user.py
CHANGED
|
@@ -45,6 +45,7 @@ def dashboard(request: Request, db: Session = Depends(get_db)):
|
|
| 45 |
activities = (
|
| 46 |
db.query(Activity)
|
| 47 |
.options(joinedload(Activity.tasks).joinedload(Task.submissions))
|
|
|
|
| 48 |
.order_by(Activity.start_at.asc())
|
| 49 |
.all()
|
| 50 |
)
|
|
@@ -85,6 +86,9 @@ def activity_detail(activity_id: int, request: Request, db: Session = Depends(ge
|
|
| 85 |
activity = get_activity_with_group_context(db, activity_id)
|
| 86 |
if not activity:
|
| 87 |
raise HTTPException(status_code=404, detail="活动不存在")
|
|
|
|
|
|
|
|
|
|
| 88 |
|
| 89 |
now = local_now()
|
| 90 |
group_submission_by_task = build_group_submission_map(activity, user.group_id)
|
|
@@ -128,8 +132,12 @@ async def submit_task(
|
|
| 128 |
return redirect(f"/activities/{activity_id}")
|
| 129 |
|
| 130 |
activity = db.query(Activity).filter(Activity.id == activity_id).first()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
task = db.query(Task).filter(Task.id == task_id, Task.activity_id == activity_id).first()
|
| 132 |
-
if not
|
| 133 |
raise HTTPException(status_code=404, detail="任务不存在")
|
| 134 |
|
| 135 |
now = local_now()
|
|
@@ -203,7 +211,7 @@ def activity_clues(activity_id: int, request: Request, db: Session = Depends(get
|
|
| 203 |
return JSONResponse({"error": "unauthorized"}, status_code=401)
|
| 204 |
|
| 205 |
activity = db.query(Activity).options(joinedload(Activity.tasks)).filter(Activity.id == activity_id).first()
|
| 206 |
-
if not activity:
|
| 207 |
return JSONResponse({"error": "not_found"}, status_code=404)
|
| 208 |
|
| 209 |
now = local_now()
|
|
@@ -226,7 +234,7 @@ def activity_status(activity_id: int, request: Request, db: Session = Depends(ge
|
|
| 226 |
return JSONResponse({"error": "unauthorized"}, status_code=401)
|
| 227 |
|
| 228 |
activity = get_activity_with_group_context(db, activity_id)
|
| 229 |
-
if not activity:
|
| 230 |
return JSONResponse({"error": "not_found"}, status_code=404)
|
| 231 |
|
| 232 |
submission_map = build_group_submission_map(activity, user.group_id)
|
|
@@ -275,3 +283,8 @@ def activity_status(activity_id: int, request: Request, db: Session = Depends(ge
|
|
| 275 |
|
| 276 |
|
| 277 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
activities = (
|
| 46 |
db.query(Activity)
|
| 47 |
.options(joinedload(Activity.tasks).joinedload(Task.submissions))
|
| 48 |
+
.filter(Activity.is_visible.is_(True))
|
| 49 |
.order_by(Activity.start_at.asc())
|
| 50 |
.all()
|
| 51 |
)
|
|
|
|
| 86 |
activity = get_activity_with_group_context(db, activity_id)
|
| 87 |
if not activity:
|
| 88 |
raise HTTPException(status_code=404, detail="活动不存在")
|
| 89 |
+
if not activity.is_visible:
|
| 90 |
+
add_flash(request, "error", "该活动当前暂未对用户开放。")
|
| 91 |
+
return redirect("/dashboard")
|
| 92 |
|
| 93 |
now = local_now()
|
| 94 |
group_submission_by_task = build_group_submission_map(activity, user.group_id)
|
|
|
|
| 132 |
return redirect(f"/activities/{activity_id}")
|
| 133 |
|
| 134 |
activity = db.query(Activity).filter(Activity.id == activity_id).first()
|
| 135 |
+
if not activity or not activity.is_visible:
|
| 136 |
+
add_flash(request, "error", "该活动当前暂未对用户开放。")
|
| 137 |
+
return redirect("/dashboard")
|
| 138 |
+
|
| 139 |
task = db.query(Task).filter(Task.id == task_id, Task.activity_id == activity_id).first()
|
| 140 |
+
if not task:
|
| 141 |
raise HTTPException(status_code=404, detail="任务不存在")
|
| 142 |
|
| 143 |
now = local_now()
|
|
|
|
| 211 |
return JSONResponse({"error": "unauthorized"}, status_code=401)
|
| 212 |
|
| 213 |
activity = db.query(Activity).options(joinedload(Activity.tasks)).filter(Activity.id == activity_id).first()
|
| 214 |
+
if not activity or not activity.is_visible:
|
| 215 |
return JSONResponse({"error": "not_found"}, status_code=404)
|
| 216 |
|
| 217 |
now = local_now()
|
|
|
|
| 234 |
return JSONResponse({"error": "unauthorized"}, status_code=401)
|
| 235 |
|
| 236 |
activity = get_activity_with_group_context(db, activity_id)
|
| 237 |
+
if not activity or not activity.is_visible:
|
| 238 |
return JSONResponse({"error": "not_found"}, status_code=404)
|
| 239 |
|
| 240 |
submission_map = build_group_submission_map(activity, user.group_id)
|
|
|
|
| 283 |
|
| 284 |
|
| 285 |
|
| 286 |
+
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
|
app/services/__pycache__/bootstrap.cpython-313.pyc
CHANGED
|
Binary files a/app/services/__pycache__/bootstrap.cpython-313.pyc and b/app/services/__pycache__/bootstrap.cpython-313.pyc differ
|
|
|
app/services/bootstrap.py
CHANGED
|
@@ -148,6 +148,7 @@ def migrate_task_media_to_files() -> None:
|
|
| 148 |
def upgrade_schema() -> None:
|
| 149 |
ensure_column("admins", "last_seen_at", "last_seen_at DATETIME NULL")
|
| 150 |
ensure_column("users", "last_seen_at", "last_seen_at DATETIME NULL")
|
|
|
|
| 151 |
ensure_column("activities", "leaderboard_visible", "leaderboard_visible TINYINT(1) NOT NULL DEFAULT 1")
|
| 152 |
ensure_column("activities", "clue_interval_minutes", "clue_interval_minutes INT NULL")
|
| 153 |
ensure_column("tasks", "display_order", "display_order INT NOT NULL DEFAULT 1")
|
|
@@ -231,3 +232,4 @@ def seed_super_admin(db: Session) -> None:
|
|
| 231 |
|
| 232 |
|
| 233 |
|
|
|
|
|
|
| 148 |
def upgrade_schema() -> None:
|
| 149 |
ensure_column("admins", "last_seen_at", "last_seen_at DATETIME NULL")
|
| 150 |
ensure_column("users", "last_seen_at", "last_seen_at DATETIME NULL")
|
| 151 |
+
ensure_column("activities", "is_visible", "is_visible TINYINT(1) NOT NULL DEFAULT 1")
|
| 152 |
ensure_column("activities", "leaderboard_visible", "leaderboard_visible TINYINT(1) NOT NULL DEFAULT 1")
|
| 153 |
ensure_column("activities", "clue_interval_minutes", "clue_interval_minutes INT NULL")
|
| 154 |
ensure_column("tasks", "display_order", "display_order INT NOT NULL DEFAULT 1")
|
|
|
|
| 232 |
|
| 233 |
|
| 234 |
|
| 235 |
+
|
app/templates/admin_activities.html
CHANGED
|
@@ -32,6 +32,10 @@
|
|
| 32 |
<span>活动说明</span>
|
| 33 |
<textarea name="description" rows="3" placeholder="介绍活动安排、打卡要求和注意事项"></textarea>
|
| 34 |
</label>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
<label class="checkbox-row">
|
| 36 |
<input type="checkbox" name="leaderboard_visible" checked />
|
| 37 |
<span>允许用户查看实时排行榜</span>
|
|
@@ -91,16 +95,27 @@
|
|
| 91 |
<p class="muted">{{ activity.start_at|datetime_local }} 至 {{ activity.deadline_at|datetime_local }}</p>
|
| 92 |
<p class="muted">{{ activity.tasks|length }} 个任务 · 创建人 {{ activity.created_by.display_name }}</p>
|
| 93 |
<p class="muted">线索间隔:{{ activity.clue_interval_minutes if activity.clue_interval_minutes is not none else '与活动开始同步' }}</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
</div>
|
| 95 |
<div class="activity-actions">
|
| 96 |
<a class="btn btn-primary small-btn" href="/admin/activities/{{ activity.id }}/edit">编辑活动</a>
|
| 97 |
<form method="post" action="/admin/activities/{{ activity.id }}/visibility" class="inline-form visibility-form">
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
<label class="checkbox-row compact-checkbox">
|
| 99 |
<input type="checkbox" name="leaderboard_visible" {% if activity.leaderboard_visible %}checked{% endif %} />
|
| 100 |
<span>用户可见排行榜</span>
|
| 101 |
</label>
|
| 102 |
<button class="btn btn-secondary small-btn" type="submit">保存</button>
|
| 103 |
</form>
|
|
|
|
|
|
|
|
|
|
| 104 |
</div>
|
| 105 |
</article>
|
| 106 |
{% else %}
|
|
@@ -139,7 +154,11 @@
|
|
| 139 |
const template = builder.querySelector('[data-task-template]');
|
| 140 |
const clone = template.cloneNode(true);
|
| 141 |
clone.querySelectorAll('input, textarea').forEach((field) => {
|
| 142 |
-
field.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
});
|
| 144 |
attachRemove(clone);
|
| 145 |
builder.appendChild(clone);
|
|
@@ -148,4 +167,3 @@
|
|
| 148 |
})();
|
| 149 |
</script>
|
| 150 |
{% endblock %}
|
| 151 |
-
|
|
|
|
| 32 |
<span>活动说明</span>
|
| 33 |
<textarea name="description" rows="3" placeholder="介绍活动安排、打卡要求和注意事项"></textarea>
|
| 34 |
</label>
|
| 35 |
+
<label class="checkbox-row">
|
| 36 |
+
<input type="checkbox" name="is_visible" checked />
|
| 37 |
+
<span>允许用户查看该活动</span>
|
| 38 |
+
</label>
|
| 39 |
<label class="checkbox-row">
|
| 40 |
<input type="checkbox" name="leaderboard_visible" checked />
|
| 41 |
<span>允许用户查看实时排行榜</span>
|
|
|
|
| 95 |
<p class="muted">{{ activity.start_at|datetime_local }} 至 {{ activity.deadline_at|datetime_local }}</p>
|
| 96 |
<p class="muted">{{ activity.tasks|length }} 个任务 · 创建人 {{ activity.created_by.display_name }}</p>
|
| 97 |
<p class="muted">线索间隔:{{ activity.clue_interval_minutes if activity.clue_interval_minutes is not none else '与活动开始同步' }}</p>
|
| 98 |
+
<div class="chip-row">
|
| 99 |
+
<span class="chip">{{ '用户端可见' if activity.is_visible else '用户端隐藏' }}</span>
|
| 100 |
+
<span class="chip">{{ '排行榜可见' if activity.leaderboard_visible else '排行榜隐藏' }}</span>
|
| 101 |
+
</div>
|
| 102 |
</div>
|
| 103 |
<div class="activity-actions">
|
| 104 |
<a class="btn btn-primary small-btn" href="/admin/activities/{{ activity.id }}/edit">编辑活动</a>
|
| 105 |
<form method="post" action="/admin/activities/{{ activity.id }}/visibility" class="inline-form visibility-form">
|
| 106 |
+
<label class="checkbox-row compact-checkbox">
|
| 107 |
+
<input type="checkbox" name="is_visible" {% if activity.is_visible %}checked{% endif %} />
|
| 108 |
+
<span>用户可见活动</span>
|
| 109 |
+
</label>
|
| 110 |
<label class="checkbox-row compact-checkbox">
|
| 111 |
<input type="checkbox" name="leaderboard_visible" {% if activity.leaderboard_visible %}checked{% endif %} />
|
| 112 |
<span>用户可见排行榜</span>
|
| 113 |
</label>
|
| 114 |
<button class="btn btn-secondary small-btn" type="submit">保存</button>
|
| 115 |
</form>
|
| 116 |
+
<form method="post" action="/admin/activities/{{ activity.id }}/delete" class="inline-form">
|
| 117 |
+
<button class="btn btn-danger small-btn" type="submit" onclick="return confirm('确定删除这个活动吗?相关任务、审核记录和本地提交图片都会一并删除。');">删除活动</button>
|
| 118 |
+
</form>
|
| 119 |
</div>
|
| 120 |
</article>
|
| 121 |
{% else %}
|
|
|
|
| 154 |
const template = builder.querySelector('[data-task-template]');
|
| 155 |
const clone = template.cloneNode(true);
|
| 156 |
clone.querySelectorAll('input, textarea').forEach((field) => {
|
| 157 |
+
if (field.type === 'checkbox') {
|
| 158 |
+
field.checked = false;
|
| 159 |
+
} else {
|
| 160 |
+
field.value = '';
|
| 161 |
+
}
|
| 162 |
});
|
| 163 |
attachRemove(clone);
|
| 164 |
builder.appendChild(clone);
|
|
|
|
| 167 |
})();
|
| 168 |
</script>
|
| 169 |
{% endblock %}
|
|
|
app/templates/admin_activity_edit.html
CHANGED
|
@@ -12,6 +12,10 @@
|
|
| 12 |
<span class="pill">创建人 {{ activity.created_by.display_name }}</span>
|
| 13 |
<span class="pill">{{ activity.tasks|length }} 个任务</span>
|
| 14 |
<span class="pill">开始 {{ activity.start_at|datetime_local }}</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
</div>
|
| 16 |
</section>
|
| 17 |
|
|
@@ -45,6 +49,10 @@
|
|
| 45 |
<span>活动说明</span>
|
| 46 |
<textarea name="description" rows="3">{{ activity.description or '' }}</textarea>
|
| 47 |
</label>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
<label class="checkbox-row">
|
| 49 |
<input type="checkbox" name="leaderboard_visible" {% if activity.leaderboard_visible %}checked{% endif %} />
|
| 50 |
<span>允许用户查看实时排行榜</span>
|
|
@@ -59,7 +67,8 @@
|
|
| 59 |
<p class="mini-note">管理员始终可见,用于现场统筹和审核判断;用户是否可见由上方开关控制。</p>
|
| 60 |
</div>
|
| 61 |
<div class="chip-row">
|
| 62 |
-
<span class="chip">
|
|
|
|
| 63 |
<span class="chip">按完成点位数优先,再按总耗时排序</span>
|
| 64 |
</div>
|
| 65 |
</div>
|
|
@@ -206,6 +215,8 @@
|
|
| 206 |
</section>
|
| 207 |
</form>
|
| 208 |
|
|
|
|
|
|
|
| 209 |
{% for task in activity.tasks %}
|
| 210 |
<form id="delete-task-{{ task.id }}" method="post" action="/admin/tasks/{{ task.id }}/delete"></form>
|
| 211 |
{% endfor %}
|
|
@@ -254,3 +265,7 @@
|
|
| 254 |
</script>
|
| 255 |
{% endblock %}
|
| 256 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
<span class="pill">创建人 {{ activity.created_by.display_name }}</span>
|
| 13 |
<span class="pill">{{ activity.tasks|length }} 个任务</span>
|
| 14 |
<span class="pill">开始 {{ activity.start_at|datetime_local }}</span>
|
| 15 |
+
<span class="pill">{{ '用户端可见' if activity.is_visible else '用户端隐藏' }}</span>
|
| 16 |
+
</div>
|
| 17 |
+
<div class="card-footer">
|
| 18 |
+
<button class="btn btn-danger" type="submit" form="delete-activity-form" onclick="return confirm('确定删除这个活动吗?相关任务、审核记录和本地提交图片都会一并删除。');">删除整个活动</button>
|
| 19 |
</div>
|
| 20 |
</section>
|
| 21 |
|
|
|
|
| 49 |
<span>活动说明</span>
|
| 50 |
<textarea name="description" rows="3">{{ activity.description or '' }}</textarea>
|
| 51 |
</label>
|
| 52 |
+
<label class="checkbox-row">
|
| 53 |
+
<input type="checkbox" name="is_visible" {% if activity.is_visible %}checked{% endif %} />
|
| 54 |
+
<span>允许用户查看该活动</span>
|
| 55 |
+
</label>
|
| 56 |
<label class="checkbox-row">
|
| 57 |
<input type="checkbox" name="leaderboard_visible" {% if activity.leaderboard_visible %}checked{% endif %} />
|
| 58 |
<span>允许用户查看实时排行榜</span>
|
|
|
|
| 67 |
<p class="mini-note">管理员始终可见,用于现场统筹和审核判断;用户是否可见由上方开关控制。</p>
|
| 68 |
</div>
|
| 69 |
<div class="chip-row">
|
| 70 |
+
<span class="chip">活动{{ '可见' if activity.is_visible else '隐藏' }}</span>
|
| 71 |
+
<span class="chip">排行榜{{ '可见' if activity.leaderboard_visible else '隐藏' }}</span>
|
| 72 |
<span class="chip">按完成点位数优先,再按总耗时排序</span>
|
| 73 |
</div>
|
| 74 |
</div>
|
|
|
|
| 215 |
</section>
|
| 216 |
</form>
|
| 217 |
|
| 218 |
+
<form id="delete-activity-form" method="post" action="/admin/activities/{{ activity.id }}/delete"></form>
|
| 219 |
+
|
| 220 |
{% for task in activity.tasks %}
|
| 221 |
<form id="delete-task-{{ task.id }}" method="post" action="/admin/tasks/{{ task.id }}/delete"></form>
|
| 222 |
{% endfor %}
|
|
|
|
| 265 |
</script>
|
| 266 |
{% endblock %}
|
| 267 |
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
|
app/templates/admin_dashboard.html
CHANGED
|
@@ -62,8 +62,9 @@
|
|
| 62 |
<div>
|
| 63 |
<strong>{{ activity.title }}</strong>
|
| 64 |
<p class="muted">{{ activity.tasks|length }} 个任务 · {{ activity.start_at|datetime_local }}</p>
|
|
|
|
| 65 |
</div>
|
| 66 |
-
<span class="status-badge">{{ '
|
| 67 |
</div>
|
| 68 |
{% else %}
|
| 69 |
<p class="muted">还没有发布活动。</p>
|
|
@@ -116,3 +117,4 @@
|
|
| 116 |
</section>
|
| 117 |
{% endif %}
|
| 118 |
{% endblock %}
|
|
|
|
|
|
| 62 |
<div>
|
| 63 |
<strong>{{ activity.title }}</strong>
|
| 64 |
<p class="muted">{{ activity.tasks|length }} 个任务 · {{ activity.start_at|datetime_local }}</p>
|
| 65 |
+
<p class="muted">{{ '用户端可见' if activity.is_visible else '用户端隐藏' }} · {{ '排行榜可见' if activity.leaderboard_visible else '排行榜隐藏' }}</p>
|
| 66 |
</div>
|
| 67 |
+
<span class="status-badge {% if activity.is_visible %}status-approved{% else %}status-rejected{% endif %}">{{ '活动可见' if activity.is_visible else '活动隐藏' }}</span>
|
| 68 |
</div>
|
| 69 |
{% else %}
|
| 70 |
<p class="muted">还没有发布活动。</p>
|
|
|
|
| 117 |
</section>
|
| 118 |
{% endif %}
|
| 119 |
{% endblock %}
|
| 120 |
+
|