Files
smartmate/frontend/src/api/scheduleCenter.ts
Losita 7b04b073ce Version: 0.9.81.dev.260506
后端:
1. Credit 价格规则补齐利润率与实际计费单价语义:新增 `profit_rate_bps` 与 `charge_*_price_micros` 展示字段,下沉共享价格推导 helper,tokenstore rpc/client/proto/model/default rule 全链路同步,LLM usage 扣费统一改按加价后的 charge 单价换算。
2. task-class 更新链路修正全量覆盖与归属校验:`runtime/conv` 保留 item id,DAO 更新前显式校验 task-class 与 item 归属,改用显式字段 map 落库 nil/空切片/零值,避免 `RowsAffected=0` 误判越权,同时补齐任务项可编辑字段更新。
3. GormCache task-class 失效补空 user_id 保护:更新语句缺少模型上下文时直接跳过失效,避免缓存插件因空指针影响主事务。

前端:
4. 课表中心补齐任务类编辑能力:新增 `updateTaskClass` API,创建弹窗支持编辑态回填与 item id 提交,日程页支持先拉详情再编辑并在保存后刷新任务类详情与列表。
5. 计划广场详情补点赞交互与奖励提示:详情页新增点赞/取消点赞按钮、奖励反馈文案与计数展示,论坛类型补 `reward_hint`,评论区与帖子作者头像统一接入兜底头像工具。
6. 品牌与展示细节收口:侧边栏与 favicon 切到项目 logo,首页标题改为 `SmartMate`,主面板缩放上限微调,论坛列表头像显示与整体品牌观感同步统一。
2026-05-06 21:53:17 +08:00

200 lines
6.5 KiB
TypeScript

import axios from 'axios'
import http from '@/api/http'
import type { ApiResponse, PlainResponse } from '@/types/api'
import type {
ApplyBatchIntoScheduleItem,
ScheduleDeletePayloadItem,
ScheduleWeekData,
TaskClassCreatePayload,
TaskClassDetail,
TaskClassListItem,
CourseDraftRow,
CourseImportPayload,
CourseImageParseResponse,
} from '@/types/schedule'
import { extractErrorMessage } from '@/utils/http'
import { createIdempotencyKey } from '@/utils/idempotency'
type WeekScheduleResponseData = ScheduleWeekData | ScheduleWeekData[] | null | undefined
function normalizeWeekScheduleData(data: WeekScheduleResponseData): ScheduleWeekData[] {
if (Array.isArray(data)) {
return data
}
if (data && typeof data === 'object') {
return [data]
}
return []
}
export async function getWeekSchedule(week?: number) {
try {
const response = await http.get<ApiResponse<WeekScheduleResponseData>>('/schedule/week', {
params: {
week: typeof week === 'number' ? week : 0,
},
})
return normalizeWeekScheduleData(response.data.data)
} catch (error) {
throw new Error(extractErrorMessage(error, '\u5468\u603b\u65e5\u7a0b\u52a0\u8f7d\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5'))
}
}
export async function getTaskClassList() {
try {
const response = await http.get<ApiResponse<{ task_classes: TaskClassListItem[] }>>('/task-class/list')
return response.data.data?.task_classes ?? []
} catch (error) {
throw new Error(extractErrorMessage(error, '\u4efb\u52a1\u7c7b\u5217\u8868\u52a0\u8f7d\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5'))
}
}
export async function getTaskClassDetail(taskClassId: number) {
try {
const response = await http.get<ApiResponse<TaskClassDetail>>('/task-class/get', {
params: {
task_class_id: taskClassId,
},
})
return response.data.data
} catch (error) {
throw new Error(extractErrorMessage(error, '\u4efb\u52a1\u7c7b\u8be6\u60c5\u52a0\u8f7d\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5'))
}
}
export async function createTaskClass(payload: TaskClassCreatePayload, idempotencyKey = createIdempotencyKey('task-class-add')) {
try {
const response = await http.post<PlainResponse>('/task-class/add', payload, {
headers: {
'X-Idempotency-Key': idempotencyKey,
},
})
return response.data
} catch (error) {
throw new Error(extractErrorMessage(error, '\u521b\u5efa\u4efb\u52a1\u7c7b\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5'))
}
}
export async function smartPlanning(taskClassId: number) {
try {
const response = await http.get<ApiResponse<ScheduleWeekData[]>>('/schedule/smart-planning', {
params: {
task_class_id: taskClassId,
},
})
return response.data.data ?? []
} catch (error) {
throw new Error(extractErrorMessage(error, '\u667a\u80fd\u7c97\u6392\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5'))
}
}
export async function smartPlanningMulti(taskClassIds: number[]) {
try {
const response = await http.post<ApiResponse<ScheduleWeekData[]>>('/schedule/smart-planning-multi', {
task_class_ids: taskClassIds,
})
return response.data.data ?? []
} catch (error) {
throw new Error(extractErrorMessage(error, '\u6279\u91cf\u667a\u80fd\u7c97\u6392\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5'))
}
}
export async function applyBatchIntoSchedule(taskClassId: number, items: ApplyBatchIntoScheduleItem[], idempotencyKey = createIdempotencyKey('schedule-apply')) {
try {
const response = await http.put<PlainResponse>(
'/task-class/apply-batch-into-schedule',
{
task_class_id: taskClassId,
items,
},
{
headers: {
'X-Idempotency-Key': idempotencyKey,
},
},
)
return response.data
} catch (error) {
throw new Error(extractErrorMessage(error, '\u6b63\u5f0f\u5e94\u7528\u65e5\u7a0b\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5'))
}
}
export async function deleteScheduleEntries(items: ScheduleDeletePayloadItem[], idempotencyKey = createIdempotencyKey('schedule-delete')) {
try {
const response = await http.delete<PlainResponse>('/schedule/delete', {
data: items,
headers: {
'X-Idempotency-Key': idempotencyKey,
},
})
return response.data
} catch (error) {
throw new Error(extractErrorMessage(error, '\u89e3\u9664\u5b89\u6392\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5'))
}
}
export async function deleteTaskClassItem(taskItemId: number, idempotencyKey = createIdempotencyKey('task-class-item-delete')) {
try {
const response = await http.delete<PlainResponse>('/task-class/delete-item', {
params: {
task_item_id: taskItemId,
},
headers: {
'X-Idempotency-Key': idempotencyKey,
},
})
return response.data
} catch (error) {
throw new Error(extractErrorMessage(error, '\u5220\u9664\u4efb\u52a1\u5757\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5'))
}
}
export async function parseCourseImage(file: File, signal?: AbortSignal) {
try {
const formData = new FormData()
formData.append('image', file)
const response = await http.post<ApiResponse<CourseImageParseResponse>>('/course/parse-image', formData, {
timeout: 300000,
signal,
})
return response.data.data
} catch (error) {
if (axios.isCancel(error)) {
throw new Error('\u8bc6\u522b\u5df2\u53d6\u6d88')
}
throw new Error(extractErrorMessage(error, '\u8bfe\u8868\u56fe\u7247\u8bc6\u522b\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5'))
}
}
export async function importCourses(payload: CourseImportPayload, idempotencyKey = createIdempotencyKey('course-import')) {
try {
const response = await http.post<PlainResponse>('/course/import', payload, {
headers: {
'X-Idempotency-Key': idempotencyKey,
},
})
return response.data
} catch (error) {
throw new Error(extractErrorMessage(error, '\u8bfe\u7a0b\u5bfc\u5165\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5'))
}
}
export async function updateTaskClass(taskClassId: number, payload: TaskClassCreatePayload, idempotencyKey = createIdempotencyKey('task-class-update')) {
try {
const response = await http.put<PlainResponse>('/task-class/update', payload, {
params: {
task_class_id: taskClassId,
},
headers: {
'X-Idempotency-Key': idempotencyKey,
},
})
return response.data
} catch (error) {
throw new Error(extractErrorMessage(error, '更新任务类失败,请稍后重试'))
}
}