feat(freight): shop freight templates, server-side checkout fees, company dictionary (add-freight-templates)

This commit is contained in:
Chengdong Zhang
2026-09-24 15:03:51 +08:00
parent 5b426486ac
commit 473d19d089
44 changed files with 2409 additions and 67 deletions
+41 -4
View File
@@ -1,6 +1,12 @@
<script setup lang="ts">
import { t as localized } from "@vmall/shared";
import type { Order, OrderStatus, Shipment, ShipmentItemBody } from "@vmall/shared";
import type {
Order,
OrderStatus,
Shipment,
ShipmentItemBody,
ShippingCompany,
} from "@vmall/shared";
definePageMeta({ middleware: "auth" });
@@ -11,9 +17,11 @@ const route = useRoute();
const orderId = computed(() => String(route.params.id));
const order = ref<Order | null>(null);
const shipments = ref<Shipment[]>([]);
const companies = ref<ShippingCompany[]>([]);
const quantities = ref<Record<string, number>>({});
const carrier = ref("");
const trackingNo = ref("");
const shippingCompanyCode = ref("");
const loading = ref(true);
const busy = ref(false);
const actionId = ref("");
@@ -34,10 +42,16 @@ function formatDate(value: string): string {
return new Date(value).toLocaleString(locale.value === "zh" ? "zh-CN" : "en-US");
}
function companyName(code: string | null | undefined): string {
if (!code) return "—";
const company = companies.value.find((item) => item.code === code);
return company ? localized(company.name, locale.value) : code;
}
function shippedQuantity(itemId: string): number {
return shipments.value
.filter((shipment) => shipment.order_id === orderId.value)
.flatMap((shipment) => shipment.items)
.flatMap((shipment) => shipment.items ?? [])
.filter((item) => item.order_item_id === itemId)
.reduce((total, item) => total + item.qty, 0);
}
@@ -63,12 +77,14 @@ async function loadOrder(): Promise<void> {
loading.value = true;
error.value = "";
try {
const [loadedOrder, loadedShipments] = await Promise.all([
const [loadedOrder, loadedShipments, loadedCompanies] = await Promise.all([
findShopOrder(),
$api.shop.listShipments(),
$api.listShippingCompanies(),
]);
order.value = loadedOrder;
shipments.value = loadedShipments.filter((shipment) => shipment.order_id === orderId.value);
companies.value = loadedCompanies;
const nextQuantities: Record<string, number> = {};
loadedOrder.items.forEach((item) => {
nextQuantities[item.id] = Math.max(
@@ -76,7 +92,7 @@ async function loadOrder(): Promise<void> {
item.qty -
loadedShipments
.filter((shipment) => shipment.order_id === orderId.value)
.flatMap((shipment) => shipment.items)
.flatMap((shipment) => shipment.items ?? [])
.filter((shipmentItem) => shipmentItem.order_item_id === item.id)
.reduce((total, shipmentItem) => total + shipmentItem.qty, 0),
);
@@ -94,6 +110,10 @@ async function createShipment(): Promise<void> {
error.value = translate("common.required");
return;
}
if (!shippingCompanyCode.value) {
error.value = translate("shop.shippingCompanyRequired");
return;
}
const items: ShipmentItemBody[] = order.value.items
.map((item) => ({ order_item_id: item.id, qty: quantities.value[item.id] ?? 0 }))
.filter((item) => item.qty > 0);
@@ -109,9 +129,11 @@ async function createShipment(): Promise<void> {
carrier.value.trim(),
trackingNo.value.trim(),
items,
shippingCompanyCode.value,
);
carrier.value = "";
trackingNo.value = "";
shippingCompanyCode.value = "";
await loadOrder();
} catch (err: unknown) {
error.value = err instanceof Error ? err.message : translate("common.error");
@@ -208,6 +230,19 @@ onMounted(() => {
<VField :label="$t('shop.trackingNo')">
<VInput id="tracking-no" v-model="trackingNo" required />
</VField>
<VField :label="$t('shop.shippingCompany')">
<select
id="shipping-company"
v-model="shippingCompanyCode"
required
class="border-border bg-surface text-text focus:border-primary focus:outline-primary/30 w-full rounded-md border px-3 py-2 text-sm focus:outline-2"
>
<option value="" disabled>{{ $t("shop.selectShippingCompany") }}</option>
<option v-for="company in companies" :key="company.code" :value="company.code">
{{ localized(company.name, locale) }}
</option>
</select>
</VField>
</div>
<h3 class="mb-3 text-base font-semibold">{{ $t("shop.shipmentQuantities") }}</h3>
<div
@@ -246,6 +281,7 @@ onMounted(() => {
<tr>
<th>{{ $t("shipment.shipmentNo") }}</th>
<th>{{ $t("shop.carrier") }}</th>
<th>{{ $t("shop.shippingCompany") }}</th>
<th>{{ $t("shop.trackingNo") }}</th>
<th>{{ $t("common.status") }}</th>
<th>{{ $t("common.actions") }}</th>
@@ -255,6 +291,7 @@ onMounted(() => {
<tr v-for="shipment in shipments" :key="shipment.id">
<td>{{ shipment.shipment_no }}</td>
<td>{{ shipment.carrier }}</td>
<td>{{ companyName(shipment.shipping_company_code) }}</td>
<td>{{ shipment.tracking_no }}</td>
<td>
<VBadge :tone="shipmentStatusClass(shipment.status)">{{