diff --git a/.workbuddy/expert-history.json b/.workbuddy/expert-history.json index 0a9d4d5..f6d0675 100644 --- a/.workbuddy/expert-history.json +++ b/.workbuddy/expert-history.json @@ -13,5 +13,5 @@ } ] }, - "lastUpdated": 1776140518763 + "lastUpdated": 1776144770632 } \ No newline at end of file diff --git a/backend/my_work/main.go b/backend/my_work/main.go index 319b45f..437f86e 100644 --- a/backend/my_work/main.go +++ b/backend/my_work/main.go @@ -67,6 +67,7 @@ func main() { //统一初始化 models.ConfigAllInit() routers.ApiUserInit() //用户表先初始化这是必须的因为后面需要用到用户组 + routers.ApiFilesInit() routers.ApiScheduleInit() routers.ApiPurchaseInit() diff --git a/backend/my_work/models/sql.go b/backend/my_work/models/sql.go index 6d901de..d147681 100644 --- a/backend/my_work/models/sql.go +++ b/backend/my_work/models/sql.go @@ -12,19 +12,6 @@ import ( var DB *gorm.DB -type TabFileInfo_ struct { - ID uint `gorm:"primaryKey;autoIncrement"` - Name string `gorm:"not null;size:256;index"` // 前端报告的文件名 - Path string `gorm:"not null;size:300"` // - Sha256 string `gorm:"not null;size:64;index"` // - Mime string `gorm:"size:64;index"` - Type string `gorm:"size:64;index"` - Const uint `gorm:"default:1;index"` - Per uint `gorm:"default:1"` - UserID uint `gorm:"not null;index"` - Date time.Time `gorm:"type:datetime;default:CURRENT_TIMESTAMP"` // 默认当前时间 -} - type TabUser_ struct { ID uint `gorm:"primaryKey;autoIncrement"` // 自增主键 Name string `gorm:"size:100;uniqueIndex"` // 唯一约束索引 @@ -126,8 +113,6 @@ func DatabaseInit() error { DB.AutoMigrate(&TabCookie_{}) - DB.AutoMigrate(&TabFileInfo_{}) - DB.AutoMigrate(&APIRequestLog_{}) return nil diff --git a/backend/my_work/routers/apiPurchase.go b/backend/my_work/routers/apiPurchase.go index d212aa8..ca85de9 100644 --- a/backend/my_work/routers/apiPurchase.go +++ b/backend/my_work/routers/apiPurchase.go @@ -139,7 +139,7 @@ func ApiPurchase(r *gin.RouterGroup) { for _, b := range binds { fileIDs = append(fileIDs, b.FileID) } - var files []models.TabFileInfo_ + var files []TabFileInfo_ if len(fileIDs) > 0 { models.DB.Where("id IN ?", fileIDs).Find(&files) } @@ -448,7 +448,7 @@ func ApiPurchase(r *gin.RouterGroup) { //绑定文件 for i := 0; i < len(jsondata.Photos); i++ { - findFile := models.TabFileInfo_{ + findFile := TabFileInfo_{ Sha256: jsondata.Photos[i], Type: "image", } @@ -588,7 +588,7 @@ func ApiPurchase(r *gin.RouterGroup) { // 重建图片绑定:先删旧,再插新 models.DB.Where("order_id = ?", from.ID).Delete(&TabPurchaseFileBind{}) for _, hash := range from.Photos { - findFile := models.TabFileInfo_{Sha256: hash, Type: "image"} + findFile := TabFileInfo_{Sha256: hash, Type: "image"} if models.DB.Where(&findFile).First(&findFile).Error == nil { models.DB.Create(&TabPurchaseFileBind{ OrderID: from.ID, diff --git a/backend/my_work/routers/api_Files.go b/backend/my_work/routers/api_Files.go index e0d9a6f..9813ba3 100644 --- a/backend/my_work/routers/api_Files.go +++ b/backend/my_work/routers/api_Files.go @@ -6,14 +6,33 @@ import ( "ops/models" "path" "path/filepath" + "time" "github.com/gin-gonic/gin" ) +type TabFileInfo_ struct { + ID uint `gorm:"primaryKey;autoIncrement"` + Name string `gorm:"not null;size:256;index"` // 前端报告的文件名 + Path string `gorm:"not null;size:300"` // + Sha256 string `gorm:"not null;size:64;index"` // + Mime string `gorm:"size:64;index"` + Type string `gorm:"size:64;index"` + Const uint `gorm:"default:1;index"` + Per uint `gorm:"default:1"` + UserID uint `gorm:"not null;index"` + Date time.Time `gorm:"type:datetime;default:CURRENT_TIMESTAMP"` // 默认当前时间 +} + func file_save() { } +func ApiFilesInit() { + + models.DB.AutoMigrate(&TabFileInfo_{}) +} + func ApiFiles(r *gin.RouterGroup) { //getfile := r.Group("/get") //定义上传组 @@ -35,7 +54,7 @@ func ApiFiles(r *gin.RouterGroup) { download = false } if isPartOK { - file_info := models.TabFileInfo_{ + file_info := TabFileInfo_{ Sha256: hash, } if models.DB.Where(&file_info).First(&file_info).Error == nil { @@ -107,14 +126,14 @@ func ApiFiles(r *gin.RouterGroup) { } //记录到数据库 //先检查数据库有没有数据 - fund_file_info := models.TabFileInfo_{ + fund_file_info := TabFileInfo_{ Name: filename, Sha256: hash_str, Mime: mimeType, Type: "image", UserID: user.ID, } - fund_file_info2 := models.TabFileInfo_{} + fund_file_info2 := TabFileInfo_{} models.DB.Where(&fund_file_info).Find(&fund_file_info2) diff --git a/backend/my_work/routers/return.go b/backend/my_work/routers/return.go index d0faa4e..6e69930 100644 --- a/backend/my_work/routers/return.go +++ b/backend/my_work/routers/return.go @@ -3,7 +3,6 @@ package routers import ( "encoding/json" "fmt" - "ops/models" "github.com/gin-gonic/gin" ) @@ -35,7 +34,7 @@ func ReturnJson(ctx *gin.Context, errMsg string, data map[string]interface{}) { } -func ReturnFile(ctx *gin.Context, file_info *models.TabFileInfo_, preview bool) { +func ReturnFile(ctx *gin.Context, file_info *TabFileInfo_, preview bool) { if preview { ctx.File(file_info.Path) } else { diff --git a/frontend/ops_vue_js/src/components/PurchaseOrderForm.vue b/frontend/ops_vue_js/src/components/PurchaseOrderForm.vue index d6a4dad..9b8a1eb 100644 --- a/frontend/ops_vue_js/src/components/PurchaseOrderForm.vue +++ b/frontend/ops_vue_js/src/components/PurchaseOrderForm.vue @@ -25,12 +25,7 @@ const props = defineProps({ type: Object, required: true, }, - /** 回填费用列表(分为单位) */ - initialCosts: { - type: Array, - default: () => [], - }, - /** 回填图片列表 */ + /** 回填图片列表 [{ Sha256, Name, ... }] */ initialPhotos: { type: Array, default: () => [], @@ -94,12 +89,12 @@ function removeCostEntry(index) { /** 将当前 costEntries(元)转换为分并同步到父组件 */ function syncCosts() { - const converted = costEntries.map((h) => ({ + // 直接更新父组件的 form._costs,跳过 emit 链路 + props.modelValue._costs = costEntries.map((h) => ({ ...h, cost: Math.round(h.cost * 100), costt: Math.round(h.costt * 100), })); - emit("update:modelValue", { ...props.modelValue, _costs: converted }); } watch( @@ -111,25 +106,24 @@ watch( }, ); -// 回填费用(分→元) -watch( - () => props.initialCosts, - (list) => { - if (!list || list.length === 0) return; - costEntries.splice(0, costEntries.length); - list.forEach((c) => { - costEntries.push({ - type: c.costType, - int: c.quantity, - cost: parseFloat((c.price / 100).toFixed(2)), - costt: parseFloat(((c.price * c.quantity) / 100).toFixed(2)), - currencytype: c.currencyType, - }); +// ==================== 外部初始化接口 ==================== +/** + * 由父组件调用,用于回填已有费用数据(来自 API) + * @param {Array} list 费用数组,单位:分 + */ +function initCostEntries(list) { + if (!list || list.length === 0) return; + costEntries.splice(0, costEntries.length); + list.forEach((c) => { + costEntries.push({ + type: c.type ?? c.CostType ?? 1, + int: c.int ?? c.Quantity ?? 1, + cost: parseFloat(((c.cost ?? c.Price) / 100).toFixed(2)), + costt: parseFloat(((c.costt ?? c.Price * (c.int ?? c.Quantity)) / 100).toFixed(2)), + currencytype: c.currencytype ?? c.CurrencyType ?? 1, }); - syncCosts(); - }, - { immediate: true }, -); + }); +} // ==================== 图片上传 ==================== const photosRef = ref(null); @@ -141,7 +135,7 @@ function getPhotoHashes() { return photosRef.value?.return_files().map((f) => f.hash) ?? []; } -defineExpose({ getPhotoHashes, costEntries }); +defineExpose({ getPhotoHashes, costEntries, initCostEntries }); // ==================== 表单字段双向绑定 ==================== function update(field, value) { diff --git a/frontend/ops_vue_js/src/components/useDropzone.vue b/frontend/ops_vue_js/src/components/useDropzone.vue index 61b38d4..14146b1 100644 --- a/frontend/ops_vue_js/src/components/useDropzone.vue +++ b/frontend/ops_vue_js/src/components/useDropzone.vue @@ -53,6 +53,11 @@ const prop = defineProps({ type: String, default: "/api/files/upload", }, + /** 初始已有文件 [{ hash, name, ... }] */ + initialFiles: { + type: Array, + default: () => [], + }, }); // 初始化 Dropzone @@ -201,11 +206,43 @@ function return_files() { return files; } +// 加载初始已有文件(编辑场景) +function loadInitialFiles() { + if (!dropzoneInstance || !prop.initialFiles?.length) return; + prop.initialFiles.forEach((f) => { + // 构造 Dropzone 期望的 mock file 对象 + const mockFile = { + name: f.Name || f.name || f.hash, + size: f.Size || f.size || 0, + type: f.Mime || f.mime || "image/jpeg", + status: Dropzone.SUCCESS, + accepted: true, + upload: { uuid: f.Hash || f.hash || f.Sha256 }, + previewElement: null, + _removeLink: null, + }; + // 通知 Dropzone "这是一个已存在的文件,不要上传" + dropzoneInstance.emit("addedfile", mockFile); + dropzoneInstance.emit("complete", mockFile); + dropzoneInstance.files.push(mockFile); + // 填充上传结果字段 + const url = `/api/files/get/${f.Hash || f.hash || f.Sha256}`; + files.push({ + uuid: f.Hash || f.hash || f.Sha256, + hash: f.Hash || f.hash || f.Sha256, + get_url: url, + download_url: `/api/files/download/${f.Hash || f.hash || f.Sha256}`, + file_name: f.Name || f.name || "", + file_size: f.Size || f.size || 0, + is_upload: true, + }); + }); +} + // 组件挂载时初始化 onMounted(() => { initDropzone(); - - //console.log(lightbox) + loadInitialFiles(); }); // 组件卸载时销毁 diff --git a/frontend/ops_vue_js/src/i18n/en.json b/frontend/ops_vue_js/src/i18n/en.json index 5db9cc4..8be64d7 100644 --- a/frontend/ops_vue_js/src/i18n/en.json +++ b/frontend/ops_vue_js/src/i18n/en.json @@ -129,6 +129,8 @@ "input_fee": "Fee", "select_currency": "Select currency", "add": "Add", + "add_cost": "Add Cost", + "upload_photos": "Upload Photos", "other_status": "Other Status", "update_time": "Update Time", "tracking_number": "Tracking Number", diff --git a/frontend/ops_vue_js/src/i18n/zh-CN.json b/frontend/ops_vue_js/src/i18n/zh-CN.json index dab54da..855a735 100644 --- a/frontend/ops_vue_js/src/i18n/zh-CN.json +++ b/frontend/ops_vue_js/src/i18n/zh-CN.json @@ -129,6 +129,8 @@ "input_fee": "费用", "select_currency": "选择货币类型", "add": "添加", + "add_cost": "添加费用", + "upload_photos": "上传图片", "other_status": "其他状态", "update_time": "更新时间", "tracking_number": "快递单号", diff --git a/frontend/ops_vue_js/src/views/purchase/editorder.vue b/frontend/ops_vue_js/src/views/purchase/editorder.vue index 3e9a5e3..e67b520 100644 --- a/frontend/ops_vue_js/src/views/purchase/editorder.vue +++ b/frontend/ops_vue_js/src/views/purchase/editorder.vue @@ -4,18 +4,19 @@ * * 功能概述: * - 通过路由参数 :id 加载已有订单数据 - * - 使用 PurchaseOrderForm 组件展示可编辑表单 + * - 费用明细直接在本页管理(与 addorder.vue 相同模式) * - 提交时调用 /purchase/updateorder 保存修改 */ -import { reactive, ref, onMounted } from "vue"; +import { reactive, ref, computed, watch, onMounted, nextTick } from "vue"; import { useI18n } from "vue-i18n"; import { useRoute, useRouter } from "vue-router"; import { useToastStore } from "@/stores/toast"; import { usePageTitle } from "@/composables/usePageTitle"; import { useValidation } from "@/composables"; import { purchaseApi } from "@/api/purchase"; -import PurchaseOrderForm from "@/components/PurchaseOrderForm.vue"; +import tagadder from "@/components/tagadder.vue"; +import useDropzone from "@/components/useDropzone.vue"; usePageTitle("purchase_addorder.edit_order"); @@ -32,24 +33,73 @@ const loading = ref(false); const pageLoading = ref(true); const pageError = ref(""); -/** 回填的费用明细(分为单位,传给 PurchaseOrderForm) */ -const initialCosts = ref([]); -/** 回填的图片列表 */ -const initialPhotos = ref([]); - -/** 表单数据 */ +// ==================== 表单数据 ==================== const form = reactive({ title: "", remark: "", link: "", styles: "", photos: [], - costs: [], - _costs: [], // 由 PurchaseOrderForm 组件同步的分为单位费用数组 }); -/** PurchaseOrderForm 组件引用(用于获取图片哈希) */ -const formRef = ref(null); +// ==================== 费用明细(与 addorder.vue 完全一致) ==================== +const textMaxLen = 256; +const currencyOptions = { 1: "CNY", 2: "MOP", 3: "HKD", 4: "USD" }; +const costType = computed(() => ({ + 1: t("cost_type.unit_price"), + 2: t("cost_type.freight"), +})); + +/** 已添加的费用列表 */ +const costEntries = reactive([]); +const costError = ref(false); + +const newCost = reactive({ + type: 1, + int: 1, + cost: 0, + currencytype: 1, +}); + +function addCostEntry() { + if (!newCost.cost || parseFloat(newCost.cost) <= 0) { + costError.value = true; + return; + } + const cost = parseFloat(newCost.cost); + costEntries.push({ + type: newCost.type, + int: newCost.int, + cost, + costt: parseFloat((cost * newCost.int).toFixed(2)), + currencytype: newCost.currencytype, + }); + newCost.cost = 0; + newCost.type = 1; + newCost.int = 1; + newCost.currencytype = 1; + costError.value = false; +} + +function removeCostEntry(idx) { + costEntries.splice(idx, 1); +} + +watch( + () => newCost.cost, + (val) => { + const fixed = parseFloat(val).toFixed(2); + if (parseFloat(fixed) !== val) newCost.cost = parseFloat(fixed); + if (val > 0) costError.value = false; + }, +); + +// ==================== 图片上传 ==================== +const dropzoneRef = ref(null); + +function getPhotoHashes() { + return dropzoneRef.value?.return_files().map((f) => f.hash) ?? []; +} // ==================== 加载订单数据 ==================== onMounted(async () => { @@ -61,14 +111,13 @@ onMounted(async () => { try { const res = await purchaseApi.getOrder(orderId); - console.log(res) - if (res.errCode !== 0 || res.raw?.err_code !== 0) { + if (res.errCode !== 0 || !res.data) { pageError.value = t("purchase.order_not_found"); pageLoading.value = false; return; } - const { order, costs, photos } = res.raw.data; + const { order, costs, photos } = res.data; // 回填基本信息 form.title = order.Title ?? ""; @@ -76,10 +125,24 @@ onMounted(async () => { form.link = order.Link ?? ""; form.styles = order.Styles ?? ""; - // 回填费用(传给子组件,由子组件转换为元展示) - initialCosts.value = costs ?? []; + // 回填费用(分→元,直接写 costEntries) + if (costs && costs.length > 0) { + costs.forEach((c) => { + costEntries.push({ + type: c.CostType, + int: c.Quantity, + cost: parseFloat((c.Price / 100).toFixed(2)), + costt: parseFloat(((c.Price * c.Quantity) / 100).toFixed(2)), + currencytype: c.CurrencyType, + }); + }); + } + // 回填图片 - initialPhotos.value = photos ?? []; + await nextTick(); + if (photos && photos.length > 0) { + dropzoneRef.value?.loadInitialFiles(photos); + } } catch { pageError.value = t("purchase.order_not_found"); } finally { @@ -93,10 +156,15 @@ async function handleSubmit() { const ok = validate("title", form.title, t("purchase_addorder.title")); if (!ok) return; - // 获取图片哈希 - form.photos = formRef.value?.getPhotoHashes() ?? []; - // 使用子组件同步的费用(分为单位) - form.costs = form._costs ?? []; + form.photos = getPhotoHashes(); + // 费用(转为分) + const rawCosts = costEntries.map((h) => ({ + type: h.type, + int: h.int, + cost: Math.round(h.cost * 100), + costt: Math.round(h.costt * 100), + currencytype: h.currencytype, + })); loading.value = true; try { @@ -106,10 +174,10 @@ async function handleSubmit() { link: form.link, styles: form.styles, photos: form.photos, - costs: form.costs, + costs: rawCosts, }); - if (res.errCode === 0 && res.raw?.err_code === 0) { + if (res.errCode === 0) { toast.success(t("message.save_ok")); setTimeout(() => { router.replace(`/purchase/showorder/${orderId}`); @@ -168,13 +236,163 @@ async function handleSubmit() { {{ errors.title }} - - + +
+

+ {{ t("purchase_addorder.order_info") }} +

+
+ +
+ +
+ + + {{ errors.title }} +
+ + +
+ +