Files
gao-customer-h5/src/pages/package/CustomerPackageDelivery.vue
T

243 lines
8.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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">
<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 {
canCreatePackageDelivery,
comparePackageQuantity,
createPackageRequestNo,
getPackageQuantityText,
getPaymentStatusText,
isStoreManagerRequiredForPackageDelivery,
normalizePackageQuantity
} from "@/utils/package";
import { getErrorMessage } from "@/utils/error";
import { Message } from "@/utils/message";
import { gaoRoleCodes, 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);
const isStoreManager = computed(() => userStore.hasRoles(gaoRoleCodes.storeManager) && !userStore.hasRoles(gaoRoleCodes.globalManager));
const canCreateDelivery = computed(() => canCreatePackageDelivery(customerPackage.value)
&& (!isStoreManagerRequiredForPackageDelivery(customerPackage.value) || isStoreManager.value)
&& userStore.hasPermissions(permissionCodes.packageDelivery.create));
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 (!canCreatePackageDelivery(customerPackage.value)) {
Message.warning("套餐未有已核销收款,不能提交发放");
return;
}
if (isStoreManagerRequiredForPackageDelivery(customerPackage.value) && !isStoreManager.value) {
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 () => {
await loadData();
if (canCreateDelivery.value) {
navBarStore.setRightText("提交发放", "var(--td-brand-color)", save);
}
});
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 {
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>