This commit is contained in:
2026-04-14 12:14:45 +08:00
parent 4c16617e6c
commit 126e15dfa9
11 changed files with 684 additions and 14 deletions
+1 -1
View File
@@ -13,5 +13,5 @@
} }
] ]
}, },
"lastUpdated": 1776135998489 "lastUpdated": 1776136244169
} }
+12
View File
@@ -0,0 +1,12 @@
# 2026-04-14
## 修复 mapstructure IgnoreUntaggedField 编译错误
- `DecoderConfig` 不存在 `IgnoreUntaggedField` 字段,移除后改为 JSON 中转方案(`json.Marshal` + `json.Unmarshal`),绕过 mapstructure 字段名匹配问题,编译通过。
## 新增订单编辑功能
- 抽取公共组件 `src/components/PurchaseOrderForm.vue`,供 addorder/editorder 共用(标题/备注/链接/款式标签/费用明细/图片上传)
- 创建 `src/views/purchase/editorder.vue`:路由参数 `:id` 加载订单 → 回填 → 调用 `/purchase/updateorder` 保存
- 后端新增 `POST /purchase/updateorder` 接口:更新基本字段 + 重建费用明细 + 重建图片绑定 + 写操作日志,编译通过
- 前端 `purchase.js` 新增 `updateOrder(id, data)` 方法
- 注册路由 `purchase/editorder/:id`ShowOrder.vue 顶部加编辑按钮
- i18n 补充:`purchase.edit_order``purchase_addorder.edit_order``message.loading`(中英双语)
+109
View File
@@ -502,4 +502,113 @@ func ApiPurchase(r *gin.RouterGroup) {
//ReturnJson(ctx, "apiErr", nil) //ReturnJson(ctx, "apiErr", nil)
}) })
// 编辑订单基本信息
r.POST("/updateorder", func(ctx *gin.Context) {
isAuth, user, data := AuthenticationAuthority(ctx)
if !isAuth {
ReturnJson(ctx, "userCookieError", nil)
return
}
type FromUpdateOrder struct {
ID uint `json:"id"`
Title string `json:"title"`
Remark string `json:"remark"`
Link string `json:"link"`
Styles string `json:"styles"`
Photos []string `json:"photos"`
Costs []CostItem `json:"costs"`
}
var from FromUpdateOrder
if err := decodeJSON(data, &from); err != nil || from.ID == 0 {
ReturnJson(ctx, "jsonErr", nil)
return
}
if from.Title == "" {
ReturnJson(ctx, "jsonErr_1", nil)
return
}
// 校验图片哈希
for _, hash := range from.Photos {
if models.IsContainsSpecialChar(hash) {
ReturnJson(ctx, "photo_hash_invalid", nil)
return
}
}
// 校验费用
for _, c := range from.Costs {
if c.Cost <= 0 || c.Int <= 0 {
ReturnJson(ctx, "jsonErr_1", 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
}
// 记录旧数据
oldContent, _ := json.Marshal(order)
// 更新基本字段
if err := models.DB.Model(&order).Updates(map[string]interface{}{
"title": from.Title,
"remark": from.Remark,
"link": from.Link,
"styles": from.Styles,
}).Error; err != nil {
ReturnJson(ctx, "apiErr", nil)
return
}
// 重建费用明细:先删旧,再插新
models.DB.Where("order_id = ?", from.ID).Delete(&TabPurchaseCosts{})
for _, c := range from.Costs {
ct := c.CurrencyType
if ct <= 0 {
ct = 1
}
ctype := c.Type
if ctype <= 0 {
ctype = 1
}
models.DB.Create(&TabPurchaseCosts{
Price: c.Cost,
Quantity: c.Int,
UserID: user.ID,
OrderID: from.ID,
CurrencyType: ct,
CostType: ctype,
})
}
// 重建图片绑定:先删旧,再插新
models.DB.Where("order_id = ?", from.ID).Delete(&TabPurchaseFileBind{})
for _, hash := range from.Photos {
findFile := models.TabFileInfo_{Sha256: hash, Type: "image"}
if models.DB.Where(&findFile).First(&findFile).Error == nil {
models.DB.Create(&TabPurchaseFileBind{
OrderID: from.ID,
FileID: findFile.ID,
})
}
}
// 写操作日志
newContent, _ := json.Marshal(from)
models.DB.Create(&TabPurchaseLog{
UserID: user.ID,
OrderID: from.ID,
ActionType: "update",
OldContent: string(oldContent),
NewContent: string(newContent),
IP: ctx.ClientIP(),
})
ReturnJson(ctx, "apiOK", nil)
})
} }
+5
View File
@@ -20,4 +20,9 @@ export const purchaseApi = {
updateOrderStatus(id, status, comment = '', photos = []) { updateOrderStatus(id, status, comment = '', photos = []) {
return api.post('/purchase/updatestatus', { id, status, comment, photos }) return api.post('/purchase/updatestatus', { id, status, comment, photos })
}, },
/** 编辑订单基本信息(标题/备注/链接/款式/费用/图片) */
updateOrder(id, data) {
return api.post('/purchase/updateorder', { id, ...data })
},
} }
@@ -0,0 +1,323 @@
<script setup>
/**
* PurchaseOrderForm —— 采购订单表单公共组件
*
* 供 addorder.vue 和 editorder.vue 共用,包含:
* - 标题、备注、链接、款式标签
* - 费用明细(添加/删除)
* - 图片上传(useDropzone
*
* Props:
* modelValue {Object} 表单数据(v-model
* initialCosts {Array} 回填的费用列表(编辑时传入,单位:分)
* initialPhotos {Array} 回填的图片列表 [{ id, hash, name, ... }]
*
* Emits:
* update:modelValue
*/
import { reactive, ref, computed, watch, onMounted } from "vue";
import { useI18n } from "vue-i18n";
import tagadder from "@/components/tagadder.vue";
import useDropzone from "@/components/useDropzone.vue";
const props = defineProps({
modelValue: {
type: Object,
required: true,
},
/** 回填费用列表(分为单位) */
initialCosts: {
type: Array,
default: () => [],
},
/** 回填图片列表 */
initialPhotos: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(["update:modelValue"]);
const { t } = useI18n();
// ==================== 常量 ====================
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 newCost = reactive({
type: 1,
int: 1,
cost: 0,
currencyType: 1,
});
const costError = ref(false);
const newCostTotal = computed(() =>
parseFloat((newCost.int * newCost.cost).toFixed(2)),
);
function addCostEntry() {
if (newCost.cost <= 0) {
costError.value = true;
return;
}
costError.value = false;
costEntries.push({
type: newCost.type,
int: newCost.int,
cost: newCost.cost,
costt: newCostTotal.value,
currencytype: newCost.currencyType,
});
newCost.type = 1;
newCost.int = 1;
newCost.cost = 0;
newCost.currencyType = 1;
syncCosts();
}
function removeCostEntry(index) {
costEntries.splice(index, 1);
syncCosts();
}
/** 将当前 costEntries(元)转换为分并同步到父组件 */
function syncCosts() {
const converted = costEntries.map((h) => ({
...h,
cost: Math.round(h.cost * 100),
costt: Math.round(h.costt * 100),
}));
emit("update:modelValue", { ...props.modelValue, _costs: converted });
}
watch(
() => newCost.cost,
(val) => {
const fixed = parseFloat(val).toFixed(2);
if (parseFloat(fixed) !== val) newCost.cost = parseFloat(fixed);
if (val > 0) costError.value = false;
},
);
// 回填费用(分→元)
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,
});
});
syncCosts();
},
{ immediate: true },
);
// ==================== 图片上传 ====================
const photosRef = ref(null);
/**
* 供父组件调用,获取当前上传图片的哈希列表
*/
function getPhotoHashes() {
return photosRef.value?.return_files().map((f) => f.hash) ?? [];
}
defineExpose({ getPhotoHashes, costEntries });
// ==================== 表单字段双向绑定 ====================
function update(field, value) {
emit("update:modelValue", { ...props.modelValue, [field]: value });
}
</script>
<template>
<div class="space-y-4 px-6 py-5">
<!-- 标题必填 -->
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ t("purchase_addorder.part_name") }}
<span class="text-red-500">*</span>
</label>
<input
:value="modelValue.title"
@input="update('title', $event.target.value)"
type="text"
maxlength="50"
class="w-full rounded-lg border border-gray-300 bg-white px-3.5 py-2 text-sm outline-none transition-colors focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 dark:border-dk-muted dark:bg-dk-base dark:text-white"
:placeholder="t('purchase_addorder.part_name')"
/>
</div>
<!-- 备注 -->
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ t("purchase_addorder.remarks") }}
<span class="text-gray-400">{{ modelValue.remark.length }}/{{ textMaxLen }}</span>
</label>
<textarea
:value="modelValue.remark"
@input="update('remark', $event.target.value)"
class="w-full rounded-lg border border-gray-300 bg-white px-3.5 py-2 text-sm outline-none transition-colors focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 dark:border-dk-muted dark:bg-dk-base dark:text-white"
rows="4"
:placeholder="t('purchase_addorder.remarks_text')"
/>
</div>
<!-- 采购链接 -->
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ t("purchase_addorder.link") }}
</label>
<textarea
:value="modelValue.link"
@input="update('link', $event.target.value)"
class="w-full rounded-lg border border-gray-300 bg-white px-3.5 py-2 text-sm outline-none transition-colors focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 dark:border-dk-muted dark:bg-dk-base dark:text-white"
rows="2"
placeholder="url"
/>
</div>
<!-- 款式标签 -->
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ t("purchase_addorder.style_remarks") }}
</label>
<tagadder
:placeholder="t('purchase_addorder.add_style')"
:modelValue="modelValue.styles"
@update:modelValue="update('styles', $event)"
/>
</div>
<!-- 费用明细 -->
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ t("purchase_addorder.cost") }}
</label>
<!-- 已有费用列表 -->
<div v-if="costEntries.length" class="mb-4 overflow-x-auto">
<table class="w-full text-left text-sm text-gray-900">
<thead>
<tr class="border-b border-gray-200 bg-gray-50 text-gray-500 dark:border-dk-muted dark:bg-dk-base">
<th class="px-3 py-2 font-medium">{{ t("purchase_addorder.type") }}</th>
<th class="px-3 py-2 font-medium">{{ t("purchase_addorder.quantity") }}</th>
<th class="px-3 py-2 font-medium">{{ t("purchase_addorder.fee") }}</th>
<th class="px-3 py-2 font-medium">{{ t("purchase_addorder.total_price") }}</th>
<th class="px-3 py-2 font-medium">{{ t("purchase_addorder.currency") }}</th>
<th class="px-3 py-2 font-medium">{{ t("purchase_addorder.operation") }}</th>
</tr>
</thead>
<tbody>
<tr
v-for="(item, idx) in costEntries"
:key="idx"
class="border-b border-gray-100 dark:border-dk-muted"
>
<td class="px-3 py-2 font-medium text-gray-900 dark:text-white">{{ costType[item.type] }}</td>
<td class="px-3 py-2 text-gray-500">{{ item.int }}</td>
<td class="px-3 py-2 text-gray-500">{{ item.cost }}</td>
<td class="px-3 py-2 text-gray-500">{{ item.costt }}</td>
<td class="px-3 py-2 text-gray-500">{{ currencyOptions[item.currencytype] }}</td>
<td class="px-3 py-2">
<button
class="rounded px-2 py-1 text-xs font-medium text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-900/20"
@click="removeCostEntry(idx)"
>
{{ t("purchase_addorder.remove") }}
</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 添加费用表单 -->
<div class="grid grid-cols-2 gap-4 sm:grid-cols-5">
<div>
<label class="mb-1 block text-xs font-medium text-gray-500">{{ t("purchase_addorder.fee_type") }}</label>
<select
v-model="newCost.type"
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm dark:border-dk-muted dark:bg-dk-base dark:text-white"
>
<template v-for="(label, key) in costType" :key="key">
<option :value="key">{{ label }}</option>
</template>
</select>
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-500">{{ t("purchase_addorder.input_quantity") }}</label>
<input
v-model.number="newCost.int"
type="number"
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm dark:border-dk-muted dark:bg-dk-base dark:text-white"
min="1"
/>
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-500">{{ t("purchase_addorder.input_fee") }}</label>
<input
v-model="newCost.cost"
type="number"
class="w-full rounded-lg border bg-white px-3 py-2 text-sm dark:bg-dk-base dark:text-white"
:class="costError ? 'border-red-500' : 'border-gray-300 dark:border-dk-muted'"
step="0.01"
min="0"
/>
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-500">{{ t("purchase_addorder.select_currency") }}</label>
<select
v-model="newCost.currencyType"
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm dark:border-dk-muted dark:bg-dk-base dark:text-white"
>
<template v-for="(label, key) in currencyOptions" :key="key">
<option :value="key">{{ label }}</option>
</template>
</select>
</div>
<div class="flex items-end">
<button
class="w-full rounded-lg border border-gray-300 bg-blue-600 px-3 py-2 text-sm font-semibold text-blue-100 transition-colors hover:bg-blue-700 focus:ring-2 focus:ring-blue-500/20 dark:border-dk-muted dark:bg-blue-600 dark:text-white"
@click="addCostEntry"
>
{{ t("purchase_addorder.add") }}
</button>
</div>
</div>
</div>
<!-- 图片上传 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ t("purchase_addorder.photo_remarks") }}
</label>
<useDropzone
acceptFiles="image/*"
uploadURL="/api/files/upload/image"
:maxFiles="10"
:initialFiles="initialPhotos"
ref="photosRef"
/>
</div>
</div>
</template>
+5 -2
View File
@@ -99,10 +99,12 @@
"no_commits": "No status records", "no_commits": "No status records",
"commit_placeholder": "Add comment (optional)", "commit_placeholder": "Add comment (optional)",
"upload_photos": "Upload Photos", "upload_photos": "Upload Photos",
"commit_create": "Order created" "commit_create": "Order created",
"edit_order": "Edit Order"
}, },
"purchase_addorder": { "purchase_addorder": {
"add_order": "Add Order", "add_order": "Add Order",
"edit_order": "Edit Order",
"order_info": "Order Information", "order_info": "Order Information",
"title": "Title", "title": "Title",
"input_title": "Enter order title", "input_title": "Enter order title",
@@ -229,7 +231,8 @@
"old_pass_incorrect": "Old password is incorrect", "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", "save_success": "Saved successfully",
"submit": "Submit" "submit": "Submit",
"loading": "Loading..."
}, },
"settings": { "settings": {
"cancel": "Cancel", "cancel": "Cancel",
+5 -2
View File
@@ -99,10 +99,12 @@
"no_commits": "暂无状态记录", "no_commits": "暂无状态记录",
"commit_placeholder": "添加备注(可选)", "commit_placeholder": "添加备注(可选)",
"upload_photos": "上传图片", "upload_photos": "上传图片",
"commit_create": "订单创建" "commit_create": "订单创建",
"edit_order": "编辑订单"
}, },
"purchase_addorder": { "purchase_addorder": {
"add_order": "添加订单", "add_order": "添加订单",
"edit_order": "编辑订单",
"order_info": "订单信息", "order_info": "订单信息",
"title": "标题", "title": "标题",
"input_title": "输入订单标题", "input_title": "输入订单标题",
@@ -229,7 +231,8 @@
"old_pass_incorrect": "旧密码不正确", "old_pass_incorrect": "旧密码不正确",
"confirm_password_incorrect": "确认密码不正确", "confirm_password_incorrect": "确认密码不正确",
"save_success": "保存成功", "save_success": "保存成功",
"submit": "提交" "submit": "提交",
"loading": "加载中..."
}, },
"settings": { "settings": {
"cancel": "取消", "cancel": "取消",
+5
View File
@@ -49,6 +49,11 @@ const router = createRouter({
name: 'purchase-show', name: 'purchase-show',
component: () => import('@/views/purchase/ShowOrder.vue'), component: () => import('@/views/purchase/ShowOrder.vue'),
}, },
{
path: 'purchase/editorder/:id',
name: 'purchase-edit',
component: () => import('@/views/purchase/editorder.vue'),
},
{ {
path: 'warehouse', path: 'warehouse',
name: 'warehouse', name: 'warehouse',
@@ -288,8 +288,8 @@ onMounted(fetchOrder);
<template> <template>
<div class="mx-auto max-w-6xl px-6 py-6"> <div class="mx-auto max-w-6xl px-6 py-6">
<!-- 返回按钮 --> <!-- 顶部操作栏返回 + 编辑 -->
<div class="mb-4"> <div class="mb-4 flex items-center justify-between">
<RouterLink <RouterLink
to="/purchase" 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" 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"
@@ -297,6 +297,17 @@ onMounted(fetchOrder);
<IconChevronLeft :size="16" /> <IconChevronLeft :size="16" />
{{ t("purchase.back_to_list") }} {{ t("purchase.back_to_list") }}
</RouterLink> </RouterLink>
<!-- 编辑按钮 -->
<RouterLink
v-if="order"
:to="`/purchase/editorder/${order.ID}`"
class="inline-flex items-center gap-1.5 rounded-lg border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 dark:border-dk-muted dark:bg-dk-card dark:text-gray-300 dark:hover:bg-dk-base"
>
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
{{ t("purchase.edit_order") }}
</RouterLink>
</div> </div>
<!-- Loading --> <!-- Loading -->
@@ -538,7 +549,7 @@ onMounted(fetchOrder);
<td <td
class="px-6 py-3 text-base font-bold text-gray-900 dark:text-white" class="px-6 py-3 text-base font-bold text-gray-900 dark:text-white"
> >
{{ costTotalYuan.toFixed(2) }} <!-- {{ costTotalYuan.toFixed(2) }} -->
</td> </td>
<td class="px-6 py-3"> <td class="px-6 py-3">
<span <span
@@ -113,10 +113,10 @@ const costEntries = reactive([]);
* 用户填写完表单后点击"添加"按钮加入 costEntries * 用户填写完表单后点击"添加"按钮加入 costEntries
*/ */
const newCost = reactive({ const newCost = reactive({
type: 1, // 费用类型:默认"单价" type: "1", // 费用类型:默认"单价"
int: 1, // 数量:默认1 int: 1, // 数量:默认1
cost: 0, // 单价:默认0 cost: 0, // 单价:默认0
currencyType: 1, // 货币类型:默认人民币 currencyType: "1", // 货币类型:默认人民币
}); });
// 费用验证错误状态:点击添加按钮后发现费用为0时触发 // 费用验证错误状态:点击添加按钮后发现费用为0时触发
@@ -166,10 +166,10 @@ function addCostEntry() {
currencytype: newCost.currencyType, currencytype: newCost.currencyType,
}); });
// 添加后重置表单,以便继续添加下一条 // 添加后重置表单,以便继续添加下一条
newCost.type = 1; newCost.type = "1";
newCost.int = 1; newCost.int = 1;
newCost.cost = 0; newCost.cost = 0;
newCost.currencyType = 1; newCost.currencyType = "1";
} }
/** /**
@@ -230,6 +230,10 @@ async function handleSubmit() {
...h, ...h,
cost: Math.round(h.cost * 100), cost: Math.round(h.cost * 100),
costt: Math.round(h.costt * 100), costt: Math.round(h.costt * 100),
//转换type的类型,string转int
type:parseInt(h.type),
currencytype:parseInt(h.currencytype),
})); }));
// 开始 loading // 开始 loading
@@ -0,0 +1,195 @@
<script setup>
/**
* 采购订单编辑页面
*
* 功能概述:
* - 通过路由参数 :id 加载已有订单数据
* - 使用 PurchaseOrderForm 组件展示可编辑表单
* - 提交时调用 /purchase/updateorder 保存修改
*/
import { reactive, ref, onMounted } 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";
usePageTitle("purchase_addorder.edit_order");
const route = useRoute();
const router = useRouter();
const { t } = useI18n();
const toast = useToastStore();
const { validate, errors, clearErrors } = useValidation();
const orderId = Number(route.params.id);
// ==================== 状态 ====================
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);
// ==================== 加载订单数据 ====================
onMounted(async () => {
if (!orderId) {
pageError.value = t("purchase.order_not_found");
pageLoading.value = false;
return;
}
try {
const res = await purchaseApi.getOrder(orderId);
console.log(res)
if (res.errCode !== 0 || res.raw?.err_code !== 0) {
pageError.value = t("purchase.order_not_found");
pageLoading.value = false;
return;
}
const { order, costs, photos } = res.raw.data;
// 回填基本信息
form.title = order.Title ?? "";
form.remark = order.Remark ?? "";
form.link = order.Link ?? "";
form.styles = order.Styles ?? "";
// 回填费用(传给子组件,由子组件转换为元展示)
initialCosts.value = costs ?? [];
// 回填图片
initialPhotos.value = photos ?? [];
} catch {
pageError.value = t("purchase.order_not_found");
} finally {
pageLoading.value = false;
}
});
// ==================== 提交 ====================
async function handleSubmit() {
clearErrors();
const ok = validate("title", form.title, t("purchase_addorder.title"));
if (!ok) return;
// 获取图片哈希
form.photos = formRef.value?.getPhotoHashes() ?? [];
// 使用子组件同步的费用(分为单位)
form.costs = form._costs ?? [];
loading.value = true;
try {
const res = await purchaseApi.updateOrder(orderId, {
title: form.title,
remark: form.remark,
link: form.link,
styles: form.styles,
photos: form.photos,
costs: form.costs,
});
if (res.errCode === 0 && res.raw?.err_code === 0) {
toast.success(t("message.save_ok"));
setTimeout(() => {
router.replace(`/purchase/showorder/${orderId}`);
}, 800);
} else {
toast.error(t("message.server_error"));
}
} catch {
toast.error(t("message.server_error"));
} finally {
loading.value = false;
}
}
</script>
<template>
<div class="mx-auto max-w-6xl px-6 py-6">
<!-- 加载中 -->
<div v-if="pageLoading" class="flex items-center justify-center py-20 text-gray-400">
<svg class="mr-2 h-5 w-5 animate-spin" 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>
{{ t("message.loading") }}
</div>
<!-- 订单不存在 -->
<div v-else-if="pageError" class="rounded-xl border border-red-200 bg-red-50 px-6 py-10 text-center text-red-500 dark:border-red-800 dark:bg-red-900/20">
{{ pageError }}
</div>
<!-- 主卡片 -->
<div
v-else
class="flex flex-col gap-0 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-200 px-6 py-4 dark:border-dk-muted">
<h4 class="text-sm font-semibold text-gray-900 dark:text-white">
{{ t("purchase_addorder.edit_order") }}
</h4>
<!-- 返回按钮 -->
<button
class="flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-dk-base"
@click="router.back()"
>
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7" />
</svg>
{{ t("purchase.back_to_list") }}
</button>
</div>
<!-- 错误提示字段验证 -->
<div v-if="errors.title" class="mx-6 mt-4 rounded-lg bg-red-50 px-4 py-2 text-sm text-red-600 dark:bg-red-900/20 dark:text-red-400">
{{ errors.title }}
</div>
<!-- 表单主体公共组件 -->
<PurchaseOrderForm
v-model="form"
:initialCosts="initialCosts"
:initialPhotos="initialPhotos"
ref="formRef"
/>
<!-- 底部操作栏 -->
<div class="flex justify-end border-t border-gray-200 px-6 py-4 dark:border-dk-muted">
<button
class="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white transition-colors hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500/20 disabled:opacity-60"
:disabled="loading"
@click="handleSubmit"
>
<svg v-if="loading" class="h-4 w-4 animate-spin text-white" 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>
{{ t("message.save_ok") }}
</button>
</div>
</div>
</div>
</template>