247 lines
7.7 KiB
Vue
247 lines
7.7 KiB
Vue
<template>
|
|
<div v-can="permissionCodes.pointRedemption.create" class="redemption-detail">
|
|
<customer-profile-card
|
|
v-if="customer"
|
|
class="customer-profile"
|
|
:customer="customer"
|
|
v-entrance-transition
|
|
/>
|
|
<t-cell-group v-if="item" title="兑换信息" theme="card" v-entrance-transition="70">
|
|
<t-cell title="客户" :note="item.customer?.name || item.customerId || '-'" />
|
|
<t-cell title="兑换商品" :note="item.productNameSnapshot || item.skuNameSnapshot || '-'" />
|
|
<t-cell title="兑换数量" :note="`${quantityText(item.quantity)} ${item.unitSnapshot || ''}`" />
|
|
<t-cell title="积分" :note="`${formatPoint(item.actualPointCost)}${item.actualPointCost !== item.defaultPointCost ? '(已减免)' : ''}`" />
|
|
<t-cell title="状态" :note="statusText(item.status)" />
|
|
<t-cell title="兑换时间" :note="timeText(item.redeemedAt)" />
|
|
<t-cell v-if="item.remark" title="备注" :note="item.remark" />
|
|
<div class="photo-wrap bg-white">
|
|
<media-selector
|
|
:model-value="attachmentUrls"
|
|
:show-add="false"
|
|
:show-remove="false"
|
|
:show-viewer-actions="false"
|
|
title="兑换凭证照片"
|
|
/>
|
|
</div>
|
|
</t-cell-group>
|
|
<t-cell-group v-if="returnList.length" title="退货记录" theme="card" v-entrance-transition="140">
|
|
<t-cell
|
|
v-for="record in returnList"
|
|
:key="record.id"
|
|
:title="`退货 ${quantityText(record.quantity)} ${item?.unitSnapshot || ''}`"
|
|
:note="`${formatPoint(record.pointCost)} · ${timeText(record.returnedAt)}`"
|
|
/>
|
|
</t-cell-group>
|
|
<t-empty v-if="!item" :description="loading ? '正在加载兑换单' : '兑换单不存在'" />
|
|
<t-button
|
|
v-if="canReturn"
|
|
class="return-button cell-button"
|
|
theme="danger"
|
|
content="冲正兑换"
|
|
v-entrance-transition="140"
|
|
@click="returnVisible = true"
|
|
/>
|
|
</div>
|
|
<bottom-popup
|
|
v-model="returnVisible"
|
|
title="冲正兑换"
|
|
left-button-text="取消"
|
|
:right-button-text="returning ? '提交中' : '确认冲正'"
|
|
@left-button-click="returnVisible = false"
|
|
@right-button-click="submitReturn"
|
|
>
|
|
<div class="return-form">
|
|
<t-form class="form" label-width="6rem">
|
|
<t-form-item label="剩余数量">
|
|
<span v-text="`${remainText} ${item?.unitSnapshot || ''}`" />
|
|
</t-form-item>
|
|
<t-form-item label="冲正数量" required-mark>
|
|
<t-input v-model.trim="returnQuantity" type="number" align="right" borderless />
|
|
</t-form-item>
|
|
<t-form-item label="冲正原因">
|
|
<t-textarea v-model.trim="returnRemark" maxlength="500" autosize />
|
|
</t-form-item>
|
|
<t-form-item label-width="0">
|
|
<media-selector
|
|
v-model="returnPhotoUrls"
|
|
v-model:photo-items="returnPhotos"
|
|
:upload="uploadPhotoList"
|
|
edit-mode
|
|
required
|
|
title="冲正凭证照片"
|
|
/>
|
|
</t-form-item>
|
|
</t-form>
|
|
</div>
|
|
</bottom-popup>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { BottomPopup } from "timi-tdesign-mobile";
|
|
import { AttachmentAPI, useUserStore } from "timi-web-api";
|
|
import CustomerAPI from "@/api/CustomerAPI";
|
|
import PointAPI from "@/api/PointAPI";
|
|
import CustomerProfileCard from "@/components/CustomerProfileCard.vue";
|
|
import MediaSelector from "@/components/mediaSelector/MediaSelector.vue";
|
|
import type { Customer } from "@/types/Customer";
|
|
import type { PhotoItem } from "@/types/Photo";
|
|
import type { PointRedemption, PointRedemptionReturn, PointRedemptionStatus } from "@/types/Point";
|
|
import { getErrorMessage } from "@/utils/error";
|
|
import { Message } from "@/utils/message";
|
|
import { formatWanInteger } from "@/utils/money";
|
|
import { comparePackageQuantity, getPackageQuantityText, normalizePackageQuantity } from "@/utils/package";
|
|
import { uploadPhotoList } from "@/utils/photo";
|
|
import { permissionCodes } from "@/utils/permission";
|
|
|
|
defineOptions({ name: "PointRedemptionDetail" });
|
|
|
|
const route = useRoute();
|
|
const userStore = useUserStore();
|
|
const item = ref<PointRedemption>();
|
|
const customer = ref<Customer>();
|
|
const returnList = ref<PointRedemptionReturn[]>([]);
|
|
const loading = ref(false);
|
|
const returnVisible = ref(false);
|
|
const returnQuantity = ref("");
|
|
const returnRemark = ref("");
|
|
const returnPhotos = ref<PhotoItem[]>([]);
|
|
const returnRequestNo = ref("");
|
|
const returning = ref(false);
|
|
const attachmentUrls = computed(() => (item.value?.attachmentList || [])
|
|
.filter(attachment => !!attachment.id)
|
|
.map(attachment => AttachmentAPI.getReadURL(attachment.id!)));
|
|
const returnPhotoUrls = computed(() => returnPhotos.value.map(photo => photo.url));
|
|
const canReturn = computed(() => userStore.hasPermissions(permissionCodes.pointRedemption.return)
|
|
&& (item.value?.status === "COMPLETED" || item.value?.status === "PARTIALLY_RETURNED")
|
|
&& 0 < comparePackageQuantity(remaining(), "0"));
|
|
const remainText = computed(() => quantityText(remaining()));
|
|
|
|
function remaining(): string {
|
|
const total = Number(item.value?.quantity || 0);
|
|
const returned = Number(item.value?.returnedQuantity || 0);
|
|
return String(Math.max(0, total - returned));
|
|
}
|
|
|
|
async function load(): Promise<void> {
|
|
const id = String(route.params.id || "");
|
|
if (!id) {
|
|
return;
|
|
}
|
|
loading.value = true;
|
|
try {
|
|
item.value = await PointAPI.redemptionDetail(id);
|
|
customer.value = item.value.customer;
|
|
if (!customer.value && item.value.customerId) {
|
|
try {
|
|
customer.value = await CustomerAPI.detail(item.value.customerId);
|
|
} catch {
|
|
customer.value = undefined;
|
|
}
|
|
}
|
|
returnList.value = await PointAPI.redemptionReturnList(id);
|
|
} catch (error) {
|
|
Message.error(getErrorMessage(error, "兑换单加载失败"));
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
function attachments(): Array<{ tempFileId: string; attachType: "PHOTO"; name?: string }> {
|
|
return returnPhotos.value.flatMap(photo => photo.tempFileId ? [{
|
|
tempFileId: photo.tempFileId,
|
|
attachType: "PHOTO" as const
|
|
}] : []);
|
|
}
|
|
|
|
async function submitReturn(): Promise<void> {
|
|
const quantity = normalizePackageQuantity(returnQuantity.value);
|
|
const redemptionId = item.value?.id;
|
|
if (
|
|
returning.value
|
|
|| !redemptionId
|
|
|| !quantity
|
|
|| 0 < comparePackageQuantity(quantity, remaining())
|
|
|| !attachments().length
|
|
) {
|
|
Message.warning("请填写不超过剩余数量的退货数量并上传照片");
|
|
return;
|
|
}
|
|
if (!returnRequestNo.value) {
|
|
returnRequestNo.value = window.crypto.randomUUID();
|
|
}
|
|
returning.value = true;
|
|
try {
|
|
item.value = await PointAPI.redemptionReturn({
|
|
redemptionId,
|
|
requestNo: returnRequestNo.value,
|
|
quantity,
|
|
attachmentList: attachments(),
|
|
remark: returnRemark.value || null
|
|
});
|
|
customer.value = item.value.customer || customer.value;
|
|
returnList.value = await PointAPI.redemptionReturnList(redemptionId);
|
|
returnVisible.value = false;
|
|
returnQuantity.value = "";
|
|
returnRemark.value = "";
|
|
returnPhotos.value = [];
|
|
returnRequestNo.value = "";
|
|
Message.success("退货已完成");
|
|
} catch (error) {
|
|
Message.error(getErrorMessage(error, "退货失败,重试将复用同一请求号"));
|
|
} finally {
|
|
returning.value = false;
|
|
}
|
|
}
|
|
|
|
function formatPoint(value?: number): string {
|
|
return `${formatWanInteger(value ?? 0)} 分`;
|
|
}
|
|
|
|
function quantityText(value?: number | string): string {
|
|
return getPackageQuantityText(value);
|
|
}
|
|
|
|
function statusText(value?: PointRedemptionStatus): string {
|
|
return ({
|
|
COMPLETED: "已兑换",
|
|
PARTIALLY_RETURNED: "部分退货",
|
|
FULLY_RETURNED: "已退完"
|
|
} as Record<string, string>)[value || ""] || "未知";
|
|
}
|
|
|
|
function timeText(value?: number): string {
|
|
return value ? new Date(value).toLocaleString("zh-CN", { hour12: false }) : "-";
|
|
}
|
|
|
|
onMounted(load);
|
|
</script>
|
|
|
|
<style scoped lang="less">
|
|
.redemption-detail {
|
|
gap: 1rem;
|
|
display: flex;
|
|
padding: calc(var(--app-nav-height) + 1rem) 0 2rem;
|
|
flex-direction: column;
|
|
|
|
.return-button {
|
|
margin: 0 1rem;
|
|
}
|
|
|
|
.customer-profile {
|
|
margin: 0 1rem;
|
|
}
|
|
|
|
.photo-wrap {
|
|
padding: 1rem;
|
|
}
|
|
}
|
|
|
|
.return-form {
|
|
padding-bottom: 1rem;
|
|
|
|
.form {
|
|
margin: 0;
|
|
}
|
|
}
|
|
</style>
|