This commit is contained in:
2026-04-14 13:37:39 +08:00
parent c4dd44ae89
commit 2334588b26
11 changed files with 342 additions and 85 deletions
+1 -1
View File
@@ -13,5 +13,5 @@
} }
] ]
}, },
"lastUpdated": 1776140518763 "lastUpdated": 1776144770632
} }
+1
View File
@@ -67,6 +67,7 @@ func main() {
//统一初始化 //统一初始化
models.ConfigAllInit() models.ConfigAllInit()
routers.ApiUserInit() //用户表先初始化这是必须的因为后面需要用到用户组 routers.ApiUserInit() //用户表先初始化这是必须的因为后面需要用到用户组
routers.ApiFilesInit()
routers.ApiScheduleInit() routers.ApiScheduleInit()
routers.ApiPurchaseInit() routers.ApiPurchaseInit()
-15
View File
@@ -12,19 +12,6 @@ import (
var DB *gorm.DB 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 { type TabUser_ struct {
ID uint `gorm:"primaryKey;autoIncrement"` // 自增主键 ID uint `gorm:"primaryKey;autoIncrement"` // 自增主键
Name string `gorm:"size:100;uniqueIndex"` // 唯一约束索引 Name string `gorm:"size:100;uniqueIndex"` // 唯一约束索引
@@ -126,8 +113,6 @@ func DatabaseInit() error {
DB.AutoMigrate(&TabCookie_{}) DB.AutoMigrate(&TabCookie_{})
DB.AutoMigrate(&TabFileInfo_{})
DB.AutoMigrate(&APIRequestLog_{}) DB.AutoMigrate(&APIRequestLog_{})
return nil return nil
+3 -3
View File
@@ -139,7 +139,7 @@ func ApiPurchase(r *gin.RouterGroup) {
for _, b := range binds { for _, b := range binds {
fileIDs = append(fileIDs, b.FileID) fileIDs = append(fileIDs, b.FileID)
} }
var files []models.TabFileInfo_ var files []TabFileInfo_
if len(fileIDs) > 0 { if len(fileIDs) > 0 {
models.DB.Where("id IN ?", fileIDs).Find(&files) 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++ { for i := 0; i < len(jsondata.Photos); i++ {
findFile := models.TabFileInfo_{ findFile := TabFileInfo_{
Sha256: jsondata.Photos[i], Sha256: jsondata.Photos[i],
Type: "image", Type: "image",
} }
@@ -588,7 +588,7 @@ func ApiPurchase(r *gin.RouterGroup) {
// 重建图片绑定:先删旧,再插新 // 重建图片绑定:先删旧,再插新
models.DB.Where("order_id = ?", from.ID).Delete(&TabPurchaseFileBind{}) models.DB.Where("order_id = ?", from.ID).Delete(&TabPurchaseFileBind{})
for _, hash := range from.Photos { 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 { if models.DB.Where(&findFile).First(&findFile).Error == nil {
models.DB.Create(&TabPurchaseFileBind{ models.DB.Create(&TabPurchaseFileBind{
OrderID: from.ID, OrderID: from.ID,
+22 -3
View File
@@ -6,14 +6,33 @@ import (
"ops/models" "ops/models"
"path" "path"
"path/filepath" "path/filepath"
"time"
"github.com/gin-gonic/gin" "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 file_save() {
} }
func ApiFilesInit() {
models.DB.AutoMigrate(&TabFileInfo_{})
}
func ApiFiles(r *gin.RouterGroup) { func ApiFiles(r *gin.RouterGroup) {
//getfile := r.Group("/get") //定义上传组 //getfile := r.Group("/get") //定义上传组
@@ -35,7 +54,7 @@ func ApiFiles(r *gin.RouterGroup) {
download = false download = false
} }
if isPartOK { if isPartOK {
file_info := models.TabFileInfo_{ file_info := TabFileInfo_{
Sha256: hash, Sha256: hash,
} }
if models.DB.Where(&file_info).First(&file_info).Error == nil { 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, Name: filename,
Sha256: hash_str, Sha256: hash_str,
Mime: mimeType, Mime: mimeType,
Type: "image", Type: "image",
UserID: user.ID, UserID: user.ID,
} }
fund_file_info2 := models.TabFileInfo_{} fund_file_info2 := TabFileInfo_{}
models.DB.Where(&fund_file_info).Find(&fund_file_info2) models.DB.Where(&fund_file_info).Find(&fund_file_info2)
+1 -2
View File
@@ -3,7 +3,6 @@ package routers
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"ops/models"
"github.com/gin-gonic/gin" "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 { if preview {
ctx.File(file_info.Path) ctx.File(file_info.Path)
} else { } else {
@@ -25,12 +25,7 @@ const props = defineProps({
type: Object, type: Object,
required: true, required: true,
}, },
/** 回填费用列表(分为单位) */ /** 回填图片列表 [{ Sha256, Name, ... }] */
initialCosts: {
type: Array,
default: () => [],
},
/** 回填图片列表 */
initialPhotos: { initialPhotos: {
type: Array, type: Array,
default: () => [], default: () => [],
@@ -94,12 +89,12 @@ function removeCostEntry(index) {
/** 将当前 costEntries(元)转换为分并同步到父组件 */ /** 将当前 costEntries(元)转换为分并同步到父组件 */
function syncCosts() { function syncCosts() {
const converted = costEntries.map((h) => ({ // 直接更新父组件的 form._costs,跳过 emit 链路
props.modelValue._costs = costEntries.map((h) => ({
...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),
})); }));
emit("update:modelValue", { ...props.modelValue, _costs: converted });
} }
watch( watch(
@@ -111,25 +106,24 @@ watch(
}, },
); );
// 回填费用(分→元) // ==================== 外部初始化接口 ====================
watch( /**
() => props.initialCosts, * 由父组件调用,用于回填已有费用数据(来自 API)
(list) => { * @param {Array} list 费用数组,单位:分
*/
function initCostEntries(list) {
if (!list || list.length === 0) return; if (!list || list.length === 0) return;
costEntries.splice(0, costEntries.length); costEntries.splice(0, costEntries.length);
list.forEach((c) => { list.forEach((c) => {
costEntries.push({ costEntries.push({
type: c.costType, type: c.type ?? c.CostType ?? 1,
int: c.quantity, int: c.int ?? c.Quantity ?? 1,
cost: parseFloat((c.price / 100).toFixed(2)), cost: parseFloat(((c.cost ?? c.Price) / 100).toFixed(2)),
costt: parseFloat(((c.price * c.quantity) / 100).toFixed(2)), costt: parseFloat(((c.costt ?? c.Price * (c.int ?? c.Quantity)) / 100).toFixed(2)),
currencytype: c.currencyType, currencytype: c.currencytype ?? c.CurrencyType ?? 1,
}); });
}); });
syncCosts(); }
},
{ immediate: true },
);
// ==================== 图片上传 ==================== // ==================== 图片上传 ====================
const photosRef = ref(null); const photosRef = ref(null);
@@ -141,7 +135,7 @@ function getPhotoHashes() {
return photosRef.value?.return_files().map((f) => f.hash) ?? []; return photosRef.value?.return_files().map((f) => f.hash) ?? [];
} }
defineExpose({ getPhotoHashes, costEntries }); defineExpose({ getPhotoHashes, costEntries, initCostEntries });
// ==================== 表单字段双向绑定 ==================== // ==================== 表单字段双向绑定 ====================
function update(field, value) { function update(field, value) {
@@ -53,6 +53,11 @@ const prop = defineProps({
type: String, type: String,
default: "/api/files/upload", default: "/api/files/upload",
}, },
/** 初始已有文件 [{ hash, name, ... }] */
initialFiles: {
type: Array,
default: () => [],
},
}); });
// 初始化 Dropzone // 初始化 Dropzone
@@ -201,11 +206,43 @@ function return_files() {
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(() => { onMounted(() => {
initDropzone(); initDropzone();
loadInitialFiles();
//console.log(lightbox)
}); });
// 组件卸载时销毁 // 组件卸载时销毁
+2
View File
@@ -129,6 +129,8 @@
"input_fee": "Fee", "input_fee": "Fee",
"select_currency": "Select currency", "select_currency": "Select currency",
"add": "Add", "add": "Add",
"add_cost": "Add Cost",
"upload_photos": "Upload Photos",
"other_status": "Other Status", "other_status": "Other Status",
"update_time": "Update Time", "update_time": "Update Time",
"tracking_number": "Tracking Number", "tracking_number": "Tracking Number",
+2
View File
@@ -129,6 +129,8 @@
"input_fee": "费用", "input_fee": "费用",
"select_currency": "选择货币类型", "select_currency": "选择货币类型",
"add": "添加", "add": "添加",
"add_cost": "添加费用",
"upload_photos": "上传图片",
"other_status": "其他状态", "other_status": "其他状态",
"update_time": "更新时间", "update_time": "更新时间",
"tracking_number": "快递单号", "tracking_number": "快递单号",
@@ -4,18 +4,19 @@
* *
* 功能概述: * 功能概述:
* - 通过路由参数 :id 加载已有订单数据 * - 通过路由参数 :id 加载已有订单数据
* - 使用 PurchaseOrderForm 组件展示可编辑表单 * - 费用明细直接在本页管理(与 addorder.vue 相同模式)
* - 提交时调用 /purchase/updateorder 保存修改 * - 提交时调用 /purchase/updateorder 保存修改
*/ */
import { reactive, ref, onMounted } from "vue"; import { reactive, ref, computed, watch, onMounted, nextTick } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { useToastStore } from "@/stores/toast"; import { useToastStore } from "@/stores/toast";
import { usePageTitle } from "@/composables/usePageTitle"; import { usePageTitle } from "@/composables/usePageTitle";
import { useValidation } from "@/composables"; import { useValidation } from "@/composables";
import { purchaseApi } from "@/api/purchase"; 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"); usePageTitle("purchase_addorder.edit_order");
@@ -32,24 +33,73 @@ const loading = ref(false);
const pageLoading = ref(true); const pageLoading = ref(true);
const pageError = ref(""); const pageError = ref("");
/** 回填的费用明细(分为单位,传给 PurchaseOrderForm */ // ==================== 表单数据 ====================
const initialCosts = ref([]);
/** 回填的图片列表 */
const initialPhotos = ref([]);
/** 表单数据 */
const form = reactive({ const form = reactive({
title: "", title: "",
remark: "", remark: "",
link: "", link: "",
styles: "", styles: "",
photos: [], photos: [],
costs: [],
_costs: [], // 由 PurchaseOrderForm 组件同步的分为单位费用数组
}); });
/** PurchaseOrderForm 组件引用(用于获取图片哈希) */ // ==================== 费用明细(与 addorder.vue 完全一致) ====================
const formRef = ref(null); 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 () => { onMounted(async () => {
@@ -61,14 +111,13 @@ onMounted(async () => {
try { try {
const res = await purchaseApi.getOrder(orderId); const res = await purchaseApi.getOrder(orderId);
console.log(res) if (res.errCode !== 0 || !res.data) {
if (res.errCode !== 0 || res.raw?.err_code !== 0) {
pageError.value = t("purchase.order_not_found"); pageError.value = t("purchase.order_not_found");
pageLoading.value = false; pageLoading.value = false;
return; return;
} }
const { order, costs, photos } = res.raw.data; const { order, costs, photos } = res.data;
// 回填基本信息 // 回填基本信息
form.title = order.Title ?? ""; form.title = order.Title ?? "";
@@ -76,10 +125,24 @@ onMounted(async () => {
form.link = order.Link ?? ""; form.link = order.Link ?? "";
form.styles = order.Styles ?? ""; form.styles = order.Styles ?? "";
// 回填费用(传给子组件,由子组件转换为元展示 // 回填费用(分→元,直接写 costEntries
initialCosts.value = costs ?? []; 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 { } catch {
pageError.value = t("purchase.order_not_found"); pageError.value = t("purchase.order_not_found");
} finally { } finally {
@@ -93,10 +156,15 @@ async function handleSubmit() {
const ok = validate("title", form.title, t("purchase_addorder.title")); const ok = validate("title", form.title, t("purchase_addorder.title"));
if (!ok) return; if (!ok) return;
// 获取图片哈希 form.photos = getPhotoHashes();
form.photos = formRef.value?.getPhotoHashes() ?? []; // 费用(转为分)
// 使用子组件同步的费用(分为单位) const rawCosts = costEntries.map((h) => ({
form.costs = form._costs ?? []; type: h.type,
int: h.int,
cost: Math.round(h.cost * 100),
costt: Math.round(h.costt * 100),
currencytype: h.currencytype,
}));
loading.value = true; loading.value = true;
try { try {
@@ -106,10 +174,10 @@ async function handleSubmit() {
link: form.link, link: form.link,
styles: form.styles, styles: form.styles,
photos: form.photos, 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")); toast.success(t("message.save_ok"));
setTimeout(() => { setTimeout(() => {
router.replace(`/purchase/showorder/${orderId}`); router.replace(`/purchase/showorder/${orderId}`);
@@ -168,13 +236,163 @@ async function handleSubmit() {
{{ errors.title }} {{ errors.title }}
</div> </div>
<!-- 表单主体公共组件 --> <!-- ==================== 订单信息区块 ==================== -->
<PurchaseOrderForm <div class="border-b border-gray-200 px-6 py-4 dark:border-dk-muted">
v-model="form" <h4 class="text-sm font-semibold text-gray-900 dark:text-white">
:initialCosts="initialCosts" {{ t("purchase_addorder.order_info") }}
:initialPhotos="initialPhotos" </h4>
ref="formRef" </div>
<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
v-model="form.title"
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"
:class="errors.title ? 'border-red-500' : 'border-gray-300'"
:placeholder="t('purchase_addorder.part_name')"
/> />
<span v-if="errors.title" class="mt-1 block text-xs text-red-500">{{ errors.title }}</span>
</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">{{ form.remark.length }}/{{ textMaxLen }}</span>
</label>
<textarea
v-model="form.remark"
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
v-model="form.link"
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"
/>
</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')" v-model="form.styles" />
</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"
>
<option v-for="(label, key) in costType" :key="key" :value="Number(key)">{{ label }}</option>
</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"
>
<option v-for="(label, key) in currencyOptions" :key="key" :value="Number(key)">{{ label }}</option>
</select>
</div>
<div class="flex items-end">
<button
class="w-full rounded-lg border border-gray-300 bg-gray-50 px-4 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-100 dark:border-dk-muted dark:bg-dk-base dark:text-gray-200 dark:hover:bg-dk-muted"
@click="addCostEntry"
>
{{ t("purchase_addorder.add_cost") }}
</button>
</div>
</div>
</div>
<!-- ==================== 图片上传 ==================== -->
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">{{ t("purchase_addorder.upload_photos") }}</label>
<useDropzone ref="dropzoneRef" :initialFiles="[]" />
</div>
</div>
<!-- 底部操作栏 --> <!-- 底部操作栏 -->
<div class="flex justify-end border-t border-gray-200 px-6 py-4 dark:border-dk-muted"> <div class="flex justify-end border-t border-gray-200 px-6 py-4 dark:border-dk-muted">