update customer package
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user