diff --git a/.workbuddy/expert-history.json b/.workbuddy/expert-history.json index a4b647d..543c17d 100644 --- a/.workbuddy/expert-history.json +++ b/.workbuddy/expert-history.json @@ -13,5 +13,5 @@ } ] }, - "lastUpdated": 1776081278156 + "lastUpdated": 1776083682738 } \ No newline at end of file diff --git a/.workbuddy/memory/2026-04-13.md b/.workbuddy/memory/2026-04-13.md index 594000e..6b70c93 100644 --- a/.workbuddy/memory/2026-04-13.md +++ b/.workbuddy/memory/2026-04-13.md @@ -21,6 +21,12 @@ - `ShowOrder.vue` 详情页新增:状态快捷切换按钮(四个状态一键切换)、状态变更 commit 历史时间线(竖排列表,含状态标签+时间+评论) - i18n 同步新增 8 个翻译 key +### 新增:状态变更弹窗(含备注输入) +- `ShowOrder.vue` 中状态按钮不再直接变更,改为弹出确认框 +- 弹窗包含:目标状态标签 + 备注 textarea(支持 Ctrl+Enter 快捷确认) +- 备注内容通过 `updateOrderStatus(id, status, comment)` 提交 +- `message.save_success` 已补充到中英 i18n 文件 + ### 注意事项 - 重启后端后 GORM AutoMigrate 会自动新增字段和表,无需手动 SQL - 前端 `CostItem` 的 CurrencyType/Type 改为 int,与后端一致 diff --git a/backend/my_work/routers/apiPurchase.go b/backend/my_work/routers/apiPurchase.go index 02269c7..ebcc4e6 100644 --- a/backend/my_work/routers/apiPurchase.go +++ b/backend/my_work/routers/apiPurchase.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "ops/models" + "strings" "time" "github.com/gin-gonic/gin" @@ -71,6 +72,7 @@ type TabPurchaseCommit struct { Status string `gorm:"size:50;comment:变更后的状态"` OldStatus string `gorm:"size:50;comment:变更前的状态"` Comment string `gorm:"type:text;comment:评论/备注"` + Photos string `gorm:"type:text;comment:变更图片(JSON数组,存放sha256哈希)"` IP string `gorm:"size:50;comment:操作IP"` CreatedAt *time.Time `gorm:"type:datetime;autoCreateTime"` } @@ -142,11 +144,56 @@ func ApiPurchase(r *gin.RouterGroup) { var commits []TabPurchaseCommit models.DB.Where("order_id = ?", from.ID).Order("created_at DESC").Find(&commits) + // 解析每条 commit 的 Photos JSON 字段为数组 + type CommitResponse struct { + ID uint `json:"id"` + OrderID uint `json:"orderId"` + UserID uint `json:"userId"` + Action string `json:"action"` + Status string `json:"status"` + OldStatus string `json:"oldStatus"` + Comment string `json:"comment"` + IP string `json:"ip"` + CreatedAt time.Time `json:"createdAt"` + Photos []string `json:"photos"` + } + var commitResps []CommitResponse + for _, c := range commits { + // Status 优先用数据库字段;若为空(历史旧数据),从 Comment 备注中截取状态 + status := c.Status + if status == "" { + status = strings.TrimPrefix(c.Comment, "状态变更为: ") + status = strings.TrimPrefix(status, "变更状态为: ") + // 如果截取后跟原文一样,说明不是"状态变更为"格式,取原文作为展示 + if status == c.Comment { + status = "" + } + } + resp := CommitResponse{ + ID: c.ID, + OrderID: c.OrderID, + UserID: c.UserID, + Action: c.Action, + Status: status, + OldStatus: c.OldStatus, + Comment: c.Comment, + IP: c.IP, + CreatedAt: time.Time{}, + } + if c.CreatedAt != nil { + resp.CreatedAt = *c.CreatedAt + } + if c.Photos != "" { + json.Unmarshal([]byte(c.Photos), &resp.Photos) + } + commitResps = append(commitResps, resp) + } + ReturnJson(ctx, "apiOK", gin.H{ "order": order, "costs": costs, "photos": files, - "commits": commits, + "commits": commitResps, }) }) @@ -159,9 +206,10 @@ func ApiPurchase(r *gin.RouterGroup) { } type FromUpdateStatus struct { - ID uint `json:"id"` - Status string `json:"status" binding:"required"` - Comment string `json:"comment"` + ID uint `json:"id"` + Status string `json:"status" binding:"required"` + Comment string `json:"comment"` + Photos []string `json:"photos"` // 变更附带的图片哈希 } var from FromUpdateStatus if err := mapstructure.Decode(data, &from); err != nil || from.ID == 0 { @@ -169,6 +217,14 @@ func ApiPurchase(r *gin.RouterGroup) { return } + // 校验图片哈希(不包含标点符号的哈希值) + for _, hash := range from.Photos { + if models.IsContainsSpecialChar(hash) { + ReturnJson(ctx, "photo_hash_invalid", nil) + return + } + } + // 校验状态值 validStatuses := map[string]bool{ "pending": true, @@ -204,9 +260,15 @@ func ApiPurchase(r *gin.RouterGroup) { // 写状态变更 commit comment := from.Comment - if comment == "" { + if comment == "" && len(from.Photos) == 0 { comment = "状态变更为: " + from.Status } + photosJSON := "" + if len(from.Photos) > 0 { + if pj, err := json.Marshal(from.Photos); err == nil { + photosJSON = string(pj) + } + } commit := TabPurchaseCommit{ OrderID: order.ID, UserID: user.ID, @@ -214,6 +276,7 @@ func ApiPurchase(r *gin.RouterGroup) { Status: from.Status, OldStatus: oldStatus, Comment: comment, + Photos: photosJSON, IP: ctx.ClientIP(), } models.DB.Create(&commit) diff --git a/frontend/ops_vue_js/src/api/purchase.js b/frontend/ops_vue_js/src/api/purchase.js index 2a8c1b4..e3ec4fd 100644 --- a/frontend/ops_vue_js/src/api/purchase.js +++ b/frontend/ops_vue_js/src/api/purchase.js @@ -16,8 +16,8 @@ export const purchaseApi = { return api.post('/purchase/getorder', { id }) }, - /** 更新订单状态(可附带评论) */ - updateOrderStatus(id, status, comment = '') { - return api.post('/purchase/updatestatus', { id, status, comment }) + /** 更新订单状态(可附带评论和图片) */ + updateOrderStatus(id, status, comment = '', photos = []) { + return api.post('/purchase/updatestatus', { id, status, comment, photos }) }, } diff --git a/frontend/ops_vue_js/src/i18n/en.json b/frontend/ops_vue_js/src/i18n/en.json index d6b7f69..318e9a2 100644 --- a/frontend/ops_vue_js/src/i18n/en.json +++ b/frontend/ops_vue_js/src/i18n/en.json @@ -91,9 +91,12 @@ "no_costs": "No cost records", "cost_total": "Total", "change_status": "Change Status", + "change_remark": "Change Remark", + "status_photos": "Change Photos", "commit_history": "Status History", "no_commits": "No status records", "commit_placeholder": "Add comment (optional)", + "upload_photos": "Upload Photos", "commit_create": "Order created" }, "purchase_addorder": { @@ -222,7 +225,8 @@ "type_new_pass": "Enter new password", "type_cof_pass": "Confirm new password", "old_pass_incorrect": "Old password is incorrect", - "confirm_password_incorrect": "Confirm password is incorrect" + "confirm_password_incorrect": "Confirm password is incorrect", + "save_success": "Saved successfully" }, "settings": { "cancel": "Cancel", diff --git a/frontend/ops_vue_js/src/i18n/zh-CN.json b/frontend/ops_vue_js/src/i18n/zh-CN.json index 54620d4..a254c2a 100644 --- a/frontend/ops_vue_js/src/i18n/zh-CN.json +++ b/frontend/ops_vue_js/src/i18n/zh-CN.json @@ -91,9 +91,12 @@ "no_costs": "暂无费用记录", "cost_total": "合计", "change_status": "变更状态", + "change_remark": "变更备注", + "status_photos": "变更图片", "commit_history": "状态记录", "no_commits": "暂无状态记录", "commit_placeholder": "添加备注(可选)", + "upload_photos": "上传图片", "commit_create": "订单创建" }, "purchase_addorder": { @@ -222,7 +225,8 @@ "type_new_pass": "输入新密码", "type_cof_pass": "确认新密码", "old_pass_incorrect": "旧密码不正确", - "confirm_password_incorrect": "确认密码不正确" + "confirm_password_incorrect": "确认密码不正确", + "save_success": "保存成功" }, "settings": { "cancel": "取消", diff --git a/frontend/ops_vue_js/src/views/purchase/PurchaseList.vue b/frontend/ops_vue_js/src/views/purchase/PurchaseList.vue index ef5e5d7..a3e1637 100644 --- a/frontend/ops_vue_js/src/views/purchase/PurchaseList.vue +++ b/frontend/ops_vue_js/src/views/purchase/PurchaseList.vue @@ -58,8 +58,10 @@ function goToPage(page) { } function jumpToOrder(id) { - const resolved = router.resolve({ path: `/purchase/showorder/${id}` }) - window.open(resolved.href, '_blank') + // const resolved = router.resolve({ path: `/purchase/showorder/${id}` }) + // window.open(resolved.href, '_blank') + + router.replace(`/purchase/showorder/${id}`); } function formatDate(dateStr) { @@ -121,7 +123,7 @@ onMounted(fetchOrders) No. {{ t('purchase.item_name') }} {{ t('purchase.purpose') }} - {{ t('purchase.quantity') }} + {{ t('purchase.created_at') }} {{ t('purchase.status') }} diff --git a/frontend/ops_vue_js/src/views/purchase/ShowOrder.vue b/frontend/ops_vue_js/src/views/purchase/ShowOrder.vue index 5c8fa3e..af9b341 100644 --- a/frontend/ops_vue_js/src/views/purchase/ShowOrder.vue +++ b/frontend/ops_vue_js/src/views/purchase/ShowOrder.vue @@ -1,155 +1,284 @@