243 lines
8.0 KiB
Vue
243 lines
8.0 KiB
Vue
<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>
|