diff --git a/src/admin/achievement/components/AchievementModal.vue b/src/admin/achievement/components/AchievementModal.vue
new file mode 100644
index 0000000..bab15b1
--- /dev/null
+++ b/src/admin/achievement/components/AchievementModal.vue
@@ -0,0 +1,160 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ metricHelp }}
+
+
+
+
+
+
+
+
+
+
+ 未解锁时学生只能看到 ???
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+
+ 保存
+
+
+
+
+
diff --git a/src/admin/achievement/list.vue b/src/admin/achievement/list.vue
new file mode 100644
index 0000000..6c1f2f7
--- /dev/null
+++ b/src/admin/achievement/list.vue
@@ -0,0 +1,135 @@
+
+
+
+
+
+ 新建成就
+
+
+
+ 「已解锁人数」是唯一的仪表盘:配置一周后仍为
+ 0,多半是阈值配错了而不是太难。
+
+
+
+
+
+
+
diff --git a/src/admin/api.ts b/src/admin/api.ts
index 2d8f7c8..fe34e5b 100644
--- a/src/admin/api.ts
+++ b/src/admin/api.ts
@@ -531,3 +531,48 @@ export function pinAIReport(id: number) {
export function getPinnedAIReports() {
return http.get("admin/ai/reports", { params: { pinned_only: "true" } })
}
+
+// ==================== 成就 ====================
+
+export interface AdminAchievement {
+ id: number
+ name: string
+ description: string
+ icon: string
+ rarity: string
+ hidden: boolean
+ metric: string
+ metric_name: string
+ operator: "gte" | "lte"
+ threshold: number
+ visible: boolean
+ unlock_count: number
+ order: number
+ create_time: string
+}
+
+export interface MetricOption {
+ key: string
+ name: string
+ help_text: string
+}
+
+export function getAdminAchievements() {
+ return http.get("admin/achievement")
+}
+
+export function getMetricOptions() {
+ return http.get("admin/achievement/metrics")
+}
+
+export function createAchievement(data: Partial) {
+ return http.post("admin/achievement", data)
+}
+
+export function updateAchievement(data: Partial) {
+ return http.put("admin/achievement", data)
+}
+
+export function deleteAchievement(id: number) {
+ return http.delete("admin/achievement", { params: { id } })
+}
diff --git a/src/routes.ts b/src/routes.ts
index 067f28e..8f8f383 100644
--- a/src/routes.ts
+++ b/src/routes.ts
@@ -301,6 +301,14 @@ export const admins: RouteRecordRaw = {
component: () => import("admin/problemset/list.vue"),
meta: { requiresTeacherAdmin: true },
},
+ {
+ path: "achievement/list",
+ name: "admin achievement list",
+ // 后端 admin 接口是 super_admin_required,这里必须跟着用
+ // requiresSuperAdmin,否则教师能进页面但每个请求都 403
+ component: () => import("admin/achievement/list.vue"),
+ meta: { requiresSuperAdmin: true },
+ },
{
path: "problemset/create",
name: "admin problemset create",
diff --git a/src/shared/layout/admin.vue b/src/shared/layout/admin.vue
index 1923e46..bf9d9e0 100644
--- a/src/shared/layout/admin.vue
+++ b/src/shared/layout/admin.vue
@@ -141,6 +141,15 @@ const options = computed(() => {
),
key: "admin tutorial list",
},
+ {
+ label: () =>
+ h(
+ RouterLink,
+ { to: "/admin/achievement/list" },
+ { default: () => "成就" },
+ ),
+ key: "admin achievement list",
+ },
{
label: () =>
h(