增加状态切换
This commit is contained in:
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"sessions": {
|
||||||
|
"81d18d7cb0a14f7b80ab19186392c337": [
|
||||||
|
{
|
||||||
|
"expertId": "BackendArchitect",
|
||||||
|
"name": "Joy",
|
||||||
|
"profession": "后端架构师",
|
||||||
|
"avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/BackendArchitect/BackendArchitect.png",
|
||||||
|
"promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/BackendArchitect/BackendArchitect_zh.md",
|
||||||
|
"usedAt": 1776079998628,
|
||||||
|
"industryId": "all"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lastUpdated": 1776081278156
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# 2026-04-13
|
||||||
|
|
||||||
|
## ops2 项目采购模块改造
|
||||||
|
|
||||||
|
为采购订单增加状态管理功能,涉及前后端多处改动:
|
||||||
|
|
||||||
|
### 后端改动
|
||||||
|
- `TabPurchaseOrder` 新增 `OrderStatus` 字段(pending/ordered/arrived/received),默认 pending
|
||||||
|
- `TabPurchaseOrder` 新增 `UpdatedAt` 字段(GORM autoUpdateTime)
|
||||||
|
- `TabPurchaseCosts` 新增 `CurrencyType`(1-CNY 2-MOP 3-HKD 4-USD)和 `CostType`(1-单价 2-运费)
|
||||||
|
- 新表 `TabPurchaseCommit`:记录每次状态变更(Action: create/create_status)、旧状态、新状态、评论、操作IP
|
||||||
|
- `CostItem` 前端结构体 `CurrencyType` 和 `Type` 从 string 改为 int(与数据库一致)
|
||||||
|
- `/purchase/getorder` 新增返回 `commits`(状态变更记录列表)
|
||||||
|
- `/purchase/updatestatus` 新接口:更新订单状态,可选附带 comment,写入 TabPurchaseCommit + TabPurchaseLog
|
||||||
|
- `addorder` 创建时自动写入第一条 commit(状态 pending)
|
||||||
|
- 状态值白名单校验:只允许 pending/ordered/arrived/received
|
||||||
|
|
||||||
|
### 前端改动
|
||||||
|
- `api/purchase.js` 新增 `updateOrderStatus(id, status, comment)` 方法
|
||||||
|
- `PurchaseList.vue` 列表页去掉假数据列,显示真实标题/备注/创建时间,状态用彩色标签(黄-待处理、蓝-已下单、紫-已到达、绿-已收件)
|
||||||
|
- `ShowOrder.vue` 详情页新增:状态快捷切换按钮(四个状态一键切换)、状态变更 commit 历史时间线(竖排列表,含状态标签+时间+评论)
|
||||||
|
- i18n 同步新增 8 个翻译 key
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
- 重启后端后 GORM AutoMigrate 会自动新增字段和表,无需手动 SQL
|
||||||
|
- 前端 `CostItem` 的 CurrencyType/Type 改为 int,与后端一致
|
||||||
@@ -12,11 +12,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type CostItem struct {
|
type CostItem struct {
|
||||||
Cost int `json:"cost"` // 费用
|
Cost int `json:"cost"` // 费用(分)
|
||||||
CostT int `json:"costt"` // 总价
|
CostT int `json:"costt"` // 总价
|
||||||
CurrencyType string `json:"currencytype"` // 货币类型
|
CurrencyType int `json:"currencytype"` // 货币类型: 1-CNY 2-MOP 3-HKD 4-USD
|
||||||
Int int `json:"int"` // 数量
|
Int int `json:"int"` // 数量
|
||||||
Type string `json:"type"` // 费用类型
|
Type int `json:"type"` // 费用类型: 1-单价 2-运费
|
||||||
}
|
}
|
||||||
type From_purchase_addorder struct {
|
type From_purchase_addorder struct {
|
||||||
Costs []CostItem `json:"costs"` // 成本
|
Costs []CostItem `json:"costs"` // 成本
|
||||||
@@ -38,7 +38,9 @@ type TabPurchaseOrder struct {
|
|||||||
Remark string `gorm:"type:text;comment:备注"`
|
Remark string `gorm:"type:text;comment:备注"`
|
||||||
Link string `gorm:"size:1000;comment:链接"`
|
Link string `gorm:"size:1000;comment:链接"`
|
||||||
Styles string `gorm:"type:text;comment:样式数组"`
|
Styles string `gorm:"type:text;comment:样式数组"`
|
||||||
|
OrderStatus string `gorm:"size:50;default:pending;comment:订单状态: pending-待处理 ordered-已下单 arrived-已到达 received-已收件"`
|
||||||
CreatedAt *time.Time `gorm:"type:datetime;autoCreateTime"`
|
CreatedAt *time.Time `gorm:"type:datetime;autoCreateTime"`
|
||||||
|
UpdatedAt *time.Time `gorm:"type:datetime;autoUpdateTime"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,6 +50,8 @@ type TabPurchaseCosts struct {
|
|||||||
UserID uint `gorm:"not null"`
|
UserID uint `gorm:"not null"`
|
||||||
Price int `gorm:"not null"`
|
Price int `gorm:"not null"`
|
||||||
Quantity int `gorm:"not null"`
|
Quantity int `gorm:"not null"`
|
||||||
|
CurrencyType int `gorm:"default:1;comment:货币类型: 1-CNY 2-MOP 3-HKD 4-USD"`
|
||||||
|
CostType int `gorm:"default:1;comment:费用类型: 1-单价 2-运费"`
|
||||||
CreatedAt *time.Time `gorm:"type:datetime;autoCreateTime"`
|
CreatedAt *time.Time `gorm:"type:datetime;autoCreateTime"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,6 +62,19 @@ type TabPurchaseFileBind struct {
|
|||||||
CreatedAt *time.Time `gorm:"type:datetime;autoCreateTime"`
|
CreatedAt *time.Time `gorm:"type:datetime;autoCreateTime"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TabPurchaseCommit 记录订单状态变更及评论
|
||||||
|
type TabPurchaseCommit struct {
|
||||||
|
ID uint `gorm:"primarykey"`
|
||||||
|
OrderID uint `gorm:"not null;index;comment:关联订单ID"`
|
||||||
|
UserID uint `gorm:"not null;comment:操作人ID"`
|
||||||
|
Action string `gorm:"size:50;not null;comment:操作类型: create-创建 create_status-状态变更"`
|
||||||
|
Status string `gorm:"size:50;comment:变更后的状态"`
|
||||||
|
OldStatus string `gorm:"size:50;comment:变更前的状态"`
|
||||||
|
Comment string `gorm:"type:text;comment:评论/备注"`
|
||||||
|
IP string `gorm:"size:50;comment:操作IP"`
|
||||||
|
CreatedAt *time.Time `gorm:"type:datetime;autoCreateTime"`
|
||||||
|
}
|
||||||
|
|
||||||
type TabPurchaseLog struct {
|
type TabPurchaseLog struct {
|
||||||
ID uint `gorm:"primarykey"`
|
ID uint `gorm:"primarykey"`
|
||||||
OrderID uint `gorm:"not null;index;comment:关联OrderID"`
|
OrderID uint `gorm:"not null;index;comment:关联OrderID"`
|
||||||
@@ -77,11 +94,152 @@ func ApiPurchaseInit() {
|
|||||||
models.DB.AutoMigrate(&TabPurchaseCosts{})
|
models.DB.AutoMigrate(&TabPurchaseCosts{})
|
||||||
models.DB.AutoMigrate(&TabPurchaseFileBind{})
|
models.DB.AutoMigrate(&TabPurchaseFileBind{})
|
||||||
models.DB.AutoMigrate(&TabPurchaseLog{})
|
models.DB.AutoMigrate(&TabPurchaseLog{})
|
||||||
|
models.DB.AutoMigrate(&TabPurchaseCommit{})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ApiPurchase(r *gin.RouterGroup) {
|
func ApiPurchase(r *gin.RouterGroup) {
|
||||||
|
|
||||||
|
r.POST("/getorder", func(ctx *gin.Context) {
|
||||||
|
isAuth, _, data := AuthenticationAuthority(ctx)
|
||||||
|
if !isAuth {
|
||||||
|
ReturnJson(ctx, "userCookieError", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type FromGetOrder struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
}
|
||||||
|
var from FromGetOrder
|
||||||
|
if err := mapstructure.Decode(data, &from); err != nil || from.ID == 0 {
|
||||||
|
ReturnJson(ctx, "jsonErr", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var order TabPurchaseOrder
|
||||||
|
if err := models.DB.Where("id = ?", from.ID).First(&order).Error; err != nil {
|
||||||
|
ReturnJson(ctx, "order_not_found", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询关联费用
|
||||||
|
var costs []TabPurchaseCosts
|
||||||
|
models.DB.Where("order_id = ?", from.ID).Find(&costs)
|
||||||
|
|
||||||
|
// 查询关联图片
|
||||||
|
var binds []TabPurchaseFileBind
|
||||||
|
models.DB.Where("order_id = ?", from.ID).Find(&binds)
|
||||||
|
var fileIDs []uint
|
||||||
|
for _, b := range binds {
|
||||||
|
fileIDs = append(fileIDs, b.FileID)
|
||||||
|
}
|
||||||
|
var files []models.TabFileInfo_
|
||||||
|
if len(fileIDs) > 0 {
|
||||||
|
models.DB.Where("id IN ?", fileIDs).Find(&files)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询状态变更记录
|
||||||
|
var commits []TabPurchaseCommit
|
||||||
|
models.DB.Where("order_id = ?", from.ID).Order("created_at DESC").Find(&commits)
|
||||||
|
|
||||||
|
ReturnJson(ctx, "apiOK", gin.H{
|
||||||
|
"order": order,
|
||||||
|
"costs": costs,
|
||||||
|
"photos": files,
|
||||||
|
"commits": commits,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 更新订单状态(可附带评论)
|
||||||
|
r.POST("/updatestatus", func(ctx *gin.Context) {
|
||||||
|
isAuth, user, data := AuthenticationAuthority(ctx)
|
||||||
|
if !isAuth {
|
||||||
|
ReturnJson(ctx, "userCookieError", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type FromUpdateStatus struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
Status string `json:"status" binding:"required"`
|
||||||
|
Comment string `json:"comment"`
|
||||||
|
}
|
||||||
|
var from FromUpdateStatus
|
||||||
|
if err := mapstructure.Decode(data, &from); err != nil || from.ID == 0 {
|
||||||
|
ReturnJson(ctx, "jsonErr", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验状态值
|
||||||
|
validStatuses := map[string]bool{
|
||||||
|
"pending": true,
|
||||||
|
"ordered": true,
|
||||||
|
"arrived": true,
|
||||||
|
"received": true,
|
||||||
|
}
|
||||||
|
if !validStatuses[from.Status] {
|
||||||
|
ReturnJson(ctx, "invalid_status", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var order TabPurchaseOrder
|
||||||
|
if err := models.DB.Where("id = ?", from.ID).First(&order).Error; err != nil {
|
||||||
|
ReturnJson(ctx, "order_not_found", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
oldStatus := order.OrderStatus
|
||||||
|
if oldStatus == from.Status {
|
||||||
|
ReturnJson(ctx, "status_no_change", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新状态
|
||||||
|
updates := map[string]interface{}{
|
||||||
|
"order_status": from.Status,
|
||||||
|
}
|
||||||
|
if err := models.DB.Model(&order).Updates(updates).Error; err != nil {
|
||||||
|
ReturnJson(ctx, "apiErr", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写状态变更 commit
|
||||||
|
comment := from.Comment
|
||||||
|
if comment == "" {
|
||||||
|
comment = "状态变更为: " + from.Status
|
||||||
|
}
|
||||||
|
commit := TabPurchaseCommit{
|
||||||
|
OrderID: order.ID,
|
||||||
|
UserID: user.ID,
|
||||||
|
Action: "create_status",
|
||||||
|
Status: from.Status,
|
||||||
|
OldStatus: oldStatus,
|
||||||
|
Comment: comment,
|
||||||
|
IP: ctx.ClientIP(),
|
||||||
|
}
|
||||||
|
models.DB.Create(&commit)
|
||||||
|
|
||||||
|
// 写操作日志
|
||||||
|
newContent, _ := json.Marshal(map[string]string{
|
||||||
|
"status": from.Status,
|
||||||
|
"comment": comment,
|
||||||
|
})
|
||||||
|
oldContent, _ := json.Marshal(map[string]string{
|
||||||
|
"status": oldStatus,
|
||||||
|
})
|
||||||
|
tosqllog := TabPurchaseLog{
|
||||||
|
UserID: user.ID,
|
||||||
|
OrderID: order.ID,
|
||||||
|
ActionType: "update_status",
|
||||||
|
NewContent: string(newContent),
|
||||||
|
OldContent: string(oldContent),
|
||||||
|
IP: ctx.ClientIP(),
|
||||||
|
Remark: comment,
|
||||||
|
}
|
||||||
|
models.DB.Create(&tosqllog)
|
||||||
|
|
||||||
|
ReturnJson(ctx, "apiOK", nil)
|
||||||
|
})
|
||||||
|
|
||||||
r.POST("/getorders", func(ctx *gin.Context) {
|
r.POST("/getorders", func(ctx *gin.Context) {
|
||||||
isAuth, _, data := AuthenticationAuthority(ctx)
|
isAuth, _, data := AuthenticationAuthority(ctx)
|
||||||
if isAuth {
|
if isAuth {
|
||||||
@@ -187,26 +345,33 @@ func ApiPurchase(r *gin.RouterGroup) {
|
|||||||
|
|
||||||
if is_data_ok {
|
if is_data_ok {
|
||||||
//校验通过
|
//校验通过
|
||||||
//载入数据库
|
//photos, _ := json.Marshal(jsondata.Photos)
|
||||||
|
|
||||||
//fmt.Println("yes")
|
|
||||||
|
|
||||||
//photos, _ := json.Marshal(jsondata.Photos) //把图片数组转换成字符串
|
|
||||||
new_data := TabPurchaseOrder{
|
new_data := TabPurchaseOrder{
|
||||||
UserID: user.ID,
|
UserID: user.ID,
|
||||||
Title: jsondata.Title,
|
Title: jsondata.Title,
|
||||||
Remark: jsondata.Remark,
|
Remark: jsondata.Remark,
|
||||||
Link: jsondata.Link,
|
Link: jsondata.Link,
|
||||||
Styles: jsondata.Styles,
|
Styles: jsondata.Styles,
|
||||||
|
OrderStatus: "pending", // 默认待处理
|
||||||
}
|
}
|
||||||
models.DB.Create(&new_data)
|
models.DB.Create(&new_data)
|
||||||
|
|
||||||
for i := 0; i < len(jsondata.Costs); i++ {
|
for i := 0; i < len(jsondata.Costs); i++ {
|
||||||
|
currencyType := jsondata.Costs[i].CurrencyType
|
||||||
|
if currencyType <= 0 {
|
||||||
|
currencyType = 1 // 默认 CNY
|
||||||
|
}
|
||||||
|
costType := jsondata.Costs[i].Type
|
||||||
|
if costType <= 0 {
|
||||||
|
costType = 1 // 默认单价
|
||||||
|
}
|
||||||
new_cost_data := TabPurchaseCosts{
|
new_cost_data := TabPurchaseCosts{
|
||||||
Price: jsondata.Costs[i].Cost,
|
Price: jsondata.Costs[i].Cost,
|
||||||
Quantity: jsondata.Costs[i].Int,
|
Quantity: jsondata.Costs[i].Int,
|
||||||
UserID: user.ID,
|
UserID: user.ID,
|
||||||
OrderID: new_data.ID,
|
OrderID: new_data.ID,
|
||||||
|
CurrencyType: currencyType,
|
||||||
|
CostType: costType,
|
||||||
}
|
}
|
||||||
models.DB.Create(&new_cost_data)
|
models.DB.Create(&new_cost_data)
|
||||||
}
|
}
|
||||||
@@ -224,18 +389,31 @@ func ApiPurchase(r *gin.RouterGroup) {
|
|||||||
}
|
}
|
||||||
models.DB.Create(&bind)
|
models.DB.Create(&bind)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
newContent, _ := json.Marshal(jsondata) // 👈 转 JSON
|
|
||||||
|
// 写创建日志
|
||||||
|
newContent, _ := json.Marshal(jsondata)
|
||||||
tosqllog := TabPurchaseLog{
|
tosqllog := TabPurchaseLog{
|
||||||
UserID: user.ID,
|
UserID: user.ID,
|
||||||
OrderID: new_data.ID,
|
OrderID: new_data.ID,
|
||||||
ActionType: "create",
|
ActionType: "create",
|
||||||
NewContent: string(newContent), // 👈 直接赋值
|
NewContent: string(newContent),
|
||||||
OldContent: "",
|
OldContent: "",
|
||||||
IP: ctx.ClientIP(),
|
IP: ctx.ClientIP(),
|
||||||
}
|
}
|
||||||
models.DB.Debug().Create(&tosqllog)
|
models.DB.Create(&tosqllog)
|
||||||
|
|
||||||
|
// 写状态创建 commit
|
||||||
|
commitLog := TabPurchaseCommit{
|
||||||
|
OrderID: new_data.ID,
|
||||||
|
UserID: user.ID,
|
||||||
|
Action: "create",
|
||||||
|
Status: "pending",
|
||||||
|
OldStatus: "",
|
||||||
|
Comment: "订单创建",
|
||||||
|
IP: ctx.ClientIP(),
|
||||||
|
}
|
||||||
|
models.DB.Create(&commitLog)
|
||||||
|
|
||||||
ReturnJson(ctx, "apiOK", nil)
|
ReturnJson(ctx, "apiOK", nil)
|
||||||
|
|
||||||
|
|||||||
@@ -10,4 +10,14 @@ export const purchaseApi = {
|
|||||||
addOrder(data) {
|
addOrder(data) {
|
||||||
return api.post('/purchase/addorder', data)
|
return api.post('/purchase/addorder', data)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** 获取单个订单详情(包含费用明细、图片、状态变更记录) */
|
||||||
|
getOrder(id) {
|
||||||
|
return api.post('/purchase/getorder', { id })
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 更新订单状态(可附带评论) */
|
||||||
|
updateOrderStatus(id, status, comment = '') {
|
||||||
|
return api.post('/purchase/updatestatus', { id, status, comment })
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,10 @@
|
|||||||
"created_at": "Created At",
|
"created_at": "Created At",
|
||||||
"updated_at": "Updated At",
|
"updated_at": "Updated At",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
|
"status_pending": "Pending",
|
||||||
|
"status_ordered": "Ordered",
|
||||||
|
"status_arrived": "Arrived",
|
||||||
|
"status_received": "Received",
|
||||||
"completed": "Completed",
|
"completed": "Completed",
|
||||||
"pending": "Pending",
|
"pending": "Pending",
|
||||||
"show": "Show",
|
"show": "Show",
|
||||||
@@ -74,7 +78,23 @@
|
|||||||
"add_part": "Add Order",
|
"add_part": "Add Order",
|
||||||
"exp_report": "Export Report",
|
"exp_report": "Export Report",
|
||||||
"There_are_a_total_of": ",There are a total of",
|
"There_are_a_total_of": ",There are a total of",
|
||||||
"items": "items."
|
"items": "items.",
|
||||||
|
"order_detail": "Order Detail",
|
||||||
|
"back_to_list": "Back to List",
|
||||||
|
"order_not_found": "Order Not Found",
|
||||||
|
"order_info": "Order Information",
|
||||||
|
"cost_detail": "Cost Details",
|
||||||
|
"photo_remarks": "Photos",
|
||||||
|
"link": "Link",
|
||||||
|
"no_photos": "No photos",
|
||||||
|
"open_link": "Open Link",
|
||||||
|
"no_costs": "No cost records",
|
||||||
|
"cost_total": "Total",
|
||||||
|
"change_status": "Change Status",
|
||||||
|
"commit_history": "Status History",
|
||||||
|
"no_commits": "No status records",
|
||||||
|
"commit_placeholder": "Add comment (optional)",
|
||||||
|
"commit_create": "Order created"
|
||||||
},
|
},
|
||||||
"purchase_addorder": {
|
"purchase_addorder": {
|
||||||
"add_order": "Add Order",
|
"add_order": "Add Order",
|
||||||
|
|||||||
@@ -66,6 +66,10 @@
|
|||||||
"created_at": "创建日期",
|
"created_at": "创建日期",
|
||||||
"updated_at": "更新日期",
|
"updated_at": "更新日期",
|
||||||
"status": "状态",
|
"status": "状态",
|
||||||
|
"status_pending": "待处理",
|
||||||
|
"status_ordered": "已下单",
|
||||||
|
"status_arrived": "已到达",
|
||||||
|
"status_received": "已收件",
|
||||||
"completed": "已完成",
|
"completed": "已完成",
|
||||||
"pending": "待处理",
|
"pending": "待处理",
|
||||||
"show": "显示",
|
"show": "显示",
|
||||||
@@ -74,7 +78,23 @@
|
|||||||
"add_part": "添加订单",
|
"add_part": "添加订单",
|
||||||
"exp_report": "生成报告",
|
"exp_report": "生成报告",
|
||||||
"There_are_a_total_of": ",一共",
|
"There_are_a_total_of": ",一共",
|
||||||
"items": "个物件"
|
"items": "个物件",
|
||||||
|
"order_detail": "订单详情",
|
||||||
|
"back_to_list": "返回列表",
|
||||||
|
"order_not_found": "订单不存在",
|
||||||
|
"order_info": "订单信息",
|
||||||
|
"cost_detail": "费用明细",
|
||||||
|
"photo_remarks": "图片备注",
|
||||||
|
"link": "链接",
|
||||||
|
"no_photos": "暂无图片",
|
||||||
|
"open_link": "打开链接",
|
||||||
|
"no_costs": "暂无费用记录",
|
||||||
|
"cost_total": "合计",
|
||||||
|
"change_status": "变更状态",
|
||||||
|
"commit_history": "状态记录",
|
||||||
|
"no_commits": "暂无状态记录",
|
||||||
|
"commit_placeholder": "添加备注(可选)",
|
||||||
|
"commit_create": "订单创建"
|
||||||
},
|
},
|
||||||
"purchase_addorder": {
|
"purchase_addorder": {
|
||||||
"add_order": "添加订单",
|
"add_order": "添加订单",
|
||||||
|
|||||||
@@ -122,14 +122,13 @@ onMounted(fetchOrders)
|
|||||||
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400">{{ t('purchase.item_name') }}</th>
|
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400">{{ t('purchase.item_name') }}</th>
|
||||||
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400">{{ t('purchase.purpose') }}</th>
|
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400">{{ t('purchase.purpose') }}</th>
|
||||||
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400">{{ t('purchase.quantity') }}</th>
|
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400">{{ t('purchase.quantity') }}</th>
|
||||||
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 whitespace-nowrap">{{ t('purchase.unit_price') }}</th>
|
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 whitespace-nowrap">{{ t('purchase.created_at') }}</th>
|
||||||
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 whitespace-nowrap">{{ t('purchase.total_price') }}</th>
|
|
||||||
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400">{{ t('purchase.status') }}</th>
|
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400">{{ t('purchase.status') }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-if="loading">
|
<tr v-if="loading">
|
||||||
<td colspan="7" class="px-6 py-8 text-center text-gray-400">
|
<td colspan="6" class="px-6 py-8 text-center text-gray-400">
|
||||||
<svg class="mx-auto mb-2 h-5 w-5 animate-spin text-gray-400" viewBox="0 0 24 24" fill="none">
|
<svg class="mx-auto mb-2 h-5 w-5 animate-spin text-gray-400" viewBox="0 0 24 24" fill="none">
|
||||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
||||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||||
@@ -140,16 +139,26 @@ onMounted(fetchOrders)
|
|||||||
<tr
|
<tr
|
||||||
v-for="order in orders"
|
v-for="order in orders"
|
||||||
:key="order.ID"
|
:key="order.ID"
|
||||||
class="border-b border-gray-100 transition-colors hover:bg-blue-50/50 dark:border-dk-muted/50 dark:bg-dk-card dark:hover:bg-dk-base/50"
|
class="cursor-pointer border-b border-gray-100 transition-colors hover:bg-blue-50/50 dark:border-dk-muted/50 dark:bg-dk-card dark:hover:bg-dk-base/50"
|
||||||
@click="jumpToOrder(order.ID)"
|
@click="jumpToOrder(order.ID)"
|
||||||
>
|
>
|
||||||
<td class="px-6 py-3 text-gray-400">{{ order.ID }}</td>
|
<td class="px-6 py-3 text-gray-400">{{ order.ID }}</td>
|
||||||
<td class="px-6 py-3 font-medium text-gray-900 dark:text-white">{{ order.Title }}</td>
|
<td class="px-6 py-3 font-medium text-gray-900 dark:text-white">{{ order.Title }}</td>
|
||||||
<td class="px-6 py-3 text-gray-600 dark:text-gray-300">{{ order.Remark }}</td>
|
<td class="px-6 py-3 max-w-[200px] truncate text-gray-600 dark:text-gray-300">{{ order.Remark || '-' }}</td>
|
||||||
<td class="px-6 py-3 text-gray-600 dark:text-gray-300">1</td>
|
<td class="px-6 py-3 whitespace-nowrap text-gray-500 dark:text-gray-400">{{ formatDate(order.CreatedAt) }}</td>
|
||||||
<td class="px-6 py-3 whitespace-nowrap text-gray-500">{{ formatDate(order.UnitPriceAt) }}</td>
|
<td class="px-6 py-3">
|
||||||
<td class="px-6 py-3 whitespace-nowrap text-gray-500">{{ formatDate(order.TotalPriceAt) }}</td>
|
<span
|
||||||
<td class="px-6 py-3 text-gray-600 dark:text-gray-300">1</td>
|
class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold"
|
||||||
|
:class="{
|
||||||
|
'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/40 dark:text-yellow-400': order.OrderStatus === 'pending',
|
||||||
|
'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-400': order.OrderStatus === 'ordered',
|
||||||
|
'bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-400': order.OrderStatus === 'arrived',
|
||||||
|
'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-400': order.OrderStatus === 'received',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ t('purchase.status_' + order.OrderStatus) }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -1,17 +1,404 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { useRouter } from 'vue-router'
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||||||
|
import { useRoute, useRouter, RouterLink } from 'vue-router'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useToastStore } from '@/stores/toast'
|
||||||
import { usePageTitle } from '@/composables/usePageTitle'
|
import { usePageTitle } from '@/composables/usePageTitle'
|
||||||
|
import { purchaseApi } from '@/api/purchase'
|
||||||
|
import {
|
||||||
|
IconChevronLeft, IconExternalLink, IconPhoto,
|
||||||
|
IconCheck, IconLoader2
|
||||||
|
} from '@tabler/icons-vue'
|
||||||
|
|
||||||
usePageTitle('purchase.add_part')
|
usePageTitle('purchase.order_detail')
|
||||||
|
const { t, locale } = useI18n()
|
||||||
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const orderId = router.params.id
|
const toast = useToastStore()
|
||||||
|
|
||||||
|
const orderId = computed(() => parseInt(route.params.id))
|
||||||
|
|
||||||
|
const order = ref(null)
|
||||||
|
const costs = ref([])
|
||||||
|
const photos = ref([])
|
||||||
|
const commits = ref([])
|
||||||
|
const loading = ref(true)
|
||||||
|
const notFound = ref(false)
|
||||||
|
const updatingStatus = ref(false)
|
||||||
|
|
||||||
|
// 状态选项
|
||||||
|
const statusOptions = [
|
||||||
|
{ value: 'pending', labelKey: 'status_pending', color: 'yellow' },
|
||||||
|
{ value: 'ordered', labelKey: 'status_ordered', color: 'blue' },
|
||||||
|
{ value: 'arrived', labelKey: 'status_arrived', color: 'purple' },
|
||||||
|
{ value: 'received', labelKey: 'status_received', color: 'green' },
|
||||||
|
]
|
||||||
|
|
||||||
|
// 状态颜色映射
|
||||||
|
const statusColorClass = computed(() => ({
|
||||||
|
pending: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/40 dark:text-yellow-400',
|
||||||
|
ordered: 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-400',
|
||||||
|
arrived: 'bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-400',
|
||||||
|
received: 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-400',
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 货币选项
|
||||||
|
const currencyOptions = { 1: 'CNY', 2: 'MOP', 3: 'HKD', 4: 'USD' }
|
||||||
|
|
||||||
|
// 费用类型映射
|
||||||
|
const costTypeMap = computed(() => ({
|
||||||
|
1: t('cost_type.unit_price'),
|
||||||
|
2: t('cost_type.freight'),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 合计费用
|
||||||
|
const costTotalYuan = computed(() => {
|
||||||
|
return costs.value.reduce((sum, c) => sum + (c.Price || 0) * (c.Quantity || 0), 0) / 100
|
||||||
|
})
|
||||||
|
|
||||||
|
// 按货币分组统计
|
||||||
|
const costsByCurrency = computed(() => {
|
||||||
|
const groups = {}
|
||||||
|
costs.value.forEach(c => {
|
||||||
|
const cur = currencyOptions[c.CurrencyType] || 'Unknown'
|
||||||
|
const amount = c.Price && c.Quantity ? (c.Price * c.Quantity / 100).toFixed(2) : '0.00'
|
||||||
|
if (!groups[cur]) groups[cur] = 0
|
||||||
|
groups[cur] += parseFloat(amount)
|
||||||
|
})
|
||||||
|
return Object.entries(groups).map(([currency, total]) => ({
|
||||||
|
currency,
|
||||||
|
total: total.toFixed(2),
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
function formatDate(dateStr) {
|
||||||
|
if (!dateStr) return '-'
|
||||||
|
const d = new Date(dateStr)
|
||||||
|
if (isNaN(d.getTime())) return '-'
|
||||||
|
return new Intl.DateTimeFormat(locale.value, {
|
||||||
|
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||||
|
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false,
|
||||||
|
}).format(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPrice(priceInCents) {
|
||||||
|
if (!priceInCents) return '0.00'
|
||||||
|
return (priceInCents / 100).toFixed(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPhotoUrl(file) {
|
||||||
|
return `/api/files/get/${file.Sha256}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function openLink() {
|
||||||
|
if (!order.value?.Link) return
|
||||||
|
let url = order.value.Link.trim()
|
||||||
|
if (!/^https?:\/\//i.test(url)) url = 'https://' + url
|
||||||
|
window.open(url, '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusLabel(status) {
|
||||||
|
const opt = statusOptions.find(o => o.value === status)
|
||||||
|
return opt ? t('purchase.' + opt.labelKey) : status
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusColorClass(status) {
|
||||||
|
return statusColorClass.value[status] || 'bg-gray-100 text-gray-600'
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleStatusChange(newStatus) {
|
||||||
|
if (newStatus === order.value?.OrderStatus) return
|
||||||
|
updatingStatus.value = true
|
||||||
|
try {
|
||||||
|
const { errCode } = await purchaseApi.updateOrderStatus(orderId.value, newStatus, '')
|
||||||
|
if (errCode === 0) {
|
||||||
|
order.value.OrderStatus = newStatus
|
||||||
|
toast.success(t('message.save_success') || 'Saved')
|
||||||
|
await fetchOrder() // 刷新 commit 记录
|
||||||
|
} else {
|
||||||
|
toast.error(t('message.server_error'))
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
toast.error(t('message.server_error'))
|
||||||
|
} finally {
|
||||||
|
updatingStatus.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchOrder() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const { errCode, data } = await purchaseApi.getOrder(orderId.value)
|
||||||
|
if (errCode === 0 && data) {
|
||||||
|
order.value = data.order ?? null
|
||||||
|
costs.value = data.costs ?? []
|
||||||
|
photos.value = data.photos ?? []
|
||||||
|
commits.value = data.commits ?? []
|
||||||
|
} else {
|
||||||
|
notFound.value = true
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
notFound.value = true
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(fetchOrder)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="mx-auto max-w-6xl px-6 py-6">
|
<div class="mx-auto max-w-6xl px-6 py-6">
|
||||||
<h2 class="mb-6 text-2xl font-bold text-gray-900 dark:text-white">Order#{{ orderId }}</h2>
|
|
||||||
<div class="rounded-xl border border-gray-200 bg-white px-12 py-12 text-center shadow-lg dark:border-dk-muted dark:bg-dk-card">
|
<!-- 返回按钮 -->
|
||||||
<p class="text-gray-400">{{ $t('message.functionality_not_yet_developed') }}</p>
|
<div class="mb-4">
|
||||||
|
<RouterLink
|
||||||
|
to="/purchase"
|
||||||
|
class="inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-dk-card dark:hover:text-gray-200"
|
||||||
|
>
|
||||||
|
<IconChevronLeft :size="16" />
|
||||||
|
{{ t('purchase.back_to_list') }}
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading -->
|
||||||
|
<div v-if="loading" class="flex items-center justify-center py-24">
|
||||||
|
<svg class="h-8 w-8 animate-spin text-blue-500" viewBox="0 0 24 24" fill="none">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||||
|
</svg>
|
||||||
|
<span class="ml-3 text-gray-500">Loading...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Not Found -->
|
||||||
|
<div v-else-if="notFound" class="rounded-xl border border-gray-200 bg-white py-16 text-center shadow-lg dark:border-dk-muted dark:bg-dk-card">
|
||||||
|
<p class="text-gray-400">{{ t('purchase.order_not_found') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Order Detail -->
|
||||||
|
<div v-else class="flex flex-col gap-6">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="rounded-xl border border-gray-200 bg-white shadow-lg dark:border-dk-muted dark:bg-dk-card">
|
||||||
|
<div class="flex items-center justify-between border-b border-gray-100 px-6 py-4 dark:border-dk-muted">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
{{ t('purchase.order_detail') }} #{{ orderId }}
|
||||||
|
</h2>
|
||||||
|
<!-- 当前状态标签 -->
|
||||||
|
<span
|
||||||
|
v-if="order?.OrderStatus"
|
||||||
|
class="inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-semibold"
|
||||||
|
:class="getStatusColorClass(order.OrderStatus)"
|
||||||
|
>
|
||||||
|
<span v-if="updatingStatus" class="inline-flex items-center gap-1">
|
||||||
|
<IconLoader2 :size="10" class="animate-spin" />
|
||||||
|
</span>
|
||||||
|
{{ getStatusLabel(order.OrderStatus) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-sm text-gray-400">{{ formatDate(order?.CreatedAt) }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 状态快捷切换按钮 -->
|
||||||
|
<div class="flex flex-wrap gap-2 border-b border-gray-100 px-6 py-3 dark:border-dk-muted">
|
||||||
|
<span class="text-sm text-gray-500 dark:text-gray-400">{{ t('purchase.change_status') }}:</span>
|
||||||
|
<button
|
||||||
|
v-for="opt in statusOptions"
|
||||||
|
:key="opt.value"
|
||||||
|
class="inline-flex items-center gap-1 rounded-full border px-3 py-1 text-xs font-medium transition-all"
|
||||||
|
:class="order?.OrderStatus === opt.value
|
||||||
|
? [getStatusColorClass(opt.value), 'border-transparent']
|
||||||
|
: 'border-gray-200 text-gray-500 hover:border-gray-300 hover:bg-gray-50 dark:border-dk-muted dark:text-gray-400 dark:hover:bg-dk-base'"
|
||||||
|
:disabled="updatingStatus"
|
||||||
|
@click="handleStatusChange(opt.value)"
|
||||||
|
>
|
||||||
|
<IconCheck v-if="order?.OrderStatus === opt.value" :size="12" />
|
||||||
|
{{ t('purchase.' + opt.labelKey) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Order Info -->
|
||||||
|
<div class="space-y-4 px-6 py-5">
|
||||||
|
<h4 class="text-sm font-semibold text-gray-500 dark:text-gray-400">{{ t('purchase.order_info') }}</h4>
|
||||||
|
|
||||||
|
<div class="grid gap-4 sm:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-xs font-medium text-gray-400">{{ t('purchase_addorder.part_name') }}</label>
|
||||||
|
<p class="font-medium text-gray-900 dark:text-white">{{ order?.Title || '-' }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-xs font-medium text-gray-400">{{ t('purchase.link') }}</label>
|
||||||
|
<div v-if="order?.Link" class="flex items-center gap-2">
|
||||||
|
<p class="max-w-xs truncate text-blue-600 dark:text-blue-400">{{ order.Link }}</p>
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center gap-1 rounded px-2 py-0.5 text-xs text-blue-600 hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-blue-900/20"
|
||||||
|
@click="openLink"
|
||||||
|
>
|
||||||
|
<IconExternalLink :size="14" />
|
||||||
|
{{ t('purchase.open_link') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p v-else class="text-gray-400">-</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="order?.Styles">
|
||||||
|
<label class="mb-1 block text-xs font-medium text-gray-400">{{ t('purchase_addorder.style_remarks') }}</label>
|
||||||
|
<p class="text-gray-700 dark:text-gray-200">{{ order.Styles }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="order?.Remark" class="sm:col-span-2">
|
||||||
|
<label class="mb-1 block text-xs font-medium text-gray-400">{{ t('purchase_addorder.remarks') }}</label>
|
||||||
|
<p class="whitespace-pre-wrap text-gray-700 dark:text-gray-200">{{ order.Remark }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 费用明细 -->
|
||||||
|
<div class="rounded-xl border border-gray-200 bg-white shadow-lg dark:border-dk-muted dark:bg-dk-card">
|
||||||
|
<div class="border-b border-gray-100 px-6 py-4 dark:border-dk-muted">
|
||||||
|
<h4 class="text-sm font-semibold text-gray-500 dark:text-gray-400">{{ t('purchase.cost_detail') }}</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="costs.length" class="overflow-x-auto px-0">
|
||||||
|
<table class="w-full text-left text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr class="border-b border-gray-100 bg-gray-50 text-gray-500 dark:border-dk-muted dark:bg-dk-base">
|
||||||
|
<th class="px-6 py-3 font-medium">{{ t('purchase_addorder.fee_type') }}</th>
|
||||||
|
<th class="px-6 py-3 font-medium">{{ t('purchase_addorder.quantity') }}</th>
|
||||||
|
<th class="px-6 py-3 font-medium">{{ t('purchase.unit_price') }}</th>
|
||||||
|
<th class="px-6 py-3 font-medium">{{ t('purchase.total_price') }}</th>
|
||||||
|
<th class="px-6 py-3 font-medium">{{ t('purchase_addorder.currency') }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="(item, idx) in costs"
|
||||||
|
:key="idx"
|
||||||
|
class="border-b border-gray-50 dark:border-dk-muted/50"
|
||||||
|
>
|
||||||
|
<td class="px-6 py-3 font-medium text-gray-800 dark:text-white">
|
||||||
|
{{ costTypeMap[item.CostType] || item.CostType }}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-3 text-gray-600 dark:text-gray-300">{{ item.Quantity }}</td>
|
||||||
|
<td class="px-6 py-3 text-gray-600 dark:text-gray-300">{{ formatPrice(item.Price) }}</td>
|
||||||
|
<td class="px-6 py-3 font-medium text-gray-900 dark:text-white">
|
||||||
|
{{ formatPrice(item.Price * item.Quantity) }}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-3 text-gray-600 dark:text-gray-300">
|
||||||
|
{{ currencyOptions[item.CurrencyType] || '-' }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr class="bg-gray-50 dark:bg-dk-base">
|
||||||
|
<td colspan="3" class="px-6 py-3 text-sm font-semibold text-gray-600 dark:text-gray-300">
|
||||||
|
{{ t('purchase.cost_total') }}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-3 text-base font-bold text-gray-900 dark:text-white">
|
||||||
|
{{ costTotalYuan.toFixed(2) }}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-3">
|
||||||
|
<span
|
||||||
|
v-for="g in costsByCurrency"
|
||||||
|
:key="g.currency"
|
||||||
|
class="mr-2 inline-block rounded bg-blue-100 px-1.5 py-0.5 text-xs font-medium text-blue-700 dark:bg-blue-900/40 dark:text-blue-300"
|
||||||
|
>
|
||||||
|
{{ g.currency }} {{ g.total }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="px-6 py-8 text-center text-gray-400">
|
||||||
|
{{ t('purchase.no_costs') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 状态记录(Commit History) -->
|
||||||
|
<div class="rounded-xl border border-gray-200 bg-white shadow-lg dark:border-dk-muted dark:bg-dk-card">
|
||||||
|
<div class="border-b border-gray-100 px-6 py-4 dark:border-dk-muted">
|
||||||
|
<h4 class="text-sm font-semibold text-gray-500 dark:text-gray-400">{{ t('purchase.commit_history') }}</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="commits.length" class="divide-y divide-gray-50 px-6 py-2 dark:divide-dk-muted/50">
|
||||||
|
<div
|
||||||
|
v-for="commit in commits"
|
||||||
|
:key="commit.ID"
|
||||||
|
class="flex items-start gap-4 py-3"
|
||||||
|
>
|
||||||
|
<!-- 时间线点 -->
|
||||||
|
<div class="mt-1 flex-shrink-0">
|
||||||
|
<div
|
||||||
|
class="h-2.5 w-2.5 rounded-full"
|
||||||
|
:class="{
|
||||||
|
'bg-yellow-400': commit.Status === 'pending',
|
||||||
|
'bg-blue-400': commit.Status === 'ordered',
|
||||||
|
'bg-purple-400': commit.Status === 'arrived',
|
||||||
|
'bg-green-400': commit.Status === 'received',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 内容 -->
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<span class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium" :class="getStatusColorClass(commit.Status)">
|
||||||
|
{{ getStatusLabel(commit.Status) }}
|
||||||
|
</span>
|
||||||
|
<span v-if="commit.Action === 'create'" class="text-xs text-gray-400">
|
||||||
|
{{ t('purchase.commit_create') }}
|
||||||
|
</span>
|
||||||
|
<span v-else class="text-xs text-gray-400">
|
||||||
|
{{ commit.OldStatus ? getStatusLabel(commit.OldStatus) + ' → ' : '' }}{{ getStatusLabel(commit.Status) }}
|
||||||
|
</span>
|
||||||
|
<span class="text-xs text-gray-400">{{ formatDate(commit.CreatedAt) }}</span>
|
||||||
|
</div>
|
||||||
|
<p v-if="commit.Comment" class="mt-1 text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
{{ commit.Comment }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="px-6 py-8 text-center text-sm text-gray-400">
|
||||||
|
{{ t('purchase.no_commits') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 图片备注 -->
|
||||||
|
<div class="rounded-xl border border-gray-200 bg-white shadow-lg dark:border-dk-muted dark:bg-dk-card">
|
||||||
|
<div class="border-b border-gray-100 px-6 py-4 dark:border-dk-muted">
|
||||||
|
<h4 class="text-sm font-semibold text-gray-500 dark:text-gray-400">{{ t('purchase.photo_remarks') }}</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="photos.length" class="flex flex-wrap gap-3 px-6 py-5">
|
||||||
|
<a
|
||||||
|
v-for="photo in photos"
|
||||||
|
:key="photo.ID"
|
||||||
|
:href="getPhotoUrl(photo)"
|
||||||
|
target="_blank"
|
||||||
|
class="group relative block overflow-hidden rounded-lg border border-gray-200 dark:border-dk-muted"
|
||||||
|
style="width: 120px; height: 120px;"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="getPhotoUrl(photo)"
|
||||||
|
:alt="photo.Name || 'photo'"
|
||||||
|
class="h-full w-full object-cover transition-transform group-hover:scale-105"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="flex flex-col items-center justify-center gap-2 px-6 py-10 text-gray-400">
|
||||||
|
<IconPhoto :size="32" class="opacity-40" />
|
||||||
|
<span class="text-sm">{{ t('purchase.no_photos') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user