fix SalesOrderDetail.vue style

This commit is contained in:
Timi
2026-09-05 01:38:03 +08:00
parent 25bbdafcc8
commit ca8599450a
7 changed files with 382 additions and 117 deletions
-1
View File
@@ -97,7 +97,6 @@ function openPhotoViewer(): void {
.header { .header {
display: flex; display: flex;
align-items: center; align-items: center;
padding-right: .5rem;
justify-content: space-between; justify-content: space-between;
.name { .name {
+18 -1
View File
@@ -94,8 +94,25 @@ async function refresh(): Promise<void> { try { await fetchList(true); } catch (
async function loadMore(): Promise<void> { try { await fetchList(false); } catch (error) { Message.error(getErrorMessage(error, "履约单加载失败")); } } async function loadMore(): Promise<void> { try { await fetchList(false); } catch (error) { Message.error(getErrorMessage(error, "履约单加载失败")); } }
async function selectType(value: DropdownValue): Promise<void> { if (!Array.isArray(value)) { type.value = String(value); await fetchList(true); } } async function selectType(value: DropdownValue): Promise<void> { if (!Array.isArray(value)) { type.value = String(value); await fetchList(true); } }
async function selectStatus(value: DropdownValue): Promise<void> { if (!Array.isArray(value)) { status.value = String(value); await fetchList(true); } } async function selectStatus(value: DropdownValue): Promise<void> { if (!Array.isArray(value)) { status.value = String(value); await fetchList(true); } }
async function toDetail(item: OrderFulfillment): Promise<void> { if (item.id) await router.push(`/management/order-fulfillments/${item.id}`); } async function toDetail(item: OrderFulfillment): Promise<void> {
if (item.id) await router.push({
path: `/management/order-fulfillments/${item.id}`,
query: { transitionDepth: String(getCurrentRouteDepth() + 1) }
});
}
async function toCreate(): Promise<void> { await router.push("/management/order-fulfillments/create"); } async function toCreate(): Promise<void> { await router.push("/management/order-fulfillments/create"); }
function getCurrentRouteDepth(): number {
const transitionDepth = Array.isArray(route.query.transitionDepth)
? route.query.transitionDepth[0]
: route.query.transitionDepth;
const depth = Number(transitionDepth);
if (transitionDepth && Number.isInteger(depth) && 0 <= depth && depth <= 20) {
return depth;
}
return Number(route.meta.depth ?? 0);
}
onMounted(async () => { navBarStore.clearRight(); navBarStore.setCustomRightVisible(canCreate.value); try { await fetchList(true); } catch (error) { Message.error(getErrorMessage(error, "履约单加载失败")); } }); onMounted(async () => { navBarStore.clearRight(); navBarStore.setCustomRightVisible(canCreate.value); try { await fetchList(true); } catch (error) { Message.error(getErrorMessage(error, "履约单加载失败")); } });
onUnmounted(() => navBarStore.clearRight()); onUnmounted(() => navBarStore.clearRight());
</script> </script>
+15 -1
View File
@@ -132,10 +132,24 @@ function getStatusTheme(statusValue?: FundRecordStatus): "success" | "warning" |
async function toDetail(item: FundRecord): Promise<void> { async function toDetail(item: FundRecord): Promise<void> {
if (item.id) { if (item.id) {
await router.push(`/management/fund-records/${item.id}`); await router.push({
path: `/management/fund-records/${item.id}`,
query: { transitionDepth: String(getCurrentRouteDepth() + 1) }
});
} }
} }
function getCurrentRouteDepth(): number {
const transitionDepth = Array.isArray(route.query.transitionDepth)
? route.query.transitionDepth[0]
: route.query.transitionDepth;
const depth = Number(transitionDepth);
if (transitionDepth && Number.isInteger(depth) && 0 <= depth && depth <= 20) {
return depth;
}
return Number(route.meta.depth ?? 0);
}
onMounted(async () => { onMounted(async () => {
navBarStore.clearRight(); navBarStore.clearRight();
try { try {
+343 -58
View File
@@ -3,45 +3,121 @@
<template v-if="order"> <template v-if="order">
<customer-profile-card v-if="order.customer" class="customer" :customer="order.customer" v-entrance-transition /> <customer-profile-card v-if="order.customer" class="customer" :customer="order.customer" v-entrance-transition />
<section class="hero" v-entrance-transition="35"> <section class="hero" v-entrance-transition="35">
<span class="eyebrow" v-text="'销售订单'" /> <div class="header">
<div class="hero-title"> <span class="eyebrow">销售订单</span>
<h1 v-text="order.orderNo || order.id || '未命名订单'" />
<t-tag :content="getSalesOrderStatusText(order.status)" variant="light-outline" :theme="getStatusTheme(order.status)" /> <t-tag :content="getSalesOrderStatusText(order.status)" variant="light-outline" :theme="getStatusTheme(order.status)" />
</div> </div>
<div class="hero-meta"> <h1 class="no" v-text="order.orderNo" />
<span v-text="getSalesOrderTypeText(order.orderType)" /> <div class="meta">
<strong v-text="formatPackageAmount(order.saleAmount)" /> <pass-time-label class="time" :timestamp="order.purchasedAt" underline />
<div class="amount" :class="getPaymentAmountClass(order.paymentStatus)">
<span>¥</span>
<strong class="value" v-text="formatPackageAmount(order.saleAmount)" />
</div>
</div> </div>
</section> </section>
<t-cell-group title="订单信息" theme="card" v-entrance-transition="70"> <t-cell-group title="订单信息" class="info" theme="card" v-entrance-transition="70">
<t-cell title="订单号" :note="order.orderNo || order.id" />
<t-cell title="订单类型" :note="getSalesOrderTypeText(order.orderType)" />
<t-cell title="订单状态">
<template #note><t-tag :content="getSalesOrderStatusText(order.status)" variant="light-outline" :theme="getStatusTheme(order.status)" /></template>
</t-cell>
<t-cell title="下单时间" :note="order.purchasedAt ? Time.toDateTime(order.purchasedAt) : '未知'" />
<t-cell v-if="order.sourceEventRecord?.id" title="来源登记记录" arrow @click="toEventRecord" /> <t-cell v-if="order.sourceEventRecord?.id" title="来源登记记录" arrow @click="toEventRecord" />
<t-cell v-if="order.remark" title="备注" :note="order.remark" /> <t-cell title="备注" :note="Text.display(order.remark)" />
<div class="photo-wrap bg-white">
<photo-scroller
:model-value="photoURLList"
:photo-items="photoItems"
title="订单照片"
:show-add="false"
:show-remove="false"
:show-viewer-actions="false"
v-entrance-transition="105"
/>
</div>
</t-cell-group> </t-cell-group>
<t-cell-group v-if="order.itemList?.length" title="订单明细" theme="card" v-entrance-transition="140"> <t-cell-group
v-if="order.itemList?.length"
class="product"
title="订单明细"
theme="card"
v-entrance-transition="140"
>
<t-cell <t-cell
v-for="item in order.itemList" v-for="item in order.itemList"
:key="item.id" :key="item.id"
:title="item.productNameSnapshot || '未命名产品'" :title="getItemTitle(item)"
:description="getItemDescription(item)" >
:note="formatPackageAmount(item.lineAmount)" <template #description>
<div class="description">
<span>
<span>¥</span>
<strong class="red" v-text="formatPackageAmount(item.lineAmount)" />
</span>
<span>
<span>已履约</span>
<strong :class="getFulfilledQuantityClass(item)" v-text="getPackageQuantityText(item.fulfilledQuantity)" />
</span>
<span>
<span>剩余履约</span>
<strong :class="getRemainingQuantityClass(item)" v-text="getRemainingQuantityText(item)" />
</span>
</div>
</template>
<template #note>
<t-tag
:content="getItemFulfillmentStatus(item).text"
:theme="getItemFulfillmentStatus(item).theme"
variant="light-outline"
/> />
</t-cell-group> </template>
<sales-order-finance-panel :order="order" :can-read-fund="canReadFund" @fund="toFundRecords" @record="toFundDetail" /> </t-cell>
<t-cell-group title="统一履约" theme="card" v-entrance-transition="210">
<t-cell title="履约记录" note="查看本单所有履约单" arrow @click="toFulfillmentList" /> <t-cell title="履约记录" note="查看本单所有履约单" arrow @click="toFulfillmentList" />
<t-cell v-if="canCreateFulfillment && order.orderType === 'DIRECT_PRODUCT' && order.status === 'ACTIVE'" title="创建履约单" note="直接产品订单统一履约" arrow @click="toCreateFulfillment" /> </t-cell-group>
<t-button
v-if="canCreateFulfillment && canFulfillByPayment && order.orderType === 'DIRECT_PRODUCT' && order.status === 'ACTIVE'"
class="cell-button"
theme="primary"
content="履约产品"
@click="toCreateFulfillment"
/>
<t-cell-group class="finance-panel" title="资金摘要" theme="card">
<t-cell title="成交金额">
<template #note><span :class="getPaymentAmountClass(order.paymentStatus)" v-text="formatPackageAmount(order.saleAmount)" /></template>
</t-cell>
<t-cell title="已生效收款">
<template #note><span :class="getAmountClass(order.paidAmount, 'green')" v-text="formatPackageAmount(order.paidAmount)" /></template>
</t-cell>
<t-cell title="待核销收款">
<template #note><span :class="getAmountClass(order.pendingPaidAmount, 'orange')" v-text="formatPackageAmount(order.pendingPaidAmount)" /></template>
</t-cell>
<t-cell title="剩余待收款">
<template #note><span :class="getRemainingAmountClass(order.unpaidAmount)" v-text="formatPackageAmount(order.unpaidAmount)" /></template>
</t-cell>
<t-cell title="有效退款">
<template #note><span :class="getAmountClass(order.refundedAmount, 'red')" v-text="formatPackageAmount(order.refundedAmount)" /></template>
</t-cell>
<t-cell title="收款状态" :note="getSalesPaymentStatusText(order.paymentStatus)" />
<t-cell v-if="order.paymentStatus === 'PAID'" title="全额实收时间" :note="getPaidAtText(order.paidAt)" />
<t-cell title="退款状态" :note="getSalesRefundStatusText(order.refundStatus)" />
<t-cell v-if="canReadFund" title="查看全部资金流水" arrow @click="toFundRecords" />
</t-cell-group> </t-cell-group>
<section class="actions" v-entrance-transition="280"> <section class="actions" v-entrance-transition="280">
<t-button v-if="canReceive" theme="primary" @click="toPayment">登记收款</t-button> <t-button
<t-button v-if="canRefund" theme="danger" variant="outline" @click="toRefund">登记退款</t-button> v-if="canReceive"
<t-button v-if="canConfirmNoRefund" theme="default" variant="outline" @click="toNoRefund">确认无需退款</t-button> theme="primary"
<t-button v-if="canCancel" theme="danger" variant="outline" @click="toCancel">取消订单</t-button> @click="toPayment"
>登记收款</t-button>
<t-button
v-if="canRefund"
theme="danger"
@click="toRefund"
>登记退款</t-button>
<t-button
v-if="canConfirmNoRefund"
theme="default"
@click="toNoRefund"
>确认无需退款</t-button>
<t-button
v-if="canCancel"
theme="danger"
@click="toCancel"
>取消订单</t-button>
</section> </section>
</template> </template>
<t-empty v-else :description="loading ? '正在加载订单' : '订单不存在'" /> <t-empty v-else :description="loading ? '正在加载订单' : '订单不存在'" />
@@ -49,16 +125,23 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Time } from "timi-web"; import { PassTimeLabel, Text, Time } from "timi-web";
import { useUserStore } from "timi-web-api"; import { useUserStore } from "timi-web-api";
import SalesOrderAPI from "@/api/SalesOrderAPI"; import SalesOrderAPI from "@/api/SalesOrderAPI";
import CustomerProfileCard from "@/components/CustomerProfileCard.vue"; import CustomerProfileCard from "@/components/CustomerProfileCard.vue";
import SalesOrderFinancePanel from "@/pages/salesOrder/components/SalesOrderFinancePanel.vue"; import PhotoScroller from "@/components/PhotoScroller.vue";
import { useNavBarStore } from "@/store/navBarStore"; import { useNavBarStore } from "@/store/navBarStore";
import type { SalesOrder, SalesOrderItem } from "@/types/SalesOrder"; import type { SalesOrder, SalesOrderItem, SalesPaymentStatus } from "@/types/SalesOrder";
import type { PhotoItem } from "@/types/Photo";
import { formatPackageAmount, getPackageQuantityText } from "@/utils/package"; import { formatPackageAmount, getPackageQuantityText } from "@/utils/package";
import { getAttachmentReadURL } from "@/utils/attachment";
import { getErrorMessage } from "@/utils/error"; import { getErrorMessage } from "@/utils/error";
import { getSalesOrderStatusText, getSalesOrderTypeText, getStatusTheme } from "@/utils/salesOrder"; import {
getSalesOrderStatusText,
getSalesPaymentStatusText,
getSalesRefundStatusText,
getStatusTheme
} from "@/utils/salesOrder";
import { Message } from "@/utils/message"; import { Message } from "@/utils/message";
import { permissionCodes } from "@/utils/permission"; import { permissionCodes } from "@/utils/permission";
@@ -71,53 +154,255 @@ const order = ref<SalesOrder>();
const loading = ref(false); const loading = ref(false);
const canReadFund = computed(() => userStore.hasPermissions(permissionCodes.fundRecord.read)); const canReadFund = computed(() => userStore.hasPermissions(permissionCodes.fundRecord.read));
const canCreateFulfillment = computed(() => userStore.hasPermissions(permissionCodes.orderFulfillment.create)); const canCreateFulfillment = computed(() => userStore.hasPermissions(permissionCodes.orderFulfillment.create));
const canOperateStock = computed(() => userStore.hasPermissions(permissionCodes.productStock.create));
const canReceive = computed(() => !!order.value && order.value.status !== "CANCELLED" && order.value.paymentStatus !== "PAID" && userStore.hasPermissions(permissionCodes.fundReceive.create)); 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 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 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" && userStore.hasPermissions(permissionCodes.salesOrder.cancel));
function getItemDescription(item: SalesOrderItem): string { const canFulfillByPayment = computed(() => order.value?.paymentStatus === "PAID" || (order.value?.paymentStatus === "PARTIALLY_PAID" && canOperateStock.value));
const photoItems = computed<PhotoItem[]>(() => (order.value?.attachmentList || [])
.filter(item => !!item.id)
.map(item => ({
attachment: item,
attachType: item.attachType || "PHOTO",
previewUrl: getAttachmentReadURL(item.id || ""),
url: getAttachmentReadURL(item.id || "", 90)
})));
const photoURLList = computed(() => photoItems.value.map(item => item.url));
function getPaidAtText(paidAt?: number | null): string {
return paidAt ? Time.toDateTime(paidAt) : "待后端回填";
}
function getPaymentAmountClass(status?: SalesPaymentStatus): "green" | "orange" | "red" | "gray" {
if (status === "PAID") return "green";
if (status === "PARTIALLY_PAID") return "orange";
if (status === "UNPAID") return "red";
return "gray";
}
function getAmountClass(amount?: number | null, activeClass: "green" | "orange" | "red" = "green"): "green" | "orange" | "red" | "gray" {
if (amount === undefined || amount === null || amount === 0) return "gray";
return activeClass;
}
function getRemainingAmountClass(amount?: number | null): "green" | "red" | "gray" {
if (amount === undefined || amount === null) return "gray";
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 getRemainingQuantity(item: SalesOrderItem): number {
const quantity = Number(item.quantity || 0); const quantity = Number(item.quantity || 0);
const fulfilledQuantity = Number(item.fulfilledQuantity || 0); const fulfilledQuantity = Number(item.fulfilledQuantity || 0);
const remainingQuantity = Math.max(0, quantity - fulfilledQuantity); return Math.max(0, quantity - fulfilledQuantity);
return `${getPackageQuantityText(item.quantity)} ${item.unitSnapshot || ""} · 已履约 ${getPackageQuantityText(item.fulfilledQuantity)} · 剩余待履约 ${getPackageQuantityText(remainingQuantity.toFixed(3))}`; }
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)) {
return { text: "已完成", theme: "success" };
}
if (0 < fulfilledQuantity) {
return { text: "部分履约", theme: "warning" };
}
return { text: "未履约", theme: "danger" };
} }
async function loadData(): Promise<void> { async function loadData(): Promise<void> {
const id = String(route.params.id || ""); const id = String(route.params.id || "");
if (!id) return; if (!id) return;
loading.value = true; loading.value = true;
try { order.value = await SalesOrderAPI.detail(id); } try {
catch (error) { Message.error(getErrorMessage(error, "订单加载失败")); } order.value = await SalesOrderAPI.detail(id);
finally { loading.value = false; } } catch (error) {
Message.error(getErrorMessage(error, "订单加载失败"));
} finally {
loading.value = false;
} }
async function toEventRecord(): Promise<void> { if (order.value?.sourceEventRecord?.id) await router.push(`/management/registration-event-records/${order.value.sourceEventRecord.id}`); } }
async function toFundRecords(): Promise<void> { if (order.value?.id) await router.push({ path: "/management/fund-records", query: { salesOrderId: order.value.id } }); }
async function toFundDetail(id: string): Promise<void> { await router.push(`/management/fund-records/${id}`); } async function toEventRecord(): Promise<void> {
async function toFulfillmentList(): Promise<void> { if (order.value?.id) await router.push({ path: "/management/order-fulfillments", query: { salesOrderId: order.value.id } }); } if (order.value?.sourceEventRecord?.id) await router.push(`/management/registration-event-records/${order.value.sourceEventRecord.id}`);
async function toCreateFulfillment(): Promise<void> { if (order.value?.id) await router.push({ path: "/management/order-fulfillments/create", query: { salesOrderId: order.value.id } }); } }
async function toPayment(): Promise<void> { if (order.value?.id) await router.push(`/management/sales-orders/${order.value.id}/payment`); }
async function toRefund(): Promise<void> { if (order.value?.id) await router.push(`/management/sales-orders/${order.value.id}/refund`); } async function toFundRecords(): Promise<void> {
async function toNoRefund(): Promise<void> { if (order.value?.id) await router.push(`/management/sales-orders/${order.value.id}/no-refund`); } if (order.value?.id) await router.push({
async function toCancel(): Promise<void> { if (order.value?.id) await router.push(`/management/sales-orders/${order.value.id}/cancel`); } path: "/management/fund-records",
query: {
salesOrderId: order.value.id,
transitionDepth: String(getCurrentRouteDepth() + 1)
}
});
}
async function toFulfillmentList(): Promise<void> {
if (order.value?.id) await router.push({
path: "/management/order-fulfillments",
query: {
salesOrderId: order.value.id,
transitionDepth: String(getCurrentRouteDepth() + 1)
}
});
}
async function toCreateFulfillment(): Promise<void> {
if (order.value?.id) await router.push({
path: "/management/order-fulfillments/create",
query: { salesOrderId: order.value.id }
});
}
async function toPayment(): Promise<void> {
if (order.value?.id) await router.push(`/management/sales-orders/${order.value.id}/payment`);
}
async function toRefund(): Promise<void> {
if (order.value?.id) await router.push(`/management/sales-orders/${order.value.id}/refund`);
}
async function toNoRefund(): Promise<void> {
if (order.value?.id) await router.push(`/management/sales-orders/${order.value.id}/no-refund`);
}
async function toCancel(): Promise<void> {
if (order.value?.id) await router.push(`/management/sales-orders/${order.value.id}/cancel`);
}
function getCurrentRouteDepth(): number {
const transitionDepth = Array.isArray(route.query.transitionDepth)
? route.query.transitionDepth[0]
: route.query.transitionDepth;
const depth = Number(transitionDepth);
if (transitionDepth && Number.isInteger(depth) && 0 <= depth && depth <= 20) {
return depth;
}
return Number(route.meta.depth ?? 0);
}
onMounted(loadData); onMounted(loadData);
onUnmounted(() => navBarStore.clearRight()); onUnmounted(() => navBarStore.clearRight());
</script> </script>
<style scoped lang="less"> <style scoped lang="less">
.sales-order-detail { gap: 1rem; padding: calc(var(--app-nav-height) + 1rem) 0 2rem; display: flex; flex-direction: column; .sales-order-detail {
.customer { margin: 0 1rem; } gap: 1rem;
.hero { margin: 0 1rem; padding: 1rem; background: linear-gradient(135deg, #fff7ed, #eff6ff); box-shadow: var(--td-shadow-1); border-radius: 1.25rem; padding: calc(var(--app-nav-height) + 1rem) 0 2rem;
.eyebrow { color: #c2410c; display: block; font-size: .6875rem; font-weight: 700; letter-spacing: .08em; } display: flex;
.hero-title { gap: .5rem; display: flex; margin-top: .35rem; align-items: center; justify-content: space-between; } flex-direction: column;
h1 { margin: 0; min-width: 0; font-size: 1.2rem; line-height: 1.35; }
.hero-meta { gap: .75rem; display: flex; margin-top: .6rem; color: var(--td-text-color-secondary); align-items: center; justify-content: space-between; } .customer {
strong { color: #c2410c; font-size: 1.2rem; white-space: nowrap; } margin: 0 1rem;
}
.actions { gap: .5rem; padding: 0 1rem; display: flex; flex-wrap: wrap; }
.actions .t-button { flex: 1 1 8rem; }
} }
:global(html.theme-dark) .sales-order-detail .hero { background: linear-gradient(135deg, rgba(194, 65, 12, .2), rgba(37, 99, 235, .18)); .hero {
.eyebrow, strong { color: #fdba74; } margin: 0 1rem;
padding: 1rem;
background: linear-gradient(135deg, #fff7ed, #eff6ff);
box-shadow: var(--td-shadow-1);
border-radius: 1.25rem;
.header {
display: flex;
align-items: center;
justify-content: space-between;
.eyebrow {
color: #c2410c;
display: block;
font-weight: 700;
letter-spacing: .08em;
}
}
.no {
margin: 1rem 0;
min-width: 0;
font-size: 1.2rem;
line-height: 1.35;
}
.meta {
gap: .75rem;
display: flex;
margin-top: .6rem;
color: var(--td-text-color-secondary);
align-items: end;
justify-content: space-between;
.time {
font-size: .875rem;
}
.amount {
.value {
font-size: 1.5rem;
}
}
}
}
.info {
.photo-wrap {
padding: .75rem;
}
}
.product {
.description {
gap: .75rem;
display: flex;
> span {
gap: .25rem;
display: flex;
}
}
}
.actions {
--td-button-border-radius: 1rem;
gap: .5rem;
padding: 0 1rem;
display: flex;
flex-wrap: wrap;
}
.actions .t-button {
flex: 1 1 8rem;
}
}
:global(html.theme-dark) .sales-order-detail .hero {
background: linear-gradient(135deg, rgba(194, 65, 12, .2), rgba(37, 99, 235, .18));
.eyebrow, strong {
color: #fdba74;
}
} }
</style> </style>
+2
View File
@@ -43,6 +43,7 @@
:order="item" :order="item"
:can-receive="canReceive" :can-receive="canReceive"
:can-fulfill="canCreateFulfillment" :can-fulfill="canCreateFulfillment"
:can-operate-stock="canOperateStock"
v-entrance-transition="index * 30" v-entrance-transition="index * 30"
@click="toDetail(item)" @click="toDetail(item)"
@receive="toPayment(item)" @receive="toPayment(item)"
@@ -77,6 +78,7 @@ const userStore = useUserStore();
const canCreate = computed(() => userStore.hasPermissions(permissionCodes.salesOrder.create)); const canCreate = computed(() => userStore.hasPermissions(permissionCodes.salesOrder.create));
const canReceive = computed(() => userStore.hasPermissions(permissionCodes.fundReceive.create)); const canReceive = computed(() => userStore.hasPermissions(permissionCodes.fundReceive.create));
const canCreateFulfillment = computed(() => userStore.hasPermissions(permissionCodes.orderFulfillment.create)); const canCreateFulfillment = computed(() => userStore.hasPermissions(permissionCodes.orderFulfillment.create));
const canOperateStock = computed(() => userStore.hasPermissions(permissionCodes.productStock.create));
const actions = computed(() => canCreate.value ? [{ const actions = computed(() => canCreate.value ? [{
key: "create", key: "create",
text: "直接产品下单", text: "直接产品下单",
@@ -1,54 +0,0 @@
<template>
<div class="finance-panel">
<t-cell-group title="资金摘要" theme="card">
<t-cell title="成交金额" :note="formatPackageAmount(props.order.saleAmount)" />
<t-cell title="已生效收款" :note="formatPackageAmount(props.order.paidAmount)" />
<t-cell title="待核销收款" :note="formatPackageAmount(props.order.pendingPaidAmount)" />
<t-cell title="剩余待收款" :note="formatPackageAmount(props.order.unpaidAmount)" />
<t-cell title="有效退款" :note="formatPackageAmount(props.order.refundedAmount)" />
<t-cell title="收款状态" :note="getSalesPaymentStatusText(props.order.paymentStatus)" />
<t-cell v-if="props.order.paymentStatus === 'PAID'" title="全额实收时间" :note="getPaidAtText(props.order.paidAt)" />
<t-cell title="退款状态" :note="getSalesRefundStatusText(props.order.refundStatus)" />
<t-cell v-if="props.canReadFund" title="查看全部资金流水" arrow @click="emit('fund')" />
</t-cell-group>
<t-cell-group title="本单资金操作" theme="card">
<t-cell
v-for="item in props.order.fundRecordList || []"
:key="item.id"
:title="getFundOperationText(item.operationType)"
:note="`${getFundStatusText(item.status)} · ${item.id || ''}`"
arrow
@click="item.id && emit('record', item.id)"
>
<template #note>
<span class="record-note">
<t-tag :content="getFundStatusText(item.status)" variant="light-outline" :theme="getStatusTheme(item.status)" />
<span class="amount" v-text="formatPackageAmount(item.amount)" />
</span>
</template>
</t-cell>
<t-cell v-if="!props.order.fundRecordList?.length" title="暂无资金流水" />
</t-cell-group>
</div>
</template>
<script setup lang="ts">
import type { SalesOrder } from "@/types/SalesOrder";
import { formatPackageAmount } from "@/utils/package";
import { getFundOperationText, getFundStatusText, getSalesPaymentStatusText, getSalesRefundStatusText, getStatusTheme } from "@/utils/salesOrder";
import { Time } from "timi-web";
const props = defineProps<{ order: SalesOrder; canReadFund: boolean }>();
const emit = defineEmits<{ fund: []; record: [id: string] }>();
function getPaidAtText(paidAt?: number | null): string {
return paidAt ? Time.toDateTime(paidAt) : "待后端回填";
}
</script>
<style scoped lang="less">
.finance-panel { display: grid; gap: 1rem;
.record-note { gap: .5rem; display: flex; align-items: center; }
.amount { color: var(--td-brand-color); font-weight: 700; }
}
</style>
@@ -81,7 +81,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { PassTimeLabel, Time } from "timi-web"; import { PassTimeLabel } from "timi-web";
import type { SalesOrder, SalesPaymentStatus } from "@/types/SalesOrder"; import type { SalesOrder, SalesPaymentStatus } from "@/types/SalesOrder";
import { getCustomerPhotoURL } from "@/utils/customer"; import { getCustomerPhotoURL } from "@/utils/customer";
import { formatPackageAmount, getPackageQuantityText } from "@/utils/package"; import { formatPackageAmount, getPackageQuantityText } from "@/utils/package";
@@ -93,6 +93,7 @@ const props = defineProps<{
order: SalesOrder; order: SalesOrder;
canReceive: boolean; canReceive: boolean;
canFulfill: boolean; canFulfill: boolean;
canOperateStock: boolean;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
click: []; click: [];
@@ -108,7 +109,8 @@ const totalProductQuantity = computed(() => sumProductQuantity("quantity"));
const fulfilledProductQuantity = computed(() => sumProductQuantity("fulfilledQuantity")); const fulfilledProductQuantity = computed(() => sumProductQuantity("fulfilledQuantity"));
const productSummaryText = computed(() => `共 ${itemList.value.length} 项 · 总数量 ${totalProductQuantity.value} · 已履约 ${fulfilledProductQuantity.value}`); const productSummaryText = computed(() => `共 ${itemList.value.length} 项 · 总数量 ${totalProductQuantity.value} · 已履约 ${fulfilledProductQuantity.value}`);
const showReceiveAction = computed(() => props.canReceive && props.order.status !== "CANCELLED" && props.order.paymentStatus !== "PAID"); const showReceiveAction = computed(() => props.canReceive && props.order.status !== "CANCELLED" && props.order.paymentStatus !== "PAID");
const showFulfillAction = computed(() => props.canFulfill && props.order.orderType === "DIRECT_PRODUCT" && props.order.status === "ACTIVE"); 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 showActions = computed(() => showReceiveAction.value || showFulfillAction.value); const showActions = computed(() => showReceiveAction.value || showFulfillAction.value);
function getPaymentStatusTheme(status?: SalesPaymentStatus): "success" | "warning" | "danger" | "default" { function getPaymentStatusTheme(status?: SalesPaymentStatus): "success" | "warning" | "danger" | "default" {