diff --git a/src/pages/fulfillment/OrderFulfillmentCreate.vue b/src/pages/fulfillment/OrderFulfillmentCreate.vue index fc6e6a9..3c19dc8 100644 --- a/src/pages/fulfillment/OrderFulfillmentCreate.vue +++ b/src/pages/fulfillment/OrderFulfillmentCreate.vue @@ -88,9 +88,12 @@ async function loadData(): Promise { } function getItemTitle(item: SourceItem): string { return `${item.productNameSnapshot || "未命名产品"} · ${item.unitSnapshot || "未设置单位"}`; } function getItemDescription(item: SourceItem): string { - return isPackage.value - ? `可履约 ${getPackageQuantityText((item as CustomerPackageItem).availableQuantity)}` - : `待履约 ${getPackageQuantityText((item as SalesOrderItem).reservedQuantity)}`; + if (isPackage.value) { + return `本次可履约 ${getPackageQuantityText((item as CustomerPackageItem).availableQuantity)}`; + } + const orderItem = item as SalesOrderItem; + const availableQuantity = Math.max(0, Number(orderItem.quantity || 0) - Number(orderItem.reservedQuantity || 0) - Number(orderItem.fulfilledQuantity || 0)); + return `本次可履约 ${getPackageQuantityText(availableQuantity.toFixed(3))}`; } function confirmTime(value: string | number): void { deliveredAtText.value = String(value); pickerVisible.value = false; } function resolveTimestamp(value: string): number | undefined { diff --git a/src/pages/salesOrder/SalesOrderDetail.vue b/src/pages/salesOrder/SalesOrderDetail.vue index 2c8b585..97ce8a6 100644 --- a/src/pages/salesOrder/SalesOrderDetail.vue +++ b/src/pages/salesOrder/SalesOrderDetail.vue @@ -28,7 +28,7 @@ v-for="item in order.itemList" :key="item.id" :title="item.productNameSnapshot || '未命名产品'" - :description="`${item.quantity || 0} ${item.unitSnapshot || ''} · 已履约 ${item.fulfilledQuantity || 0} · 待履约 ${item.reservedQuantity || 0}`" + :description="getItemDescription(item)" :note="formatPackageAmount(item.lineAmount)" /> @@ -55,8 +55,8 @@ import SalesOrderAPI from "@/api/SalesOrderAPI"; import CustomerProfileCard from "@/components/CustomerProfileCard.vue"; import SalesOrderFinancePanel from "@/pages/salesOrder/components/SalesOrderFinancePanel.vue"; import { useNavBarStore } from "@/store/navBarStore"; -import type { SalesOrder } from "@/types/SalesOrder"; -import { formatPackageAmount } from "@/utils/package"; +import type { SalesOrder, SalesOrderItem } from "@/types/SalesOrder"; +import { formatPackageAmount, getPackageQuantityText } from "@/utils/package"; import { getErrorMessage } from "@/utils/error"; import { getSalesOrderStatusText, getSalesOrderTypeText, getStatusTheme } from "@/utils/salesOrder"; import { Message } from "@/utils/message"; @@ -75,6 +75,12 @@ const canReceive = computed(() => !!order.value && order.value.status !== "CANCE const canRefund = computed(() => !!order.value && order.value.status === "CANCELLED" && order.value.refundStatus === "PENDING" && userStore.hasPermissions(permissionCodes.fund.refund)); const canConfirmNoRefund = computed(() => !!order.value && order.value.status === "CANCELLED" && order.value.refundStatus === "PENDING" && userStore.hasPermissions(permissionCodes.fund.refund)); const canCancel = computed(() => !!order.value && order.value.status === "ACTIVE" && userStore.hasPermissions(permissionCodes.salesOrder.cancel)); +function getItemDescription(item: SalesOrderItem): string { + const quantity = Number(item.quantity || 0); + const fulfilledQuantity = Number(item.fulfilledQuantity || 0); + const remainingQuantity = Math.max(0, quantity - fulfilledQuantity); + return `${getPackageQuantityText(item.quantity)} ${item.unitSnapshot || ""} · 已履约 ${getPackageQuantityText(item.fulfilledQuantity)} · 剩余待履约 ${getPackageQuantityText(remainingQuantity.toFixed(3))}`; +} async function loadData(): Promise { const id = String(route.params.id || ""); diff --git a/src/pages/salesOrder/SalesOrderList.vue b/src/pages/salesOrder/SalesOrderList.vue index a2835da..32765a5 100644 --- a/src/pages/salesOrder/SalesOrderList.vue +++ b/src/pages/salesOrder/SalesOrderList.vue @@ -24,6 +24,7 @@ - - + - - - - - + @receive="toPayment(item)" + @fulfill="toFulfillment(item)" + /> +
@@ -71,11 +62,10 @@ import { Toolkit } from "timi-web"; import { useUserStore } from "timi-web-api"; import SalesOrderAPI from "@/api/SalesOrderAPI"; import NavBarActionMenu from "@/components/NavBarActionMenu.vue"; +import SalesOrderListCell from "@/pages/salesOrder/components/SalesOrderListCell.vue"; import { useNavBarStore } from "@/store/navBarStore"; import type { SalesOrder, SalesOrderStatus, SalesOrderType, SalesPaymentStatus } from "@/types/SalesOrder"; import { getErrorMessage } from "@/utils/error"; -import { formatPackageAmount } from "@/utils/package"; -import { getSalesOrderStatusText, getSalesOrderTypeText, getSalesPaymentStatusText, getStatusTheme } from "@/utils/salesOrder"; import { Message } from "@/utils/message"; import { permissionCodes } from "@/utils/permission"; @@ -85,7 +75,15 @@ const router = useRouter(); const navBarStore = useNavBarStore(); const userStore = useUserStore(); const canCreate = computed(() => userStore.hasPermissions(permissionCodes.salesOrder.create)); -const actions = computed(() => canCreate.value ? [{ key: "create", text: "直接产品下单", icon: "add", theme: "primary" as const, action: toCreate }] : []); +const canReceive = computed(() => userStore.hasPermissions(permissionCodes.fundReceive.create)); +const canCreateFulfillment = computed(() => userStore.hasPermissions(permissionCodes.orderFulfillment.create)); +const actions = computed(() => canCreate.value ? [{ + key: "create", + text: "直接产品下单", + icon: "add", + theme: "primary" as const, + action: toCreate +}] : []); const keyword = ref(""); const orderType = ref("ALL"); const viewMode = computed(() => route.query.view === "pending-receive" || route.query.view === "pending-refund" ? String(route.query.view) : ""); @@ -127,14 +125,69 @@ async function fetchList(reset: boolean): Promise { noMore.value = rows.length < pageSize || total.value <= list.value.length; } finally { loading.value = false; } } -async function refresh(): Promise { try { await fetchList(true); } catch (error) { Message.error(getErrorMessage(error, "销售订单加载失败")); } finally { refreshing.value = false; } } -async function loadMore(): Promise { try { await fetchList(false); } catch (error) { Message.error(getErrorMessage(error, "销售订单加载失败")); } } -async function selectOrderType(value: DropdownValue): Promise { if (!Array.isArray(value)) { orderType.value = String(value); await fetchList(true); } } -async function selectStatus(value: DropdownValue): Promise { if (!Array.isArray(value)) { status.value = String(value); await fetchList(true); } } -async function selectPaymentStatus(value: DropdownValue): Promise { if (!Array.isArray(value)) { paymentStatus.value = String(value); await fetchList(true); } } -async function toDetail(item: SalesOrder): Promise { if (item.id) await router.push(`/management/sales-orders/${item.id}`); } -async function toCreate(): Promise { await router.push("/management/sales-orders/create"); } -const search = Toolkit.debounce(async () => { try { await fetchList(true); } catch (error) { Message.error(getErrorMessage(error, "销售订单加载失败")); } }, false); + +async function refresh(): Promise { + try { + await fetchList(true); + } catch (error) { + Message.error(getErrorMessage(error, "销售订单加载失败")); + } finally { + refreshing.value = false; + } +} + +async function loadMore(): Promise { + try { + await fetchList(false); + } catch (error) { + Message.error(getErrorMessage(error, "销售订单加载失败")); + } +} + +async function selectOrderType(value: DropdownValue): Promise { + if (!Array.isArray(value)) { + orderType.value = String(value); + await fetchList(true); + } +} + +async function selectStatus(value: DropdownValue): Promise { + if (!Array.isArray(value)) { + status.value = String(value); + await fetchList(true); + } +} + +async function selectPaymentStatus(value: DropdownValue): Promise { + if (!Array.isArray(value)) { + paymentStatus.value = String(value); + await fetchList(true); + } +} + +async function toDetail(item: SalesOrder): Promise { + if (item.id) await router.push(`/management/sales-orders/${item.id}`); +} + +async function toPayment(item: SalesOrder): Promise { + if (item.id) await router.push(`/management/sales-orders/${item.id}/payment`); +} + +async function toFulfillment(item: SalesOrder): Promise { + if (item.id) await router.push({ path: "/management/order-fulfillments/create", query: { salesOrderId: item.id } }); +} + +async function toCreate(): Promise { + await router.push("/management/sales-orders/create"); +} + +const search = Toolkit.debounce(async () => { + try { + await fetchList(true); + } catch (error) { + Message.error(getErrorMessage(error, "销售订单加载失败")); + } +}, false); watch(keyword, () => search()); watch(viewMode, async value => { status.value = value === "pending-refund" ? "CANCELLED" : "ALL"; @@ -174,11 +227,11 @@ onUnmounted(() => { navBarStore.clearRight(); search.cancel(); }); --td-spacer-1: 6px; width: 100%; + border: 1px solid rgba(148, 163, 184, .18); overflow: hidden; margin-top: -1rem; padding-top: .8rem; background: #fff; - border: 1px solid rgba(148, 163, 184, .18); border-radius: 0 0 1rem 1rem; &:after { @@ -197,26 +250,25 @@ onUnmounted(() => { navBarStore.clearRight(); search.cancel(); }); } } - .title, .description { - gap: .5rem; - display: flex; - align-items: center; - } + .list-wrap { + padding-top: 1rem; - .description { - color: var(--td-text-color-secondary); - flex-wrap: wrap; - } + .view-tip { + color: var(--td-text-color-secondary); + padding: .75rem 1rem 0; + font-size: .8125rem; + } - .amount { - color: var(--td-brand-color); - font-weight: 700; - } + .items { + gap: 1rem; + display: flex; + padding: 0 1rem; + flex-direction: column; - .view-tip { - color: var(--td-text-color-secondary); - padding: .75rem 1rem 0; - font-size: .8125rem; + .item { + border-radius: 1rem; + } + } } .empty { diff --git a/src/pages/salesOrder/components/SalesOrderListCell.vue b/src/pages/salesOrder/components/SalesOrderListCell.vue new file mode 100644 index 0000000..ca767d5 --- /dev/null +++ b/src/pages/salesOrder/components/SalesOrderListCell.vue @@ -0,0 +1,272 @@ + + + + +