1. 405 根因:前端 saveTaskForm 用 PUT,后端只注册 POST 路由, Fiber 返回 405。前端改 POST,后端同时兼容 POST/PUT 2. isPhoto=1 时签到照片标记必填(后端 Required + 前端红星) 3. 图片直传 OBS 成功后立即保存表单,图片地址持久化到数据库, 不再等手动点保存;上传过程失败给出 toast 提示(补 useMessage 引入)
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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: "您可以上传多张图片,会顺序轮换更新",
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
/** 获取任务列表 */
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user