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)
|
//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 = []) {
|
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>
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": "取消",
|
||||||
|
|||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user