302 lines
8.5 KiB
Vue
302 lines
8.5 KiB
Vue
<template>
|
|
<bottom-popup
|
|
v-model="visible"
|
|
:title="getStockOperationText(operation)"
|
|
left-button-text="取消"
|
|
:right-button-text="isSaving ? '提交中' : '提交'"
|
|
@left-button-click="visible = false"
|
|
@right-button-click="submit"
|
|
>
|
|
<div class="operation-popup">
|
|
<t-form class="form" label-width="5rem">
|
|
<t-form-item
|
|
label="单位"
|
|
content-align="right"
|
|
@click="openUnitPicker"
|
|
arrow
|
|
required-mark
|
|
>
|
|
<span class="picker-text" v-text="selectedUnitName" />
|
|
</t-form-item>
|
|
<t-form-item label="数量" required-mark>
|
|
<t-input v-model="quantity" type="number" align="right" borderless />
|
|
</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="备注" required-mark>
|
|
<div class="remark-wrap">
|
|
<t-textarea
|
|
v-model="remark"
|
|
borderless
|
|
maxlength="500"
|
|
autosize
|
|
placeholder="请输入库存操作备注"
|
|
/>
|
|
<t-button
|
|
v-if="quickRemarkText"
|
|
type="button"
|
|
size="extra-small"
|
|
variant="outline"
|
|
:content="quickRemarkText"
|
|
@click="applyQuickRemark"
|
|
/>
|
|
</div>
|
|
</t-form-item>
|
|
<t-form-item label-width="0">
|
|
<media-selector
|
|
:model-value="photoUrlList"
|
|
v-model:photo-items="photoItems"
|
|
:upload="uploadPhotoList"
|
|
:edit-mode="true"
|
|
title="操作照片"
|
|
required
|
|
/>
|
|
</t-form-item>
|
|
</t-form>
|
|
</div>
|
|
</bottom-popup>
|
|
<t-popup v-model="occurredAtPickerVisible" placement="bottom">
|
|
<t-date-time-picker
|
|
v-if="occurredAtPickerVisible"
|
|
v-model="occurredAtDraftText"
|
|
:start="minimumDateTimeText"
|
|
:mode="['date', 'minute']"
|
|
format="YYYY-MM-DD HH:mm"
|
|
title="库存发生时间"
|
|
@cancel="occurredAtPickerVisible = false"
|
|
@confirm="confirmOccurredAt"
|
|
/>
|
|
</t-popup>
|
|
<t-popup
|
|
v-model="unitPickerVisible"
|
|
placement="bottom"
|
|
:z-index="unitPickerZIndex"
|
|
:overlay-props="{ zIndex: unitPickerOverlayZIndex }"
|
|
>
|
|
<t-picker v-model="unitDraft" :columns="unitOptions" title="选择业务单位" @cancel="unitPickerVisible = false" @confirm="confirmUnit" />
|
|
</t-popup>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { watch } from "vue";
|
|
import { BottomPopup } from "timi-tdesign-mobile";
|
|
import { Time } from "timi-web";
|
|
import ProductAPI from "@/api/ProductAPI";
|
|
import MediaSelector from "@/components/mediaSelector/MediaSelector.vue";
|
|
import type {
|
|
Product,
|
|
ProductSku,
|
|
ProductStock,
|
|
ProductStockChangeRequest,
|
|
StockOperationType
|
|
} from "@/types/Product";
|
|
import { getErrorMessage } from "@/utils/error";
|
|
import { createProductRequestNo, getStockOperationText } from "@/utils/product";
|
|
import { Message } from "@/utils/message";
|
|
import type { PhotoItem } from "@/types/Photo";
|
|
import { uploadPhotoList } from "@/utils/photo";
|
|
|
|
type ManualStockOperation = Extract<StockOperationType, "MANUAL_IN" | "MANUAL_OUT" | "STOCKTAKE_GAIN" | "STOCKTAKE_LOSS">;
|
|
type PickerValue = string | number;
|
|
|
|
interface Props {
|
|
product: Product;
|
|
sku: ProductSku;
|
|
stock?: ProductStock;
|
|
operation: ManualStockOperation;
|
|
}
|
|
|
|
const props = defineProps<Props>();
|
|
const emit = defineEmits<{
|
|
success: [];
|
|
}>();
|
|
const visible = defineModel<boolean>("visible", { default: false });
|
|
const isSaving = ref(false);
|
|
const quantity = ref("");
|
|
const remark = ref("");
|
|
const requestNo = ref("");
|
|
const occurredAtText = ref("");
|
|
const occurredAtDraftText = ref("");
|
|
const occurredAtPickerVisible = ref(false);
|
|
const photoItems = ref<PhotoItem[]>([]);
|
|
const photoUrlList = computed(() => photoItems.value.map(item => item.url));
|
|
const selectedUnitId = ref("");
|
|
const unitPickerVisible = ref(false);
|
|
const unitDraft = ref<PickerValue[]>([]);
|
|
const unitPickerZIndex = 12001;
|
|
const unitPickerOverlayZIndex = unitPickerZIndex - 1;
|
|
const minimumDateTimeText = "1970-01-01 00:00";
|
|
|
|
const isInbound = computed(() => props.operation === "MANUAL_IN" || props.operation === "STOCKTAKE_GAIN");
|
|
const selectableUnits = computed(() => (props.sku.unitList || []).filter(unit => isInbound.value ? unit.allowPurchase : unit.allowSale));
|
|
const unitOptions = computed(() => [selectableUnits.value.map(unit => ({ label: unit.name || "未命名单位", value: unit.id || "" }))]);
|
|
const selectedUnitName = computed(() => selectableUnits.value.find(unit => unit.id === selectedUnitId.value)?.name || "选择业务单位");
|
|
const quickRemarkText = computed(() => props.operation === "MANUAL_IN" ? "正常入库" : props.operation === "MANUAL_OUT" ? "正常出库" : "");
|
|
|
|
function resetForm(): void {
|
|
quantity.value = "";
|
|
remark.value = "";
|
|
requestNo.value = "";
|
|
occurredAtText.value = "";
|
|
occurredAtDraftText.value = "";
|
|
occurredAtPickerVisible.value = false;
|
|
photoItems.value = [];
|
|
selectedUnitId.value = selectableUnits.value[0]?.id || "";
|
|
}
|
|
|
|
function openOccurredAtPicker(): void {
|
|
occurredAtDraftText.value = occurredAtText.value || Time.formatDateTime(new Date());
|
|
occurredAtPickerVisible.value = true;
|
|
}
|
|
|
|
function confirmOccurredAt(value?: string | number): void {
|
|
occurredAtText.value = String(value || occurredAtDraftText.value);
|
|
occurredAtPickerVisible.value = false;
|
|
}
|
|
|
|
function resetOccurredAt(): void {
|
|
occurredAtText.value = "";
|
|
occurredAtDraftText.value = "";
|
|
}
|
|
|
|
function openUnitPicker(): void {
|
|
if (selectableUnits.value.length) {
|
|
unitDraft.value = [selectedUnitId.value];
|
|
unitPickerVisible.value = true;
|
|
}
|
|
}
|
|
|
|
function confirmUnit(value: Array<string | number>): void {
|
|
selectedUnitId.value = String(value[0] || "");
|
|
unitPickerVisible.value = false;
|
|
}
|
|
|
|
function applyQuickRemark(): void {
|
|
if (quickRemarkText.value) {
|
|
remark.value = quickRemarkText.value;
|
|
}
|
|
}
|
|
|
|
function validate(): boolean {
|
|
if (!selectedUnitId.value) {
|
|
Message.warning("请选择业务单位");
|
|
return false;
|
|
}
|
|
if (!/^\d+(?:\.\d{1,3})?$/.test(quantity.value) || !(0 < Number(quantity.value))) {
|
|
Message.warning("数量必须是大于 0 的数字,最多三位小数");
|
|
return false;
|
|
}
|
|
if (!props.sku.allowDecimalQuantity && !/^\d+$/.test(quantity.value)) {
|
|
Message.warning("该产品不允许输入小数数量");
|
|
return false;
|
|
}
|
|
if (!remark.value.trim()) {
|
|
Message.warning("备注不能为空");
|
|
return false;
|
|
}
|
|
if (!photoItems.value.some(item => !!item.tempFileId)) {
|
|
Message.warning("请上传至少一张操作照片");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function buildRequest(): ProductStockChangeRequest {
|
|
return {
|
|
requestNo: requestNo.value || (requestNo.value = createProductRequestNo()),
|
|
skuId: props.sku.id || "",
|
|
skuUnitId: selectedUnitId.value,
|
|
quantity: quantity.value,
|
|
occurredAt: resolveTimestamp(occurredAtText.value),
|
|
remark: remark.value.trim(),
|
|
attachmentList: photoItems.value.flatMap(item => item.tempFileId ? [{ tempFileId: item.tempFileId, attachType: item.attachType || "PHOTO" }] : [])
|
|
};
|
|
}
|
|
|
|
function resolveTimestamp(value: string): number | undefined {
|
|
if (!value) {
|
|
return undefined;
|
|
}
|
|
const timestamp = new Date(value.replace(" ", "T")).getTime();
|
|
return Number.isFinite(timestamp) ? timestamp : undefined;
|
|
}
|
|
|
|
async function submit(): Promise<void> {
|
|
if (isSaving.value || !validate()) {
|
|
return;
|
|
}
|
|
isSaving.value = true;
|
|
try {
|
|
const request = buildRequest();
|
|
switch (props.operation) {
|
|
case "MANUAL_IN":
|
|
await ProductAPI.manualIn(request);
|
|
break;
|
|
case "MANUAL_OUT":
|
|
await ProductAPI.manualOut(request);
|
|
break;
|
|
case "STOCKTAKE_GAIN":
|
|
await ProductAPI.stocktakeGain(request);
|
|
break;
|
|
case "STOCKTAKE_LOSS":
|
|
await ProductAPI.stocktakeLoss(request);
|
|
break;
|
|
}
|
|
visible.value = false;
|
|
Message.success("库存操作已提交");
|
|
emit("success");
|
|
} catch (error) {
|
|
Message.error(getErrorMessage(error, "库存操作失败,请确认请求号后重试"));
|
|
} finally {
|
|
isSaving.value = false;
|
|
}
|
|
}
|
|
|
|
watch(() => [visible.value, props.operation] as const, ([isVisible]) => {
|
|
if (isVisible) {
|
|
resetForm();
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<style scoped lang="less">
|
|
.operation-popup {
|
|
padding-bottom: calc(1rem + env(safe-area-inset-bottom, 0));
|
|
|
|
.form {
|
|
margin: 0;
|
|
}
|
|
|
|
.remark-wrap {
|
|
width: 100%;
|
|
display: flex;
|
|
align-items: start;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.time-actions {
|
|
gap: .5rem;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: flex-end;
|
|
}
|
|
}
|
|
</style>
|