Files
gao-customer-h5/src/pages/salesOrder/SalesOrderPayment.vue
T
2026-09-09 15:43:36 +08:00

229 lines
6.3 KiB
Vue

<template>
<div v-can="permissionCodes.fundReceive.create" class="sales-order-operation">
<t-cell-group
v-if="order"
title="销售订单"
theme="card"
v-entrance-transition
>
<t-cell title="订单号" :note="order.orderNo || order.id" />
<t-cell
title="剩余待收款"
:note="formatPackageAmount(order.unpaidAmount)"
/>
</t-cell-group>
<t-form
v-if="order"
class="form"
label-width="6rem"
v-entrance-transition="70"
>
<t-form-item label="收款金额" required-mark>
<t-input
v-model.trim="amountText"
type="number"
align="right"
borderless
placeholder="输入收款金额"
>
<template #suffix-icon>
<t-button
size="extra-small"
theme="primary"
variant="outline"
@click="fillUnpaidAmount"
>全额收款</t-button>
</template>
</t-input>
</t-form-item>
<t-form-item label="收款时间" content-align="right">
<div class="time-actions">
<t-button
theme="primary"
variant="outline"
size="extra-small"
:content="occurredAtText || '此刻'"
@click="openOccurredAtPicker"
/>
<t-button
v-if="occurredAtText"
theme="primary"
variant="text"
size="extra-small"
content="此刻"
@click="resetOccurredAt"
/>
</div>
</t-form-item>
<t-form-item label="备注">
<t-textarea
v-model="remark"
borderless
autosize
maxlength="500"
placeholder="请输入备注"
/>
</t-form-item>
<t-form-item label-width="0">
<photo-scroller
v-if="order"
:model-value="photoUrlList"
v-model:photo-items="photoItems"
class="photo-field"
:upload="uploadPhotoList"
:edit-mode="true"
required
title="收款凭证照片"
/>
</t-form-item>
</t-form>
<t-empty v-if="!order" :description="loading ? '正在加载订单' : '订单不存在'" />
<t-popup v-model="pickerVisible" placement="bottom">
<t-date-time-picker v-if="pickerVisible" v-model="draftTime" format="YYYY-MM-DD HH:mm" :mode="['date', 'minute']" title="收款时间" @cancel="pickerVisible = false" @confirm="confirmOccurredAt" />
</t-popup>
</div>
</template>
<script setup lang="ts">
import { useUserStore, type Attachment } from "timi-web-api";
import { Time } from "timi-web";
import PhotoScroller from "@/components/PhotoScroller.vue";
import SalesOrderAPI from "@/api/SalesOrderAPI";
import { useNavBarStore } from "@/store/navBarStore";
import type { SalesOrder } from "@/types/SalesOrder";
import type { PhotoItem } from "@/types/Photo";
import { createPackageRequestNo, formatPackageAmount, formatPackageAmountInput, parsePackageAmount } from "@/utils/package";
import { getErrorMessage } from "@/utils/error";
import { Message } from "@/utils/message";
import { permissionCodes } from "@/utils/permission";
import { uploadPhotoList } from "@/utils/photo";
defineOptions({ name: "SalesOrderPayment" });
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 amountText = ref("");
const occurredAtText = ref("");
const draftTime = ref("");
const pickerVisible = ref(false);
const remark = ref("");
const photoItems = ref<PhotoItem[]>([]);
const photoUrlList = computed(() => photoItems.value.map(item => item.url));
const requestNo = ref("");
function openOccurredAtPicker(): void {
draftTime.value = occurredAtText.value || Time.formatDateTime(new Date());
pickerVisible.value = true;
}
function confirmOccurredAt(value?: string | number): void {
occurredAtText.value = String(value || draftTime.value);
pickerVisible.value = false;
}
function resetOccurredAt(): void {
occurredAtText.value = "";
draftTime.value = "";
}
function fillUnpaidAmount(): void {
amountText.value = formatPackageAmountInput(order.value?.unpaidAmount);
}
function resolveTimestamp(value: string): number | undefined {
if (!value) return undefined;
const timestamp = new Date(value.replace(" ", "T")).getTime();
return Number.isFinite(timestamp) ? timestamp : undefined;
}
function buildAttachments(): Partial<Attachment>[] {
return photoItems.value.flatMap(item => item.tempFileId ? [{
tempFileId: item.tempFileId,
attachType: "PHOTO"
}] : []);
}
async function loadData(): Promise<void> {
const id = String(route.params.id || "");
if (!id) return;
loading.value = true;
try {
order.value = await SalesOrderAPI.detail(id);
} catch (error) {
Message.error(getErrorMessage(error, "订单加载失败"));
} finally {
loading.value = false;
}
}
async function save(): Promise<void> {
if (saving.value || !order.value?.id) return;
const amount = parsePackageAmount(amountText.value);
const attachmentList = buildAttachments();
if (amount === undefined || amount === 0) {
Message.warning("请输入合法的正金额");
return;
}
if (!attachmentList.length) {
Message.warning("收款必须上传至少一张照片");
return;
}
if (!requestNo.value) requestNo.value = createPackageRequestNo();
saving.value = true;
try {
const record = await SalesOrderAPI.receivePayment({
salesOrderId: order.value.id,
requestNo: requestNo.value,
amount,
occurredAt: resolveTimestamp(occurredAtText.value),
attachmentList,
remark: remark.value.trim() || null
});
Message.success("收款登记已提交");
await router.replace(record.id && userStore.hasPermissions(permissionCodes.fundReceive.create)
? `/management/fund-records/${record.id}`
: `/management/sales-orders/${order.value.id}`);
} catch (error) {
Message.error(getErrorMessage(error, "收款登记失败,重试会复用同一请求号"));
} finally {
saving.value = false;
}
}
onMounted(async () => {
if (!userStore.hasPermissions(permissionCodes.fundReceive.create)) return;
navBarStore.setRightText("提交", "var(--td-brand-color)", save);
await loadData();
});
onUnmounted(() => navBarStore.clearRight());
</script>
<style scoped lang="less">
.sales-order-operation {
gap: 1rem;
padding: calc(var(--app-nav-height) + 1rem) 0 2rem;
display: flex;
flex-direction: column;
.picker-text {
width: 100%;
text-align: right;
}
.time-actions {
gap: .5rem;
display: flex;
align-items: center;
justify-content: flex-end;
}
.photo-field {
padding: 0 1rem;
}
}
</style>