fix: 签到照片上传 405 + 必填样式 + 选图立即上传保存
ci/woodpecker/tag/build Pipeline was successful

1. 405 根因:前端 saveTaskForm 用 PUT,后端只注册 POST 路由,
   Fiber 返回 405。前端改 POST,后端同时兼容 POST/PUT
2. isPhoto=1 时签到照片标记必填(后端 Required + 前端红星)
3. 图片直传 OBS 成功后立即保存表单,图片地址持久化到数据库,
   不再等手动点保存;上传过程失败给出 toast 提示(补 useMessage 引入)
This commit is contained in:
becomer
2026-08-23 19:44:15 +08:00
parent fa9d10c140
commit 6bad918dd8
4 changed files with 34 additions and 26 deletions
+1
View File
@@ -57,6 +57,7 @@ func main() {
task.Get("/list", handler.ListTasks)
task.Get("/form/:id", handler.GetTaskForm)
task.Post("/form/:id", handler.SaveTaskForm)
task.Put("/form/:id", handler.SaveTaskForm)
task.Put("/update/:id", handler.UpdateTask)
task.Delete("/delete/:id", handler.DeleteTask)
task.Get("/execute/:id", handler.ExecuteTask)
+9 -8
View File
@@ -575,16 +575,17 @@ func ParseTaskForm(detail *TaskFormDetail, historyRows map[string]interface{}) [
Tip: "签到完毕后查看时最底部那个最详细的地址信息",
})
// 签到照片(如果需要)
// 签到照片(如果需要,必填)
if detail.IsPhoto {
list = append(list, cpdmodel.CpTaskFormItem{
Title: "签到照片",
ColName: "signImg",
Type: cpdmodel.ItemTypeImage,
Content: histSignPhotoUrl,
Default: histSignPhotoUrl,
Show: true,
Tip: "您可以上传多张图片,会顺序轮换更新",
Title: "签到照片",
ColName: "signImg",
Type: cpdmodel.ItemTypeImage,
Required: true,
Content: histSignPhotoUrl,
Default: histSignPhotoUrl,
Show: true,
Tip: "您可以上传多张图片,会顺序轮换更新",
})
}
+1 -1
View File
@@ -167,7 +167,7 @@ export function getTaskForm(id: number) {
/** 保存任务表单 */
export function saveTaskForm(id: number, formData: FormItem[]) {
return api.put(`/task/form/${id}`, { formData })
return api.post(`/task/form/${id}`, { formData })
}
/** 获取任务列表 */
+23 -17
View File
@@ -5,7 +5,9 @@ import { CheckCircle, Clock, Bell, Calendar, Plus, Play, Trash2, ListTodo, MapPi
import { Button } from '@/components/ui/button'
import * as authApi from '@/api/auth'
import { CPD_PASSWORD_ERROR } from '@/api'
import { useMessage } from '@/composables/useMessage'
const { message } = useMessage()
const router = useRouter()
const loading = ref(true)
@@ -245,8 +247,10 @@ function authHeaders(): Record<string, string> {
return { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' }
}
// ======== 图片上传 → 浏览器直传到今日校园 OBS(不经过服务器中转) ========
// ======== 图片上传 → 浏览器直传到今日校园 OBS(不经过服务器中转),成功后立即保存表单 ========
const uploadingImage = ref(false)
async function handleImageUpload(index: number, isEdit: boolean) {
if (uploadingImage.value) return
const input = document.createElement('input')
input.type = 'file'
input.accept = 'image/*'
@@ -254,19 +258,17 @@ async function handleImageUpload(index: number, isEdit: boolean) {
const file = input.files?.[0]
if (!file) return
const taskType = isEdit ? (editTask.value?.taskType || 'sign') : (selectedItem.value?.taskType || 'sign')
const items = isEdit ? editFormItems.value : formItems.value
const taskId = isEdit ? editTask.value?.id : formTaskId.value
// 先显示本地预览
const reader = new FileReader()
reader.onload = (e) => {
const previewUrl = e.target?.result as string
if (isEdit) {
editFormItems.value[index].content = previewUrl
} else {
formItems.value[index].content = previewUrl
}
items[index].content = (e.target?.result as string) || ''
}
reader.readAsDataURL(file)
uploadingImage.value = true
try {
// Step 1: 从服务器获取 OBS 上传策略(仅获取凭证,不传文件)
const policyRes = await fetch('/api/task/upload-policy', {
@@ -276,7 +278,7 @@ async function handleImageUpload(index: number, isEdit: boolean) {
})
const policyJson = await policyRes.json()
if (policyJson.code !== 0) {
console.warn('获取上传策略失败:', policyJson.message)
message.error('获取上传策略失败: ' + (policyJson.message || policyRes.status))
return
}
const policy = policyJson.data
@@ -296,7 +298,7 @@ async function handleImageUpload(index: number, isEdit: boolean) {
body: obsForm,
})
if (obsRes.status > 299) {
console.warn('上传到 OBS 失败:', obsRes.status)
message.error('图片上传到 OBS 失败(' + obsRes.status + '),请重试')
return
}
@@ -311,18 +313,22 @@ async function handleImageUpload(index: number, isEdit: boolean) {
})
const previewJson = await previewRes.json()
if (previewJson.code !== 0) {
console.warn('获取预览 URL 失败:', previewJson.message)
message.error('获取预览 URL 失败: ' + (previewJson.message || previewRes.status))
return
}
const previewUrl = previewJson.data.previewUrl
if (isEdit) {
editFormItems.value[index].content = previewUrl
} else {
formItems.value[index].content = previewUrl
items[index].content = previewUrl
// Step 4: 立即保存表单,把图片地址持久化到数据库
if (taskId) {
await authApi.saveTaskForm(taskId, items)
message.success('图片已上传并保存')
}
} catch (e: any) {
console.warn('图片上传流程失败:', e.message)
message.error('图片上传失败: ' + (e?.message || '未知错误'))
} finally {
uploadingImage.value = false
}
}
input.click()
@@ -1028,7 +1034,7 @@ function getGPSLocation(source: string) {
<!-- 图片:点击直接选文件替换,不拉伸、无输入框 -->
<div v-else-if="isImageField(item)" class="space-y-1">
<label class="text-xs font-medium text-gray-600">{{ item.title }}</label>
<label class="text-xs font-medium text-gray-600">{{ item.title }} <span v-if="item.required" class="text-red-400">*</span></label>
<div
@click="handleImageUpload(idx, false)"
class="cursor-pointer"
@@ -1188,7 +1194,7 @@ function getGPSLocation(source: string) {
<!-- 图片:点击直接选文件替换 -->
<div v-else-if="isImageField(item)" class="space-y-1">
<label class="text-xs font-medium text-gray-600">{{ item.title }}</label>
<label class="text-xs font-medium text-gray-600">{{ item.title }} <span v-if="item.required" class="text-red-400">*</span></label>
<div
@click="handleImageUpload(idx, true)"
class="cursor-pointer"