up
This commit is contained in:
@@ -13,5 +13,5 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"lastUpdated": 1776135998489
|
||||
"lastUpdated": 1776136244169
|
||||
}
|
||||
@@ -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`(中英双语)
|
||||
@@ -502,4 +502,113 @@ func ApiPurchase(r *gin.RouterGroup) {
|
||||
//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)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@@ -20,4 +20,9 @@ export const purchaseApi = {
|
||||
updateOrderStatus(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>
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"week": {
|
||||
"week": {
|
||||
"sun": "Sunday",
|
||||
"mon": "Monday",
|
||||
"tue": "Tuesday",
|
||||
@@ -99,10 +99,12 @@
|
||||
"no_commits": "No status records",
|
||||
"commit_placeholder": "Add comment (optional)",
|
||||
"upload_photos": "Upload Photos",
|
||||
"commit_create": "Order created"
|
||||
"commit_create": "Order created",
|
||||
"edit_order": "Edit Order"
|
||||
},
|
||||
"purchase_addorder": {
|
||||
"add_order": "Add Order",
|
||||
"edit_order": "Edit Order",
|
||||
"order_info": "Order Information",
|
||||
"title": "Title",
|
||||
"input_title": "Enter order title",
|
||||
@@ -229,7 +231,8 @@
|
||||
"old_pass_incorrect": "Old password is incorrect",
|
||||
"confirm_password_incorrect": "Confirm password is incorrect",
|
||||
"save_success": "Saved successfully",
|
||||
"submit": "Submit"
|
||||
"submit": "Submit",
|
||||
"loading": "Loading..."
|
||||
},
|
||||
"settings": {
|
||||
"cancel": "Cancel",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"week": {
|
||||
"week": {
|
||||
"sun": "星期日",
|
||||
"mon": "星期一",
|
||||
"tue": "星期二",
|
||||
@@ -99,10 +99,12 @@
|
||||
"no_commits": "暂无状态记录",
|
||||
"commit_placeholder": "添加备注(可选)",
|
||||
"upload_photos": "上传图片",
|
||||
"commit_create": "订单创建"
|
||||
"commit_create": "订单创建",
|
||||
"edit_order": "编辑订单"
|
||||
},
|
||||
"purchase_addorder": {
|
||||
"add_order": "添加订单",
|
||||
"edit_order": "编辑订单",
|
||||
"order_info": "订单信息",
|
||||
"title": "标题",
|
||||
"input_title": "输入订单标题",
|
||||
@@ -229,7 +231,8 @@
|
||||
"old_pass_incorrect": "旧密码不正确",
|
||||
"confirm_password_incorrect": "确认密码不正确",
|
||||
"save_success": "保存成功",
|
||||
"submit": "提交"
|
||||
"submit": "提交",
|
||||
"loading": "加载中..."
|
||||
},
|
||||
"settings": {
|
||||
"cancel": "取消",
|
||||
|
||||
@@ -49,6 +49,11 @@ const router = createRouter({
|
||||
name: 'purchase-show',
|
||||
component: () => import('@/views/purchase/ShowOrder.vue'),
|
||||
},
|
||||
{
|
||||
path: 'purchase/editorder/:id',
|
||||
name: 'purchase-edit',
|
||||
component: () => import('@/views/purchase/editorder.vue'),
|
||||
},
|
||||
{
|
||||
path: 'warehouse',
|
||||
name: 'warehouse',
|
||||
|
||||
@@ -288,8 +288,8 @@ onMounted(fetchOrder);
|
||||
<template>
|
||||
|
||||
<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
|
||||
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"
|
||||
@@ -297,6 +297,17 @@ onMounted(fetchOrder);
|
||||
<IconChevronLeft :size="16" />
|
||||
{{ t("purchase.back_to_list") }}
|
||||
</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>
|
||||
|
||||
<!-- Loading -->
|
||||
@@ -538,7 +549,7 @@ onMounted(fetchOrder);
|
||||
<td
|
||||
class="px-6 py-3 text-base font-bold text-gray-900 dark:text-white"
|
||||
>
|
||||
{{ costTotalYuan.toFixed(2) }}
|
||||
<!-- {{ costTotalYuan.toFixed(2) }} -->
|
||||
</td>
|
||||
<td class="px-6 py-3">
|
||||
<span
|
||||
|
||||
@@ -113,10 +113,10 @@ const costEntries = reactive([]);
|
||||
* 用户填写完表单后点击"添加"按钮加入 costEntries
|
||||
*/
|
||||
const newCost = reactive({
|
||||
type: 1, // 费用类型:默认"单价"
|
||||
type: "1", // 费用类型:默认"单价"
|
||||
int: 1, // 数量:默认1
|
||||
cost: 0, // 单价:默认0
|
||||
currencyType: 1, // 货币类型:默认人民币
|
||||
currencyType: "1", // 货币类型:默认人民币
|
||||
});
|
||||
|
||||
// 费用验证错误状态:点击添加按钮后发现费用为0时触发
|
||||
@@ -166,10 +166,10 @@ function addCostEntry() {
|
||||
currencytype: newCost.currencyType,
|
||||
});
|
||||
// 添加后重置表单,以便继续添加下一条
|
||||
newCost.type = 1;
|
||||
newCost.type = "1";
|
||||
newCost.int = 1;
|
||||
newCost.cost = 0;
|
||||
newCost.currencyType = 1;
|
||||
newCost.currencyType = "1";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -230,6 +230,10 @@ async function handleSubmit() {
|
||||
...h,
|
||||
cost: Math.round(h.cost * 100),
|
||||
costt: Math.round(h.costt * 100),
|
||||
|
||||
//转换type的类型,string转int
|
||||
type:parseInt(h.type),
|
||||
currencytype:parseInt(h.currencytype),
|
||||
}));
|
||||
|
||||
// 开始 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>
|
||||
Reference in New Issue
Block a user