add package
This commit is contained in:
@@ -0,0 +1,226 @@
|
||||
<template>
|
||||
<div v-can="permissionCodes.packageDelivery.create" class="package-operation-edit">
|
||||
<t-cell-group v-if="customerPackage" title="发放前状态(以提交时后端校验为准)" theme="card" v-entrance-transition>
|
||||
<t-cell title="客户套餐" :note="customerPackage.packageNameSnapshot || '未命名套餐'" />
|
||||
<t-cell title="收款状态" :note="getPaymentStatusText(customerPackage.paymentStatus)" />
|
||||
</t-cell-group>
|
||||
<section v-if="customerPackage" class="items" v-entrance-transition="70">
|
||||
<div class="section-title"><strong>本次发放权益</strong><span>只能提交不超过可发数量的产品</span></div>
|
||||
<div v-for="item in customerPackage.itemList || []" :key="item.id" class="item-card">
|
||||
<div class="item-title"><strong v-text="`${item.productNameSnapshot}(${item.unitSnapshot})`" /><span v-text="`可发 ${getPackageQuantityText(item.availableQuantity)}`" /></div>
|
||||
<t-input v-model.trim="quantityMap[item.id || '']" type="number" borderless placeholder="本次发放数量,不填表示不发放" />
|
||||
</div>
|
||||
</section>
|
||||
<t-form v-if="customerPackage" class="form" label-align="left" label-width="6rem" v-entrance-transition="140">
|
||||
<t-form-item label="发放时间" arrow @click="deliveredPickerVisible = true">
|
||||
<span class="picker-text" v-text="deliveredAtText || '使用当前时间'" />
|
||||
</t-form-item>
|
||||
<t-form-item label="备注">
|
||||
<t-textarea v-model.trim="remark" borderless autosize maxlength="500" placeholder="发放备注" />
|
||||
</t-form-item>
|
||||
</t-form>
|
||||
<package-photo-field v-if="customerPackage" v-model="photoItems" required title="发放照片" />
|
||||
<t-empty v-if="!customerPackage" :description="isLoading ? '正在加载套餐' : '套餐不存在'" />
|
||||
<t-popup v-model="deliveredPickerVisible" placement="bottom">
|
||||
<t-date-time-picker v-model="deliveredDraft" format="YYYY-MM-DD HH:mm" :mode="['date', 'minute']" title="发放时间" @cancel="deliveredPickerVisible = false" @confirm="confirmDeliveredAt" />
|
||||
</t-popup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useUserStore } from "timi-web-api";
|
||||
import PackageAPI from "@/api/PackageAPI";
|
||||
import PackagePhotoField from "@/components/PackagePhotoField.vue";
|
||||
import { useNavBarStore } from "@/store/navBarStore";
|
||||
import type { GaoCustomerPackage, PackageAttachment } from "@/types/Package";
|
||||
import type { PhotoItem } from "@/types/Photo";
|
||||
import { comparePackageQuantity, createPackageRequestNo, getPackageQuantityText, getPaymentStatusText, normalizePackageQuantity } from "@/utils/package";
|
||||
import { getErrorMessage } from "@/utils/error";
|
||||
import { Message } from "@/utils/message";
|
||||
import { permissionCodes } from "@/utils/permission";
|
||||
|
||||
defineOptions({ name: "CustomerPackageDelivery" });
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const navBarStore = useNavBarStore();
|
||||
const customerPackage = ref<GaoCustomerPackage>();
|
||||
const quantityMap = reactive<Record<string, string>>({});
|
||||
const photoItems = ref<PhotoItem[]>([]);
|
||||
const remark = ref("");
|
||||
const deliveredAtText = ref("");
|
||||
const deliveredDraft = ref("");
|
||||
const deliveredPickerVisible = ref(false);
|
||||
const isLoading = ref(false);
|
||||
const isSaving = ref(false);
|
||||
|
||||
function confirmDeliveredAt(value: string | number): void {
|
||||
deliveredAtText.value = String(value);
|
||||
deliveredPickerVisible.value = false;
|
||||
}
|
||||
|
||||
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(): PackageAttachment[] {
|
||||
return photoItems.value.flatMap(item => item.tempFileId ? [{ tempFileId: item.tempFileId, attachType: "PHOTO", name: item.attachment?.name }] : []);
|
||||
}
|
||||
|
||||
async function loadData(): Promise<void> {
|
||||
const id = String(route.params.id || "");
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
isLoading.value = true;
|
||||
try {
|
||||
customerPackage.value = await PackageAPI.customerPackageDetail(id);
|
||||
for (const item of customerPackage.value.itemList || []) {
|
||||
if (item.id) {
|
||||
quantityMap[item.id] = "";
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
Message.error(getErrorMessage(error, "套餐加载失败"));
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function validateItems(): Array<{ customerPackageItemId: string; quantity: string }> | undefined {
|
||||
const result: Array<{ customerPackageItemId: string; quantity: string }> = [];
|
||||
for (const item of customerPackage.value?.itemList || []) {
|
||||
if (!item.id) {
|
||||
continue;
|
||||
}
|
||||
const text = quantityMap[item.id] || "";
|
||||
if (!text.trim()) {
|
||||
continue;
|
||||
}
|
||||
const quantity = normalizePackageQuantity(text);
|
||||
if (!quantity || quantity === "0") {
|
||||
Message.warning(`${item.productNameSnapshot} 的数量必须是正数,最多三位小数`);
|
||||
return undefined;
|
||||
}
|
||||
if (comparePackageQuantity(quantity, item.availableQuantity) > 0) {
|
||||
Message.warning(`${item.productNameSnapshot} 的发放数量超过当前可发数量,请刷新后重新确认`);
|
||||
return undefined;
|
||||
}
|
||||
result.push({ customerPackageItemId: item.id, quantity });
|
||||
}
|
||||
if (!result.length) {
|
||||
Message.warning("请至少填写一项本次发放数量");
|
||||
return undefined;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function save(): Promise<void> {
|
||||
if (isSaving.value) {
|
||||
return;
|
||||
}
|
||||
const customerPackageId = customerPackage.value?.id;
|
||||
const itemList = validateItems();
|
||||
if (!customerPackageId || !itemList) {
|
||||
return;
|
||||
}
|
||||
if (customerPackage.value?.paymentStatus !== "PAID") {
|
||||
Message.warning("套餐未付清,不能提交发放");
|
||||
return;
|
||||
}
|
||||
if (!photoItems.value.some(item => item.tempFileId)) {
|
||||
Message.warning("发放必须上传至少一张照片");
|
||||
return;
|
||||
}
|
||||
isSaving.value = true;
|
||||
try {
|
||||
const result = await PackageAPI.createDelivery({
|
||||
requestNo: createPackageRequestNo(),
|
||||
customerPackageId,
|
||||
deliveredAt: resolveTimestamp(deliveredAtText.value),
|
||||
itemList,
|
||||
attachmentList: buildAttachments(),
|
||||
remark: remark.value.trim() || null
|
||||
});
|
||||
Message.success("发放单已提交,等待核销");
|
||||
await PackageAPI.customerPackageDetail(customerPackageId);
|
||||
if (result.id) {
|
||||
await router.replace(`/management/package-deliveries/${result.id}`);
|
||||
return;
|
||||
}
|
||||
await router.replace(`/management/customer-packages/${customerPackageId}`);
|
||||
} catch (error) {
|
||||
Message.error(getErrorMessage(error, "发放单提交失败,请刷新后确认状态"));
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (userStore.hasPermissions(permissionCodes.packageDelivery.create)) {
|
||||
navBarStore.setRightText("提交发放", "var(--td-brand-color)", save);
|
||||
}
|
||||
await loadData();
|
||||
});
|
||||
|
||||
onUnmounted(() => navBarStore.clearRight());
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.package-operation-edit {
|
||||
gap: 1rem;
|
||||
padding: calc(var(--app-nav-height) + 1rem) 0 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.form {
|
||||
border-radius: .75rem;
|
||||
}
|
||||
|
||||
.picker-text {
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.items,
|
||||
.package-photo-field {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
color: var(--td-text-color-secondary);
|
||||
margin-bottom: .5rem;
|
||||
font-size: .8125rem;
|
||||
}
|
||||
|
||||
.section-title span {
|
||||
margin-left: .5rem;
|
||||
}
|
||||
|
||||
.item-card {
|
||||
gap: .5rem;
|
||||
padding: .75rem;
|
||||
margin-bottom: .75rem;
|
||||
display: grid;
|
||||
background: var(--td-bg-color-container);
|
||||
box-shadow: var(--td-shadow-1);
|
||||
border-radius: .75rem;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.item-title span {
|
||||
color: var(--td-brand-color);
|
||||
font-size: .75rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user