update customer package

This commit is contained in:
Timi
2026-09-06 15:49:20 +08:00
parent b8b3ecbf5d
commit 4c6920be85
19 changed files with 383 additions and 285 deletions
@@ -84,30 +84,28 @@
</template>
<script setup lang="ts">
import PackageAPI from "@/api/PackageAPI";
import SalesOrderAPI from "@/api/SalesOrderAPI";
import FulfillmentAPI from "@/api/FulfillmentAPI";
import PhotoScroller from "@/components/PhotoScroller.vue";
import { useNavBarStore } from "@/store/navBarStore";
import type { CustomerPackageItem, GaoCustomerPackage } from "@/types/Package";
import type { OrderFulfillmentItemRequest } from "@/types/Fulfillment";
import type { SalesOrder, SalesOrderItem } from "@/types/SalesOrder";
import { useUserStore } from "timi-web-api";
import type { GaoOrderFulfillmentItemRequest } from "@/types/Fulfillment";
import type { GaoSalesOrderItem, SalesOrder } from "@/types/SalesOrder";
import type { PhotoItem } from "@/types/Photo";
import type { Attachment } from "timi-web-api";
import { createPackageRequestNo, getPackageQuantityText, normalizePackageQuantity } from "@/utils/package";
import { comparePackageQuantity, createPackageRequestNo, getPackageQuantityText, normalizePackageQuantity } from "@/utils/package";
import { getErrorMessage } from "@/utils/error";
import { Message } from "@/utils/message";
import { permissionCodes } from "@/utils/permission";
import { uploadPhotoList } from "@/utils/photo";
import { getProductPhotoURL } from "@/utils/product";
type SourceItem = SalesOrderItem | CustomerPackageItem;
defineOptions({ name: "OrderFulfillmentCreate" });
const route = useRoute();
const router = useRouter();
const navBarStore = useNavBarStore();
const userStore = useUserStore();
const order = ref<SalesOrder>();
const customerPackage = ref<GaoCustomerPackage>();
const quantityMap = reactive<Record<string, string>>({});
const deliveredAtText = ref("");
const draftTime = ref("");
@@ -117,12 +115,14 @@ const requestNo = ref("");
const isSaving = ref(false);
const photoItems = ref<PhotoItem[]>([]);
const photoUrlList = computed(() => photoItems.value.map(item => item.url));
const isPackage = computed(() => !!customerPackage.value);
const sourceItems = computed<SourceItem[]>(() => customerPackage.value?.itemList || order.value?.itemList || []);
const isPackage = computed(() => order.value?.orderType === "PACKAGE");
const expectedItemType = computed(() => isPackage.value ? "PACKAGE_ENTITLEMENT" : "DIRECT_PRODUCT");
const sourceItems = computed<GaoSalesOrderItem[]>(() => (order.value?.itemList || []).filter(item => item.itemType === expectedItemType.value));
const canOperateStock = computed(() => userStore.hasPermissions(permissionCodes.productStock.create));
const canFulfillByPayment = computed(() => order.value?.paymentStatus === "PAID" || (order.value?.paymentStatus === "PARTIALLY_PAID" && canOperateStock.value));
async function loadData(): Promise<void> {
const salesOrderId = String(route.query.salesOrderId || "");
const customerPackageId = String(route.query.customerPackageId || "");
if (!salesOrderId) {
Message.warning("请从订单详情或客户套餐详情进入履约创建");
await router.replace("/management/order-fulfillments");
@@ -130,26 +130,20 @@ async function loadData(): Promise<void> {
}
try {
order.value = await SalesOrderAPI.detail(salesOrderId);
if (customerPackageId) customerPackage.value = await PackageAPI.customerPackageDetail(customerPackageId);
} catch (error) {
Message.error(getErrorMessage(error, "履约来源加载失败"));
}
}
function getAvailableQuantity(item: SourceItem): string {
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 getAvailableQuantity(item: GaoSalesOrderItem): string {
return getPackageQuantityText(item.availableQuantity);
}
function getItemDescription(item: SourceItem): string {
function getItemDescription(item: GaoSalesOrderItem): string {
return ` · 剩余 ${getAvailableQuantity(item)}`;
}
function fillAllQuantity(item: SourceItem): void {
function fillAllQuantity(item: GaoSalesOrderItem): void {
if (item.id) {
quantityMap[item.id] = getAvailableQuantity(item);
}
@@ -178,6 +172,14 @@ function validate(): boolean {
Message.warning("请从订单详情或客户套餐详情进入履约创建");
return false;
}
if (order.value.status !== "ACTIVE") {
Message.warning("当前订单不是进行中状态,不能创建履约");
return false;
}
if (!canFulfillByPayment.value) {
Message.warning("订单尚未满足履约收款条件");
return false;
}
if (!buildAttachments().length) {
Message.warning("履约必须上传至少一张照片");
return false;
@@ -188,10 +190,15 @@ function validate(): boolean {
return false;
}
for (const item of selected) {
if (!normalizePackageQuantity(quantityMap[item.id || ""])) {
const quantity = normalizePackageQuantity(quantityMap[item.id || ""]);
if (!quantity) {
Message.warning("履约数量必须是正数,最多三位小数");
return false;
}
if (0 < comparePackageQuantity(quantity, item.availableQuantity ?? "0")) {
Message.warning(`${item.productNameSnapshot || "产品"} 的可履约数量不足`);
return false;
}
}
return true;
}
@@ -201,10 +208,10 @@ async function save(): Promise<void> {
if (!requestNo.value) requestNo.value = createPackageRequestNo();
isSaving.value = true;
try {
const itemList: OrderFulfillmentItemRequest[] = sourceItems.value
const itemList: GaoOrderFulfillmentItemRequest[] = sourceItems.value
.filter(item => !!item.id && quantityMap[item.id])
.map(item => ({
...(isPackage.value ? { sourceCustomerPackageItemId: item.id } : { sourceOrderItemId: item.id }),
sourceOrderItemId: item.id!,
quantity: normalizePackageQuantity(quantityMap[item.id || ""])!
}));
const result = await FulfillmentAPI.create({
@@ -3,7 +3,7 @@
<template v-if="fulfillment">
<t-cell-group title="履约单信息" theme="card" v-entrance-transition>
<t-cell title="销售订单" arrow @click="toOrder" />
<t-cell title="履约类型" :note="getFulfillmentTypeText(fulfillment.fulfillmentType)" />
<t-cell title="订单项类型" :note="getSalesOrderItemTypeText(sourceItemType)" />
<t-cell title="状态"><template #note><t-tag :content="getFulfillmentStatusText(fulfillment.status)" variant="light-outline" :theme="getStatusTheme(fulfillment.status)" /></template></t-cell>
<t-cell title="履约时间">
<template #note>
@@ -52,22 +52,22 @@
<t-cell v-if="!fulfillment.itemList?.length" title="暂无履约明细" />
</t-cell-group>
<section
v-if="(isWaitingStock && canVerify) || (isPending && (canVerify || canCreate)) || (isVerified && canRevoke)"
v-if="(!isOrderCancelled && isWaitingStock && canVerify) || (!isOrderCancelled && isPending && (canVerify || canCreate)) || (isVerified && canRevoke)"
class="actions"
v-entrance-transition="140"
>
<t-button
v-if="isWaitingStock && canVerify"
v-if="!isOrderCancelled && isWaitingStock && canVerify"
theme="primary"
content="准备库存"
@click="prepareStock"
/>
<div v-if="isPending && canVerify" class="pair">
<div v-if="!isOrderCancelled && isPending && canVerify" class="pair">
<t-button theme="primary" content="核销履约" @click="verify" />
<t-button theme="danger" content="拒绝履约" @click="openReason('reject')" />
</div>
<t-button
v-if="isPending && canCreate"
v-if="!isOrderCancelled && isPending && canCreate"
theme="default"
content="撤销履约"
@click="openReason('cancel')"
@@ -119,16 +119,18 @@ import { BottomPopup } from "timi-tdesign-mobile";
import { PassTimeLabel } from "timi-web";
import { useUserStore } from "timi-web-api";
import FulfillmentAPI from "@/api/FulfillmentAPI";
import SalesOrderAPI from "@/api/SalesOrderAPI";
import PhotoScroller from "@/components/PhotoScroller.vue";
import { useNavBarStore } from "@/store/navBarStore";
import type { OrderFulfillment } from "@/types/Fulfillment";
import type { GaoSalesOrderItemType, SalesOrder } from "@/types/SalesOrder";
import type { PhotoItem } from "@/types/Photo";
import type { Attachment } from "timi-web-api";
import { createPackageRequestNo } from "@/utils/package";
import { getAttachmentReadURL } from "@/utils/attachment";
import { getErrorMessage } from "@/utils/error";
import { getProductPhotoURL } from "@/utils/product";
import { getFulfillmentStatusText, getFulfillmentTypeText, getStatusTheme } from "@/utils/salesOrder";
import { getFulfillmentStatusText, getSalesOrderItemTypeText, getStatusTheme } from "@/utils/salesOrder";
import { Message } from "@/utils/message";
import { permissionCodes } from "@/utils/permission";
import { uploadPhotoList } from "@/utils/photo";
@@ -140,6 +142,7 @@ const router = useRouter();
const navBarStore = useNavBarStore();
const userStore = useUserStore();
const fulfillment = ref<OrderFulfillment>();
const order = ref<SalesOrder>();
const loading = ref(false);
const operating = ref(false);
const reasonVisible = ref(false);
@@ -163,14 +166,25 @@ const fulfillmentPhotoURLList = computed(() => fulfillmentPhotoItems.value.map(i
const isWaitingStock = computed(() => fulfillment.value?.status === "WAITING_STOCK");
const isPending = computed(() => fulfillment.value?.status === "PENDING");
const isVerified = computed(() => fulfillment.value?.status === "VERIFIED");
const isOrderCancelled = computed(() => order.value?.status === "CANCELLED");
const canVerify = computed(() => userStore.hasPermissions(permissionCodes.orderFulfillment.verify));
const canCreate = computed(() => userStore.hasPermissions(permissionCodes.orderFulfillment.create));
const canRevoke = computed(() => userStore.hasPermissions(permissionCodes.orderFulfillment.revoke));
const sourceItemType = computed<GaoSalesOrderItemType | undefined>(() => {
const sourceOrderItemId = fulfillment.value?.itemList?.[0]?.sourceOrderItemId;
return order.value?.itemList?.find(item => item.id === sourceOrderItemId)?.itemType;
});
async function loadData(): Promise<void> {
const id = String(route.params.id || "");
if (!id) return;
loading.value = true;
try { fulfillment.value = await FulfillmentAPI.detail(id); }
try {
const result = await FulfillmentAPI.detail(id);
fulfillment.value = result;
order.value = result.salesOrderId && userStore.hasPermissions(permissionCodes.salesOrder.read)
? await SalesOrderAPI.detail(result.salesOrderId)
: undefined;
}
catch (error) { Message.error(getErrorMessage(error, "履约单加载失败")); }
finally { loading.value = false; }
}
@@ -194,9 +208,10 @@ function getCurrentRouteDepth(): number {
}
async function prepareStock(): Promise<void> {
if (!fulfillment.value?.id || operating.value) return;
if (isOrderCancelled.value) { Message.warning("已取消订单不能准备库存"); return; }
operating.value = true;
try {
await FulfillmentAPI.prepareStock({ fulfillmentId: fulfillment.value.id });
fulfillment.value = await FulfillmentAPI.prepareStock({ fulfillmentId: fulfillment.value.id, reason: "库存已备齐" });
Message.success("库存准备已提交");
await loadData();
} catch (error) { Message.error(getErrorMessage(error, "库存准备失败")); }
@@ -204,9 +219,10 @@ async function prepareStock(): Promise<void> {
}
async function verify(): Promise<void> {
if (!fulfillment.value?.id || operating.value) return;
if (isOrderCancelled.value) { Message.warning("已取消订单不能核销履约"); return; }
operating.value = true;
try {
await FulfillmentAPI.verify({ fulfillmentId: fulfillment.value.id, reason: "" });
fulfillment.value = await FulfillmentAPI.verify({ fulfillmentId: fulfillment.value.id, reason: "" });
Message.success("履约已核销");
await loadData();
} catch (error) { Message.error(getErrorMessage(error, "履约核销失败")); }
@@ -218,8 +234,9 @@ async function submitReason(): Promise<void> {
operating.value = true;
try {
const payload = { fulfillmentId: fulfillment.value.id, reason: reason.value.trim() };
if (reasonAction.value === "reject") await FulfillmentAPI.reject(payload);
else await FulfillmentAPI.cancel(payload);
fulfillment.value = reasonAction.value === "reject"
? await FulfillmentAPI.reject(payload)
: await FulfillmentAPI.cancel(payload);
reasonVisible.value = false;
Message.success(reasonAction.value === "reject" ? "履约已拒绝" : "履约已撤销");
await loadData();
@@ -236,7 +253,7 @@ async function submitRevoke(): Promise<void> {
if (!revokeRequestNo.value) revokeRequestNo.value = createPackageRequestNo();
operating.value = true;
try {
await FulfillmentAPI.revoke({
fulfillment.value = await FulfillmentAPI.revoke({
fulfillmentId: fulfillment.value.id,
requestNo: revokeRequestNo.value,
reason: revokeReason.value.trim(),
+5 -25
View File
@@ -2,12 +2,6 @@
<div v-can="permissionCodes.orderFulfillment.read" class="fulfillment-list">
<div class="filters">
<t-dropdown-menu class="menus">
<t-dropdown-item
v-model="type"
label="履约类型"
:options="typeOptions"
@change="selectType"
/>
<t-dropdown-item
v-model="status"
label="履约状态"
@@ -47,7 +41,7 @@ import FulfillmentAPI from "@/api/FulfillmentAPI";
import NavBarActionMenu from "@/components/NavBarActionMenu.vue";
import OrderFulfillmentListCell from "@/pages/fulfillment/components/OrderFulfillmentListCell.vue";
import { useNavBarStore } from "@/store/navBarStore";
import type { FulfillmentStatus, FulfillmentType, OrderFulfillment } from "@/types/Fulfillment";
import type { GaoFulfillmentStatus, OrderFulfillment } from "@/types/Fulfillment";
import type { NavBarAction } from "@/types/NavBarAction";
import { getErrorMessage } from "@/utils/error";
import { Message } from "@/utils/message";
@@ -76,8 +70,7 @@ const actions = computed<NavBarAction[]>(() => [
action: toCustomerPackageList
}
]);
const type = ref<"ALL" | FulfillmentType>("ALL");
const status = ref<"ALL" | FulfillmentStatus>(getInitialStatus(route.query.status));
const status = ref<"ALL" | GaoFulfillmentStatus>(getInitialStatus(route.query.status));
const list = ref<OrderFulfillment[]>([]);
const pageIndex = ref(0);
const total = ref(0);
@@ -85,11 +78,6 @@ const loading = ref(false);
const refreshing = ref(false);
const noMore = ref(false);
const pageSize = 15;
const typeOptions = [
{ label: "全部类型", value: "ALL" },
{ label: "套餐权益", value: "PACKAGE_ENTITLEMENT" },
{ label: "直接产品", value: "DIRECT_PRODUCT" }
];
const statusOptions = [
{ label: "全部状态", value: "ALL" },
{ label: "等待库存", value: "WAITING_STOCK" },
@@ -104,10 +92,10 @@ function getQueryText(value: unknown): string {
return Array.isArray(value) ? "" : typeof value === "string" ? value : "";
}
function getInitialStatus(value: unknown): "ALL" | FulfillmentStatus {
function getInitialStatus(value: unknown): "ALL" | GaoFulfillmentStatus {
const statusValue = getQueryText(value);
if (["WAITING_STOCK", "PENDING", "VERIFIED", "REJECTED", "CANCELLED", "REVOKED"].includes(statusValue)) {
return statusValue as FulfillmentStatus;
return statusValue as GaoFulfillmentStatus;
}
return "ALL";
}
@@ -128,7 +116,6 @@ async function fetchList(reset: boolean): Promise<void> {
index: pageIndex.value,
size: pageSize,
salesOrderId: getQueryText(route.query.salesOrderId) || undefined,
fulfillmentType: type.value === "ALL" ? undefined : type.value,
status: status.value === "ALL" ? undefined : status.value
});
const rows = result.list || [];
@@ -159,16 +146,9 @@ async function loadMore(): Promise<void> {
}
}
async function selectType(value: DropdownValue): Promise<void> {
if (!Array.isArray(value)) {
type.value = String(value) as "ALL" | FulfillmentType;
await fetchList(true);
}
}
async function selectStatus(value: DropdownValue): Promise<void> {
if (!Array.isArray(value)) {
status.value = String(value) as "ALL" | FulfillmentStatus;
status.value = String(value) as "ALL" | GaoFulfillmentStatus;
await fetchList(true);
}
}
+41 -18
View File
@@ -7,35 +7,36 @@
<template #title>
<div class="title">
<strong v-text="customerPackage.packageNameSnapshot || '未命名套餐'" />
<t-tag :content="getCustomerPackageStatusText(customerPackage.status)" variant="light-outline" :theme="getStatusTheme(customerPackage.status)" />
<t-tag :content="getSalesOrderStatusText(order?.status)" variant="light-outline" :theme="getStatusTheme(order?.status)" />
</div>
</template>
<template #description><span>购买于 </span><pass-time-label :timestamp="order?.purchasedAt" underline /></template>
<template #description><span>购买于 </span><pass-time-label :timestamp="order?.purchasedAt || customerPackage.salesOrder?.purchasedAt" underline /></template>
</t-cell>
<t-cell v-if="customerPackage.salesOrderId && canReadOrder" title="销售订单" :note="order?.orderNo || customerPackage.salesOrderId" arrow @click="toOrder" />
<t-cell v-if="canReadPackageConfig && customerPackage.packageConfigId" title="套餐方案详情" arrow @click="toPackageConfig" />
</t-cell-group>
<card-container title="套餐权益" description="权益数量来自客户套餐快照,履约状态来自统一履约单" v-entrance-transition="210">
<card-container title="套餐权益" description="权益数量来自销售订单项,履约状态来自统一履约单" v-entrance-transition="210">
<t-cell-group>
<t-cell v-for="item in customerPackage.itemList || []" :key="item.id" class="benefit">
<t-cell v-for="item in salesOrderItems" :key="item.id" class="benefit">
<template #title><div class="title"><strong v-text="item.productNameSnapshot || '未命名产品'" /><t-tag theme="primary" variant="light" :content="item.unitSnapshot || '未设置单位'" /></div></template>
<template #description>
<div class="stats">
<span>总数 <strong v-text="getPackageQuantityText(item.totalQuantity)" /></span>
<span>总数 <strong v-text="getPackageQuantityText(item.quantity)" /></span>
<span>可履约 <strong v-text="getPackageQuantityText(item.availableQuantity)" /></span>
<span>待核销 <strong v-text="getPackageQuantityText(item.pendingQuantity)" /></span>
<span>已履约 <strong v-text="getPackageQuantityText(item.fulfilledQuantity)" /></span>
<span>剩余 <strong v-text="getPackageQuantityText(item.remainingQuantity)" /></span>
</div>
</template>
</t-cell>
<div v-if="!customerPackage.itemList?.length" class="empty" v-text="'暂无产品权益'" />
<div v-if="!salesOrderItems.length" class="empty" v-text="'暂无产品权益'" />
</t-cell-group>
</card-container>
<t-cell-group title="统一履约" theme="card" v-entrance-transition="280">
<t-cell
v-for="item in customerPackage.fulfillmentList || []"
:key="item.id"
:title="`${getFulfillmentTypeText(item.fulfillmentType)} · ${getFulfillmentStatusText(item.status)}`"
:title="`${getSalesOrderItemTypeText(getFulfillmentItemType(item))} · ${getFulfillmentStatusText(item.status)}`"
:note="getFulfillmentSummary(item)"
arrow
@click="toFulfillment(item.id)"
@@ -43,6 +44,19 @@
<t-cell v-if="!customerPackage.fulfillmentList?.length" title="暂无履约记录" />
<t-cell v-if="canCreateFulfillment && hasAvailableItem" title="创建统一履约单" note="选择本次履约的权益数量" arrow @click="toCreateFulfillment" />
</t-cell-group>
<t-cell-group v-if="order" class="finance-panel" title="订单资金摘要" theme="card" v-entrance-transition="350">
<t-cell title="成交金额" :note="formatPackageAmount(order.saleAmount)" />
<t-cell title="已生效收款" :note="formatPackageAmount(order.paidAmount)" />
<t-cell title="待核销收款" :note="formatPackageAmount(order.pendingPaidAmount)" />
<t-cell title="有效退款" :note="formatPackageAmount(order.refundedAmount)" />
<t-cell title="收款状态" :note="getSalesPaymentStatusText(order.paymentStatus)" />
<t-cell title="退款状态" :note="getSalesRefundStatusText(order.refundStatus)" />
</t-cell-group>
<t-cell-group v-if="order?.status === 'CANCELLED'" class="cancel-info" title="取消记录" theme="card" v-entrance-transition="420">
<t-cell title="取消时间" :note="order.cancelledAt ? Time.toDateTime(order.cancelledAt) : '未知'" />
<t-cell title="取消人" :note="order.cancelOperator?.name || customerPackage.cancelOperator?.name || order.cancelOperatorUserId || '未知'" />
<t-cell title="取消原因" :note="order.cancelReason || '未填写'" />
</t-cell-group>
<t-button v-if="canCancel" class="cell-button" theme="danger" @click="openCancel">取消客户套餐</t-button>
</template>
<t-empty v-else :description="isLoading ? '正在加载客户套餐' : '客户套餐不存在'" />
@@ -73,10 +87,10 @@ import CustomerProfileCard from "@/components/CustomerProfileCard.vue";
import { useNavBarStore } from "@/store/navBarStore";
import type { GaoCustomerPackage } from "@/types/Package";
import type { OrderFulfillment } from "@/types/Fulfillment";
import type { SalesOrder } from "@/types/SalesOrder";
import type { GaoSalesOrderItemType, SalesOrder } from "@/types/SalesOrder";
import { getErrorMessage } from "@/utils/error";
import { getCustomerPackageStatusText, getPackageQuantityText } from "@/utils/package";
import { getFulfillmentStatusText, getFulfillmentTypeText } from "@/utils/salesOrder";
import { formatPackageAmount, getPackageQuantityText } from "@/utils/package";
import { getFulfillmentStatusText, getSalesOrderItemTypeText, getSalesOrderStatusText, getSalesPaymentStatusText, getSalesRefundStatusText } from "@/utils/salesOrder";
import { Message } from "@/utils/message";
import { permissionCodes } from "@/utils/permission";
@@ -91,11 +105,15 @@ const isLoading = ref(false);
const cancelVisible = ref(false);
const cancelReason = ref("");
const isCancelling = ref(false);
const canCancel = computed(() => customerPackage.value?.status === "ACTIVE" && userStore.hasPermissions(permissionCodes.customerPackage.cancel));
const canCreateFulfillment = computed(() => userStore.hasPermissions(permissionCodes.orderFulfillment.create));
const hasPendingFulfillment = computed(() => customerPackage.value?.fulfillmentList?.some(item => item.status === "WAITING_STOCK" || item.status === "PENDING") ?? false);
const canOperateStock = computed(() => userStore.hasPermissions(permissionCodes.productStock.create));
const canFulfillByPayment = computed(() => order.value?.paymentStatus === "PAID" || (order.value?.paymentStatus === "PARTIALLY_PAID" && canOperateStock.value));
const canCancel = computed(() => order.value?.status === "ACTIVE" && !hasPendingFulfillment.value && userStore.hasPermissions(permissionCodes.customerPackage.cancel));
const canCreateFulfillment = computed(() => order.value?.status === "ACTIVE" && canFulfillByPayment.value && userStore.hasPermissions(permissionCodes.orderFulfillment.create));
const canReadOrder = computed(() => userStore.hasPermissions(permissionCodes.salesOrder.read));
const canReadPackageConfig = computed(() => userStore.hasPermissions(permissionCodes.packageConfig.read));
const hasAvailableItem = computed(() => customerPackage.value?.itemList?.some(item => 0 < Number(item.availableQuantity)) ?? false);
const salesOrderItems = computed(() => order.value?.itemList || customerPackage.value?.itemList || []);
const hasAvailableItem = computed(() => salesOrderItems.value.some(item => 0 < Number(item.availableQuantity)));
async function loadData(): Promise<void> {
const id = String(route.params.id || "");
@@ -103,9 +121,10 @@ async function loadData(): Promise<void> {
isLoading.value = true;
try {
customerPackage.value = await PackageAPI.customerPackageDetail(id);
order.value = customerPackage.value.salesOrderId && canReadOrder.value
? await SalesOrderAPI.detail(customerPackage.value.salesOrderId)
: undefined;
order.value = customerPackage.value.salesOrder;
if (customerPackage.value.salesOrderId && canReadOrder.value) {
order.value = await SalesOrderAPI.detail(customerPackage.value.salesOrderId);
}
} catch (error) {
customerPackage.value = undefined;
Message.error(getErrorMessage(error, "客户套餐加载失败"));
@@ -115,6 +134,10 @@ async function loadData(): Promise<void> {
}
function getStatusTheme(status?: string): "success" | "warning" | "danger" | "default" { return status === "ACTIVE" ? "success" : status === "CANCELLED" ? "danger" : "default"; }
function getFulfillmentSummary(item: OrderFulfillment): string { return `${item.deliveredAt ? Time.toDateTime(item.deliveredAt) : "时间未知"} · ${(item.itemList || []).length} 项`; }
function getFulfillmentItemType(item: OrderFulfillment): GaoSalesOrderItemType | undefined {
const sourceOrderItemId = item.itemList?.[0]?.sourceOrderItemId;
return salesOrderItems.value.find(orderItem => orderItem.id === sourceOrderItemId)?.itemType;
}
async function toOrder(): Promise<void> {
if (order.value?.id) {
await router.push({
@@ -126,10 +149,10 @@ async function toOrder(): Promise<void> {
async function toPackageConfig(): Promise<void> { if (customerPackage.value?.packageConfigId) await router.push(`/management/package-configs/${customerPackage.value.packageConfigId}`); }
async function toFulfillment(id?: string): Promise<void> { if (id) await router.push(`/management/order-fulfillments/${id}`); }
async function toCreateFulfillment(): Promise<void> {
if (order.value?.id && customerPackage.value?.id) {
if (order.value?.id) {
await router.push({
path: "/management/order-fulfillments/create",
query: { salesOrderId: order.value.id, customerPackageId: customerPackage.value.id }
query: { salesOrderId: order.value.id }
});
}
}
+10 -5
View File
@@ -56,7 +56,7 @@
<span> - </span>
<span v-text="item.packageNameSnapshot || '未命名套餐'" />
</div>
<t-tag :content="getCustomerPackageStatusText(item.status)" variant="light-outline" :theme="getStatusTheme(item.status)" />
<t-tag :content="getSalesOrderStatusText(getPackageStatus(item))" variant="light-outline" :theme="getStatusTheme(getPackageStatus(item))" />
</div>
</template>
<template #description>
@@ -77,10 +77,11 @@ import { type DropdownValue } from "tdesign-mobile-vue";
import { Text, Toolkit } from "timi-web";
import PackageAPI from "@/api/PackageAPI";
import { useNavBarStore } from "@/store/navBarStore";
import type { CustomerPackageStatus, GaoCustomerPackage } from "@/types/Package";
import type { GaoCustomerPackage } from "@/types/Package";
import type { GaoSalesOrderStatus } from "@/types/SalesOrder";
import { getCustomerPhotoURL } from "@/utils/customer";
import { getErrorMessage } from "@/utils/error";
import { getCustomerPackageStatusText } from "@/utils/package";
import { getSalesOrderStatusText } from "@/utils/salesOrder";
import { Message } from "@/utils/message";
import { permissionCodes } from "@/utils/permission";
@@ -114,7 +115,7 @@ async function loadMore(): Promise<void> {
index: pageIndex.value,
size: 15,
keyword: keyword.value || undefined,
status: status.value === "ALL" ? undefined : status.value as CustomerPackageStatus
status: status.value === "ALL" ? undefined : status.value as GaoSalesOrderStatus
});
const list = result.list || [];
packageList.value = packageList.value.concat(list);
@@ -164,7 +165,11 @@ async function toDetail(item: GaoCustomerPackage): Promise<void> {
}
}
function getStatusTheme(statusValue?: CustomerPackageStatus): "success" | "warning" | "default" {
function getPackageStatus(item: GaoCustomerPackage): GaoSalesOrderStatus | undefined {
return item.salesOrder?.status;
}
function getStatusTheme(statusValue?: GaoSalesOrderStatus): "success" | "warning" | "default" {
return statusValue === "ACTIVE" ? "success" : statusValue === "CANCELLED" ? "warning" : "default";
}
+6 -2
View File
@@ -409,12 +409,16 @@ async function submitRecord(): Promise<void> {
isSaving.value = true;
try {
const result = await submitPurchase();
if (!result?.id) {
if (!result?.id || !result.salesOrderId) {
Message.error("套餐购买成功但未返回完整订单关联,请联系管理员");
return;
}
await settingStore.playRegisterSuccessSound();
Message.success("套餐购买已提交");
await router.replace(`/management/customer-packages/${result.id}`);
await router.replace({
path: `/management/customer-packages/${result.id}`,
query: { salesOrderId: result.salesOrderId }
});
} catch (error) {
Message.error(getErrorMessage(error, "套餐购买提交失败"));
} finally {
+2 -6
View File
@@ -78,13 +78,13 @@ export function usePackagePurchase(options: UsePackagePurchaseOptions) {
saleAmount,
purchasedAt: options.buildRegisteredAt(),
attachmentList: options.buildAttachments(),
itemList: packagePurchaseItemList.value.map((item, index) => ({
itemList: packagePurchaseItemList.value.length ? packagePurchaseItemList.value.map((item, index) => ({
productId: item.productId,
productUnitId: item.productUnitId,
totalQuantity: normalizePackageQuantity(item.totalQuantity)!,
sort: item.sort ?? index,
remark: item.remark ?? null
})),
})) : undefined,
remark: options.note.value.trim() || null
});
}
@@ -110,10 +110,6 @@ export function usePackagePurchase(options: UsePackagePurchaseOptions) {
Message.warning("成交价必须是合法的元金额,最多两位小数");
return false;
}
if (!packagePurchaseItemList.value.length) {
Message.warning("购买权益至少需要一项");
return false;
}
for (const item of packagePurchaseItemList.value) {
const quantity = normalizePackageQuantity(item.totalQuantity);
if (!item.productId || !item.productUnitId || !quantity || quantity === "0") {
+1 -1
View File
@@ -216,7 +216,7 @@ async function save(): Promise<void> {
if (!requestNo.value) {
requestNo.value = createPackageRequestNo();
}
const result = await SalesOrderAPI.fundRecordRevoke({
const result = await SalesOrderAPI.revokeFund({
requestNo: requestNo.value,
fundRecordId: record.value.id,
occurredAt: buildOccurredAt(),
+26 -3
View File
@@ -1,6 +1,10 @@
<template>
<div v-can="permissionCodes.salesOrder.cancel" class="sales-order-operation">
<t-cell-group v-if="order" title="销售订单" theme="card"><t-cell title="订单号" :note="order.orderNo || order.id" /><t-cell title="当前状态" :note="order.status || '未知'" /></t-cell-group>
<t-cell-group v-if="order" title="销售订单" theme="card">
<t-cell title="订单号" :note="order.orderNo || order.id" />
<t-cell title="当前状态" :note="getSalesOrderStatusText(order.status)" />
<t-cell v-if="hasPendingFulfillment" title="履约提示" note="存在待处理履约,暂不能取消订单" />
</t-cell-group>
<div v-if="order" class="revoke-tip">取消订单会立即撤销本单有效消费;受影响的订单积分和签到消费条件积分将由系统自动追加撤销流水。退款仍需按资金流程处理。</div>
<t-form v-if="order" class="form" label-align="left" label-width="6rem">
<t-form-item label="取消原因" required-mark><t-textarea v-model.trim="reason" borderless autosize maxlength="500" placeholder="请输入取消原因" /></t-form-item>
@@ -11,26 +15,45 @@
<script setup lang="ts">
import { useUserStore } from "timi-web-api";
import FulfillmentAPI from "@/api/FulfillmentAPI";
import SalesOrderAPI from "@/api/SalesOrderAPI";
import { useNavBarStore } from "@/store/navBarStore";
import type { SalesOrder } from "@/types/SalesOrder";
import { getErrorMessage } from "@/utils/error";
import { Message } from "@/utils/message";
import { permissionCodes } from "@/utils/permission";
import { getSalesOrderStatusText } from "@/utils/salesOrder";
defineOptions({ name: "SalesOrderCancel" });
const route = useRoute(); const router = useRouter(); const navBarStore = useNavBarStore(); const userStore = useUserStore();
const order = ref<SalesOrder>(); const loading = ref(false); const saving = ref(false); const reason = ref("");
const order = ref<SalesOrder>(); const loading = ref(false); const saving = ref(false); const reason = ref(""); const hasPendingFulfillment = ref(false);
async function loadData(): Promise<void> {
const id = String(route.params.id || "");
if (!id) return;
loading.value = true;
try { order.value = await SalesOrderAPI.detail(id); }
try {
order.value = await SalesOrderAPI.detail(id);
if (userStore.hasPermissions(permissionCodes.orderFulfillment.read)) {
const [pendingResult, waitingStockResult] = await Promise.all([
FulfillmentAPI.list({ salesOrderId: id, status: "PENDING", index: 0, size: 1 }),
FulfillmentAPI.list({ salesOrderId: id, status: "WAITING_STOCK", index: 0, size: 1 })
]);
hasPendingFulfillment.value = !!pendingResult.list?.length || !!waitingStockResult.list?.length;
}
}
catch (error) { Message.error(getErrorMessage(error, "订单加载失败")); }
finally { loading.value = false; }
}
async function save(): Promise<void> {
if (!order.value?.id || !reason.value || saving.value) return;
if (order.value.status !== "ACTIVE") {
Message.warning("只有进行中的订单可以取消");
return;
}
if (hasPendingFulfillment.value) {
Message.warning("存在待处理履约,暂不能取消订单");
return;
}
saving.value = true;
try {
await SalesOrderAPI.cancel({ salesOrderId: order.value.id, reason: reason.value.trim() });
+169 -97
View File
@@ -10,7 +10,7 @@
<h1 class="no" v-text="order.orderNo" />
<div class="meta">
<pass-time-label class="time" :timestamp="order.purchasedAt" underline />
<div class="amount" :class="getPaymentAmountClass(order.paymentStatus)">
<div class="amount red text-bold">
<span>¥</span>
<strong class="value" v-text="formatPackageAmount(order.saleAmount)" />
</div>
@@ -66,63 +66,81 @@
</div>
</t-form-item>
</t-form>
<t-cell-group
<t-form
v-if="order.itemList?.length"
class="product empty-cell-title-margin"
class="form product"
title="订单明细"
theme="card"
label-width="0"
v-entrance-transition="140"
>
<t-cell
class="item"
v-for="item in order.itemList"
:key="item.id"
>
<template #leftIcon>
<t-image
v-if="getProductPhotoURL(item)"
:src="getProductPhotoURL(item)"
:alt="item.productNameSnapshot || '产品图片'"
fit="cover"
class="avatar"
/>
<div
v-else
class="avatar fallback"
v-text="(item.productNameSnapshot || '产').slice(0, 1)"
/>
</template>
<template #title>
<div class="title">
<span v-text="getItemTitle(item)"></span>
<t-tag
:content="getItemFulfillmentStatus(item).text"
:theme="getItemFulfillmentStatus(item).theme"
variant="light-outline"
/>
<t-form-item class="form-item" v-for="item in order.itemList" :key="item.id">
<div class="item">
<div class="header">
<div class="left">
<t-image
v-if="getProductPhotoURL(item)"
:src="getProductPhotoURL(item)"
:alt="item.productNameSnapshot || '产品图片'"
fit="cover"
class="avatar"
/>
<div
v-else
class="avatar fallback"
v-text="(item.productNameSnapshot || '产').slice(0, 1)"
/>
</div>
<div class="main">
<div class="title">
<span class="name text-bold" v-text="item.productNameSnapshot" />
<t-tag
:content="getItemFulfillmentStatus(item).text"
:theme="getItemFulfillmentStatus(item).theme"
variant="light-outline"
/>
</div>
<div class="description">
<strong
v-if="item.lineAmount !== null && item.lineAmount !== undefined"
class="red"
v-text="`¥${formatPackageAmount(item.lineAmount)}`"
/>
<t-tag v-else size="small" content="套餐权益" />
<t-tag size="small" theme="primary" variant="light" :content="item.unitSnapshot" />
</div>
</div>
</div>
</template>
<template #description>
<div class="description">
<span>
<span>¥</span>
<strong class="red" v-text="formatPackageAmount(item.lineAmount)" />
</span>
<span>
<span>已履约</span>
<div class="content">
<div class="metric">
<strong v-text="getPackageQuantityText(item.quantity)" />
<span>总数</span>
</div>
<div class="metric">
<strong :class="getAvailableQuantityClass(item)" v-text="getPackageQuantityText(item.availableQuantity)" />
<span>可履约</span>
</div>
<div class="metric">
<strong v-text="getPackageQuantityText(item.pendingQuantity)" />
<span>待核销</span>
</div>
<div class="metric">
<strong :class="getFulfilledQuantityClass(item)" v-text="getPackageQuantityText(item.fulfilledQuantity)" />
</span>
<span>
<span>剩余履约</span>
<strong :class="getRemainingQuantityClass(item)" v-text="getRemainingQuantityText(item)" />
</span>
<span>已履约</span>
</div>
<div class="metric">
<strong :class="getRemainingQuantityClass(item)" v-text="getPackageQuantityText(item.remainingQuantity)" />
<span>剩余</span>
</div>
</div>
</template>
</t-cell>
<t-cell title="履约记录" note="查看本单所有履约单" arrow @click="toFulfillmentList" />
</t-cell-group>
</div>
</t-form-item>
<t-form-item label="履约记录" arrow @click="toFulfillmentList">
查看本单所有履约单
</t-form-item>
</t-form>
<t-button
v-if="canCreateFulfillment && canFulfillByPayment && order.orderType === 'DIRECT_PRODUCT' && order.status === 'ACTIVE'"
v-if="canCreateFulfillment && canFulfillByPayment && order.status === 'ACTIVE'"
class="cell-button"
theme="primary"
content="履约产品"
@@ -149,6 +167,11 @@
<t-cell title="退款状态" :note="getSalesRefundStatusText(order.refundStatus)" />
<t-cell v-if="canReadFund" title="查看全部资金流水" arrow @click="toFundRecords" />
</t-cell-group>
<t-cell-group v-if="order.status === 'CANCELLED'" class="cancel-info" title="取消记录" theme="card">
<t-cell title="取消时间" :note="order.cancelledAt ? Time.toDateTime(order.cancelledAt) : '未知'" />
<t-cell title="取消人" :note="order.cancelOperator?.name || order.cancelOperatorUserId || '未知'" />
<t-cell title="取消原因" :note="order.cancelReason || '未填写'" />
</t-cell-group>
<section class="actions" v-entrance-transition="280">
<t-button
v-if="canReceive"
@@ -179,11 +202,12 @@
<script setup lang="ts">
import { PassTimeLabel, Text, Time } from "timi-web";
import { type Attachment, useUserStore } from "timi-web-api";
import FulfillmentAPI from "@/api/FulfillmentAPI";
import SalesOrderAPI from "@/api/SalesOrderAPI";
import CustomerProfileCard from "@/components/CustomerProfileCard.vue";
import PhotoScroller from "@/components/PhotoScroller.vue";
import { useNavBarStore } from "@/store/navBarStore";
import type { SalesOrder, SalesOrderItem, SalesPaymentStatus } from "@/types/SalesOrder";
import type { GaoSalesOrderItem, GaoSalesPaymentStatus, SalesOrder } from "@/types/SalesOrder";
import type { PhotoItem } from "@/types/Photo";
import { formatPackageAmount, getPackageQuantityText } from "@/utils/package";
import { getAttachmentReadURL } from "@/utils/attachment";
@@ -205,6 +229,7 @@ const router = useRouter();
const navBarStore = useNavBarStore();
const userStore = useUserStore();
const order = ref<SalesOrder>();
const hasPendingFulfillment = ref(false);
const loading = ref(false);
const isEditing = ref(false);
const isSaving = ref(false);
@@ -217,7 +242,7 @@ const canOperateStock = computed(() => userStore.hasPermissions(permissionCodes.
const canReceive = computed(() => !!order.value && order.value.status !== "CANCELLED" && order.value.paymentStatus !== "PAID" && userStore.hasPermissions(permissionCodes.fundReceive.create));
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));
const canCancel = computed(() => !!order.value && order.value.status === "ACTIVE" && !hasPendingFulfillment.value && userStore.hasPermissions(permissionCodes.salesOrder.cancel));
const canFulfillByPayment = computed(() => order.value?.paymentStatus === "PAID" || (order.value?.paymentStatus === "PARTIALLY_PAID" && canOperateStock.value));
const photoURLList = computed(() => photoItems.value.map(item => item.url));
@@ -246,7 +271,7 @@ function getPaidAtText(paidAt?: number | null): string {
return paidAt ? Time.toDateTime(paidAt) : "待后端回填";
}
function getPaymentAmountClass(status?: SalesPaymentStatus): "green" | "orange" | "red" | "gray" {
function getPaymentAmountClass(status?: GaoSalesPaymentStatus): "green" | "orange" | "red" | "gray" {
if (status === "PAID") return "green";
if (status === "PARTIALLY_PAID") return "orange";
if (status === "UNPAID") return "red";
@@ -263,37 +288,22 @@ function getRemainingAmountClass(amount?: number | null): "green" | "red" | "gra
return amount === 0 ? "green" : "red";
}
function getItemTitle(item: SalesOrderItem): string {
const productName = item.productNameSnapshot || "未命名产品";
const quantity = getPackageQuantityText(item.quantity);
const unit = item.unitSnapshot ? ` ${item.unitSnapshot}` : "";
return `${productName} ${quantity}${unit}`;
function getAvailableQuantityClass(item: GaoSalesOrderItem): "green" | "orange" {
return Number(item.availableQuantity || 0) === 0 ? "green" : "orange";
}
function getRemainingQuantity(item: SalesOrderItem): number {
const quantity = Number(item.quantity || 0);
function getFulfilledQuantityClass(item: GaoSalesOrderItem): "green" | "orange" {
return Number(item.remainingQuantity || 0) === 0 ? "green" : "orange";
}
function getRemainingQuantityClass(item: GaoSalesOrderItem): "green" | "orange" {
return Number(item.remainingQuantity || 0) === 0 ? "green" : "orange";
}
function getItemFulfillmentStatus(item: GaoSalesOrderItem): { text: string; theme: "success" | "warning" | "danger" } {
const fulfilledQuantity = Number(item.fulfilledQuantity || 0);
return Math.max(0, quantity - fulfilledQuantity);
}
function getRemainingQuantityText(item: SalesOrderItem): string {
return getPackageQuantityText(getRemainingQuantity(item).toFixed(3));
}
function getFulfilledQuantityClass(item: SalesOrderItem): "green" | "orange" {
const quantity = Number(item.quantity || 0);
const fulfilledQuantity = Number(item.fulfilledQuantity || 0);
return fulfilledQuantity < quantity ? "orange" : "green";
}
function getRemainingQuantityClass(item: SalesOrderItem): "green" | "orange" {
return getRemainingQuantity(item) === 0 ? "green" : "orange";
}
function getItemFulfillmentStatus(item: SalesOrderItem): { text: string; theme: "success" | "warning" | "danger" } {
const quantity = Number(item.quantity || 0);
const fulfilledQuantity = Number(item.fulfilledQuantity || 0);
if (0 < quantity && (fulfilledQuantity === quantity || quantity < fulfilledQuantity)) {
const remainingQuantity = Number(item.remainingQuantity || 0);
if (0 < fulfilledQuantity && remainingQuantity === 0) {
return { text: "已完成", theme: "success" };
}
if (0 < fulfilledQuantity) {
@@ -308,12 +318,25 @@ async function loadData(): Promise<void> {
navBarStore.clearRight();
if (!id) {
order.value = undefined;
hasPendingFulfillment.value = false;
photoItems.value = [];
return;
}
loading.value = true;
try {
order.value = await SalesOrderAPI.detail(id);
hasPendingFulfillment.value = false;
if (userStore.hasPermissions(permissionCodes.orderFulfillment.read)) {
try {
const [pendingResult, waitingStockResult] = await Promise.all([
FulfillmentAPI.list({ salesOrderId: id, status: "PENDING", index: 0, size: 1 }),
FulfillmentAPI.list({ salesOrderId: id, status: "WAITING_STOCK", index: 0, size: 1 })
]);
hasPendingFulfillment.value = !!pendingResult.list?.length || !!waitingStockResult.list?.length;
} catch {
// 取消接口仍会在后端校验待处理履约,查询失败时不阻断订单详情展示。
}
}
editRemark.value = order.value.remark || "";
photoItems.value = toPhotoItems(order.value.attachmentList);
} catch (error) {
@@ -464,7 +487,6 @@ onUnmounted(() => navBarStore.clearRight());
gap: .75rem;
display: flex;
margin-top: .6rem;
color: var(--td-text-color-secondary);
align-items: end;
justify-content: space-between;
@@ -513,27 +535,77 @@ onUnmounted(() => navBarStore.clearRight());
.product {
.item {
.form-item {
padding: 10px;
.avatar {
width: 3.5rem;
height: 3.5rem;
}
.title {
flex: 1;
.item {
width: 100%;
display: flex;
justify-content: space-between;
}
flex-direction: column;
.description {
gap: .75rem;
display: flex;
> span {
gap: .25rem;
.header {
width: 100%;
display: flex;
padding-bottom: .5rem;
border-bottom: 1px solid rgba(155, 155, 155, .2);
.left {
.avatar {
width: 3.5rem;
height: 3.5rem;
}
}
.main {
flex: 1;
display: flex;
padding-left: .5rem;
flex-direction: column;
justify-content: space-between;
.title {
display: flex;
align-items: center;
justify-content: space-between;
}
.description {
gap: .5rem;
display: flex;
}
}
}
.content {
display: flex;
padding-top: 5px;
justify-content: space-around;
.metric {
gap: .25rem;
flex: 1;
display: flex;
position: relative;
align-items: center;
flex-direction: column;
&:not(:last-child):after {
content: "";
top: 50%;
right: 0;
width: 1px;
height: 50%;
transform: translateY(calc(-50% + 2.5px));
position: absolute;
background: rgba(155, 155, 155, .2);
}
span {
color: var(--td-text-color-secondary);
font-size: .65rem;
}
}
}
}
}
+7 -7
View File
@@ -70,7 +70,7 @@ 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, SalesOrderPage, SalesOrderStatus, SalesOrderType, SalesPaymentStatus, SalesRefundStatus } from "@/types/SalesOrder";
import type { GaoSalesOrderStatus, GaoSalesOrderType, GaoSalesPaymentStatus, GaoSalesRefundStatus, SalesOrder, SalesOrderPage } from "@/types/SalesOrder";
import { getErrorMessage } from "@/utils/error";
import { Message } from "@/utils/message";
import { permissionCodes } from "@/utils/permission";
@@ -93,8 +93,8 @@ const actions = computed(() => canCreate.value ? [{
}] : []);
const keyword = ref("");
const orderType = ref("ALL");
type PaymentFilter = "ALL" | SalesPaymentStatus | "PENDING_RECEIVE";
type RefundFilter = "ALL" | SalesRefundStatus;
type PaymentFilter = "ALL" | GaoSalesPaymentStatus | "PENDING_RECEIVE";
type RefundFilter = "ALL" | GaoSalesRefundStatus;
const status = ref("ALL");
const paymentStatus = ref<PaymentFilter>("ALL");
@@ -135,10 +135,10 @@ function buildPageFilter(): SalesOrderPage {
const pendingReceive = paymentStatus.value === "PENDING_RECEIVE";
return {
keyword: keyword.value || undefined,
orderType: orderType.value === "ALL" ? undefined : orderType.value as SalesOrderType,
status: status.value === "ALL" ? undefined : status.value as SalesOrderStatus,
paymentStatus: pendingReceive || paymentStatus.value === "ALL" ? undefined : paymentStatus.value as SalesPaymentStatus,
refundStatus: refundStatus.value === "ALL" ? undefined : refundStatus.value as SalesRefundStatus,
orderType: orderType.value === "ALL" ? undefined : orderType.value as GaoSalesOrderType,
status: status.value === "ALL" ? undefined : status.value as GaoSalesOrderStatus,
paymentStatus: pendingReceive || paymentStatus.value === "ALL" ? undefined : paymentStatus.value as GaoSalesPaymentStatus,
refundStatus: refundStatus.value === "ALL" ? undefined : refundStatus.value as GaoSalesRefundStatus,
pendingReceive: pendingReceive ? true : undefined,
index: pageIndex.value,
size: pageSize
+3 -5
View File
@@ -13,14 +13,13 @@ import { useUserStore } from "timi-web-api";
import SalesOrderAPI from "@/api/SalesOrderAPI";
import { useNavBarStore } from "@/store/navBarStore";
import type { SalesOrder } from "@/types/SalesOrder";
import { createPackageRequestNo } from "@/utils/package";
import { getErrorMessage } from "@/utils/error";
import { Message } from "@/utils/message";
import { permissionCodes } from "@/utils/permission";
defineOptions({ name: "SalesOrderNoRefund" });
const route = useRoute(); const router = useRouter(); const navBarStore = useNavBarStore(); const userStore = useUserStore();
const order = ref<SalesOrder>(); const loading = ref(false); const saving = ref(false); const reason = ref(""); const requestNo = ref("");
const order = ref<SalesOrder>(); const loading = ref(false); const saving = ref(false); const reason = ref("");
async function loadData(): Promise<void> {
const id = String(route.params.id || "");
if (!id) return;
@@ -31,13 +30,12 @@ async function loadData(): Promise<void> {
}
async function save(): Promise<void> {
if (!order.value?.id || !reason.value || saving.value) return;
if (!requestNo.value) requestNo.value = createPackageRequestNo();
saving.value = true;
try {
await SalesOrderAPI.confirmNoRefund({ salesOrderId: order.value.id, requestNo: requestNo.value, reason: reason.value.trim() });
await SalesOrderAPI.confirmNoRefund({ salesOrderId: order.value.id, reason: reason.value.trim() });
Message.success("已确认无需退款");
await router.replace(`/management/sales-orders/${order.value.id}`);
} catch (error) { Message.error(getErrorMessage(error, "确认失败,重试会复用同一请求号")); }
} catch (error) { Message.error(getErrorMessage(error, "确认失败,请刷新后重试")); }
finally { saving.value = false; }
}
onMounted(async () => {
@@ -15,6 +15,12 @@
<div class="customer-status">
<strong class="customer" v-text="customerName" />
<div class="status">
<t-tag
:content="getSalesOrderTypeText(props.order.orderType)"
theme="primary"
size="small"
variant="light-outline"
/>
<t-tag
:content="getSalesOrderStatusText(props.order.status)"
size="small"
@@ -82,10 +88,10 @@
<script setup lang="ts">
import { PassTimeLabel } from "timi-web";
import type { SalesOrder, SalesPaymentStatus } from "@/types/SalesOrder";
import type { GaoSalesPaymentStatus, SalesOrder } from "@/types/SalesOrder";
import { getCustomerPhotoURL } from "@/utils/customer";
import { formatPackageAmount, getPackageQuantityText } from "@/utils/package";
import { getSalesOrderStatusText, getSalesPaymentStatusText, getStatusTheme } from "@/utils/salesOrder";
import { getSalesOrderStatusText, getSalesOrderTypeText, getSalesPaymentStatusText, getStatusTheme } from "@/utils/salesOrder";
defineOptions({ name: "SalesOrderListCell" });
@@ -110,10 +116,10 @@ const fulfilledProductQuantity = computed(() => sumProductQuantity("fulfilledQua
const productSummaryText = computed(() => `共 ${itemList.value.length} 项 · 总数量 ${totalProductQuantity.value} · 已履约 ${fulfilledProductQuantity.value}`);
const showReceiveAction = computed(() => props.canReceive && props.order.status !== "CANCELLED" && props.order.paymentStatus !== "PAID");
const canFulfillByPayment = computed(() => props.order.paymentStatus === "PAID" || (props.order.paymentStatus === "PARTIALLY_PAID" && props.canOperateStock));
const showFulfillAction = computed(() => props.canFulfill && canFulfillByPayment.value && props.order.orderType === "DIRECT_PRODUCT" && props.order.status === "ACTIVE");
const showFulfillAction = computed(() => props.canFulfill && canFulfillByPayment.value && props.order.status === "ACTIVE");
const showActions = computed(() => showReceiveAction.value || showFulfillAction.value);
function getPaymentStatusTheme(status?: SalesPaymentStatus): "success" | "warning" | "danger" | "default" {
function getPaymentStatusTheme(status?: GaoSalesPaymentStatus): "success" | "warning" | "danger" | "default" {
if (status === "PAID") {
return "success";
}