add ProductStockRecordDetail.vue
This commit is contained in:
+24
-2
@@ -1,5 +1,17 @@
|
|||||||
import { axios } from "timi-web-api";
|
import { axios } from "timi-web-api";
|
||||||
import type { Product, ProductCategory, ProductCategoryPage, ProductPage, ProductPageResult, ProductStock, ProductStockChangeRequest, ProductStockPage, StockRecord, StockRecordPage } from "@/types/Product";
|
import type {
|
||||||
|
Product,
|
||||||
|
ProductCategory,
|
||||||
|
ProductCategoryPage,
|
||||||
|
ProductPage,
|
||||||
|
ProductPageResult,
|
||||||
|
ProductStock,
|
||||||
|
ProductStockChangeRequest,
|
||||||
|
ProductStockManualRevokeRequest,
|
||||||
|
ProductStockPage,
|
||||||
|
StockRecord,
|
||||||
|
StockRecordPage
|
||||||
|
} from "@/types/Product";
|
||||||
import { getCurrentStoreId, withCurrentStoreId } from "@/utils/store";
|
import { getCurrentStoreId, withCurrentStoreId } from "@/utils/store";
|
||||||
|
|
||||||
const PRODUCT_URI = "/gao/product";
|
const PRODUCT_URI = "/gao/product";
|
||||||
@@ -71,6 +83,10 @@ async function manualOut(request: ProductStockChangeRequest): Promise<ProductSto
|
|||||||
return await axios.post(`${STOCK_URI}/manual-out`, withCurrentStoreId(request));
|
return await axios.post(`${STOCK_URI}/manual-out`, withCurrentStoreId(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function manualRevoke(request: ProductStockManualRevokeRequest): Promise<ProductStock> {
|
||||||
|
return await axios.post(`${STOCK_URI}/manual-revoke`, request);
|
||||||
|
}
|
||||||
|
|
||||||
async function stocktakeGain(request: ProductStockChangeRequest): Promise<ProductStock> {
|
async function stocktakeGain(request: ProductStockChangeRequest): Promise<ProductStock> {
|
||||||
return await axios.post(`${STOCK_URI}/stocktake-gain`, withCurrentStoreId(request));
|
return await axios.post(`${STOCK_URI}/stocktake-gain`, withCurrentStoreId(request));
|
||||||
}
|
}
|
||||||
@@ -83,6 +99,10 @@ async function recordList(page: StockRecordPage): Promise<ProductPageResult<Stoc
|
|||||||
return await axios.post(`${STOCK_URI}/record/list`, withCurrentStoreId(page));
|
return await axios.post(`${STOCK_URI}/record/list`, withCurrentStoreId(page));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function recordDetail(id: string): Promise<StockRecord> {
|
||||||
|
return await axios.post(`${STOCK_URI}/record/detail`, undefined, { params: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
categoryList,
|
categoryList,
|
||||||
categoryCreate,
|
categoryCreate,
|
||||||
@@ -100,7 +120,9 @@ export default {
|
|||||||
initialIn,
|
initialIn,
|
||||||
manualIn,
|
manualIn,
|
||||||
manualOut,
|
manualOut,
|
||||||
|
manualRevoke,
|
||||||
stocktakeGain,
|
stocktakeGain,
|
||||||
stocktakeLoss,
|
stocktakeLoss,
|
||||||
recordList
|
recordList,
|
||||||
|
recordDetail
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,307 @@
|
|||||||
|
<template>
|
||||||
|
<div v-can="permissionCodes.productStock.read" class="product-stock-record-detail">
|
||||||
|
<t-loading v-if="isLoading" class="loading" size="32px" />
|
||||||
|
<template v-else-if="record && product">
|
||||||
|
<product-profile :product="product" v-entrance-transition>
|
||||||
|
<template #tags>
|
||||||
|
<t-tag
|
||||||
|
size="small"
|
||||||
|
variant="light-outline"
|
||||||
|
:theme="getOperationTheme(record.onHandDelta)"
|
||||||
|
:content="getStockOperationText(record.operationType)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</product-profile>
|
||||||
|
<t-cell-group title="库存流水" theme="card" v-entrance-transition="70">
|
||||||
|
<t-cell title="操作类型" :note="getStockOperationText(record.operationType)" />
|
||||||
|
<t-cell title="业务数量" :note="getQuantityText(record)" />
|
||||||
|
<t-cell title="基础单位数量" :note="getProductQuantityText(record.baseQuantity)" />
|
||||||
|
<t-cell title="实际库存变动">
|
||||||
|
<template #note>
|
||||||
|
<span :class="getDeltaClass(record.onHandDelta)" v-text="getDeltaText(record.onHandDelta)" />
|
||||||
|
</template>
|
||||||
|
</t-cell>
|
||||||
|
<t-cell title="预占库存变动">
|
||||||
|
<template #note>
|
||||||
|
<span :class="getDeltaClass(record.reservedDelta)" v-text="getDeltaText(record.reservedDelta)" />
|
||||||
|
</template>
|
||||||
|
</t-cell>
|
||||||
|
<t-cell title="实际库存">
|
||||||
|
<template #note>
|
||||||
|
<span class="balance">
|
||||||
|
<span v-text="getProductQuantityText(record.onHandBefore)" />
|
||||||
|
<t-icon name="arrow-right" />
|
||||||
|
<span :class="getDeltaClass(record.onHandDelta)" v-text="getProductQuantityText(record.onHandAfter)" />
|
||||||
|
<span class="unit" v-text="getProductBaseUnitName(product) || '基础单位'" />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</t-cell>
|
||||||
|
<t-cell title="预占库存">
|
||||||
|
<template #note>
|
||||||
|
<span class="balance">
|
||||||
|
<span v-text="getProductQuantityText(record.reservedBefore)" />
|
||||||
|
<t-icon name="arrow-right" />
|
||||||
|
<span :class="getDeltaClass(record.reservedDelta)" v-text="getProductQuantityText(record.reservedAfter)" />
|
||||||
|
<span class="unit" v-text="getProductBaseUnitName(product) || '基础单位'" />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</t-cell>
|
||||||
|
<t-cell title="业务类型" :note="getBizTypeText(record.bizType)" />
|
||||||
|
<t-cell title="发生时间">
|
||||||
|
<template #note>
|
||||||
|
<pass-time-label :timestamp="record.occurredAt" underline />
|
||||||
|
</template>
|
||||||
|
</t-cell>
|
||||||
|
<t-cell title="操作人" :note="record.operator?.name || record.operatorUserId || '未知操作人'" />
|
||||||
|
<t-cell v-if="record.sourceRecordId" title="原库存流水" :note="record.sourceRecordId" arrow @click="openRelatedRecord(record.sourceRecordId)" />
|
||||||
|
<t-cell v-if="record.revokeRecordId" title="冲正流水" :note="record.revokeRecordId" arrow @click="openRelatedRecord(record.revokeRecordId)" />
|
||||||
|
<t-cell v-if="record.remark" title="备注" :note="record.remark" />
|
||||||
|
<t-cell>
|
||||||
|
<template #title>
|
||||||
|
<span></span>
|
||||||
|
</template>
|
||||||
|
<template #description>
|
||||||
|
<photo-scroller
|
||||||
|
:model-value="photoURLList"
|
||||||
|
:photo-items="photoItemList"
|
||||||
|
:show-add="false"
|
||||||
|
:show-remove="false"
|
||||||
|
:show-viewer-actions="false"
|
||||||
|
title="操作照片"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</t-cell>
|
||||||
|
</t-cell-group>
|
||||||
|
<t-button
|
||||||
|
v-if="canManualRevoke"
|
||||||
|
class="cell-button"
|
||||||
|
theme="danger"
|
||||||
|
@click="openManualRevoke"
|
||||||
|
v-entrance-transition="140"
|
||||||
|
content="手工流水冲正"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<t-empty v-else :description="isLoading ? '正在加载库存流水' : '库存流水不存在'" />
|
||||||
|
<bottom-popup
|
||||||
|
v-model="revokeVisible"
|
||||||
|
title="手工流水冲正"
|
||||||
|
left-button-text="取消"
|
||||||
|
:right-button-text="isOperating ? '提交中' : '提交'"
|
||||||
|
@left-button-click="revokeVisible = false"
|
||||||
|
@right-button-click="submitManualRevoke"
|
||||||
|
>
|
||||||
|
<div class="revoke-popup">
|
||||||
|
<p class="tip">将新增一条反向库存流水,原流水不会被修改或删除。</p>
|
||||||
|
<t-form>
|
||||||
|
<t-form-item label="冲正备注" required-mark>
|
||||||
|
<t-textarea v-model="revokeRemark" borderless autosize maxlength="500" placeholder="请输入冲正备注" />
|
||||||
|
</t-form-item>
|
||||||
|
</t-form>
|
||||||
|
<package-photo-field v-model="revokePhotoItems" required title="冲正凭证照片" />
|
||||||
|
</div>
|
||||||
|
</bottom-popup>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { BottomPopup } from "timi-tdesign-mobile";
|
||||||
|
import { PassTimeLabel } from "timi-web";
|
||||||
|
import { useUserStore, type Attachment } from "timi-web-api";
|
||||||
|
import ProductAPI from "@/api/ProductAPI";
|
||||||
|
import PhotoScroller from "@/components/PhotoScroller.vue";
|
||||||
|
import ProductProfile from "@/pages/product/component/ProductProfile.vue";
|
||||||
|
import { useNavBarStore } from "@/store/navBarStore";
|
||||||
|
import type { PhotoItem } from "@/types/Photo";
|
||||||
|
import type { Product, StockBizType, StockRecord } from "@/types/Product";
|
||||||
|
import { getAttachmentReadURL } from "@/utils/attachment";
|
||||||
|
import { getErrorMessage } from "@/utils/error";
|
||||||
|
import { createProductRequestNo, getProductBaseUnitName, getProductQuantityText, getStockOperationText } from "@/utils/product";
|
||||||
|
import { Message } from "@/utils/message";
|
||||||
|
import { permissionCodes } from "@/utils/permission";
|
||||||
|
|
||||||
|
defineOptions({ name: "ProductStockRecordDetail" });
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const navBarStore = useNavBarStore();
|
||||||
|
const router = useRouter();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const record = ref<StockRecord>();
|
||||||
|
const product = ref<Product>();
|
||||||
|
const isLoading = ref(false);
|
||||||
|
const isOperating = ref(false);
|
||||||
|
const revokeVisible = ref(false);
|
||||||
|
const revokeRemark = ref("");
|
||||||
|
const revokeRequestNo = ref("");
|
||||||
|
const revokePhotoItems = ref<PhotoItem[]>([]);
|
||||||
|
const canManualRevoke = computed(() => {
|
||||||
|
const operationType = record.value?.operationType;
|
||||||
|
return (operationType === "MANUAL_IN" || operationType === "MANUAL_OUT")
|
||||||
|
&& !record.value?.sourceRecordId
|
||||||
|
&& !record.value?.revokeRecordId
|
||||||
|
&& userStore.hasPermissions(permissionCodes.productStock.manualRevoke);
|
||||||
|
});
|
||||||
|
const photoItemList = computed<PhotoItem[]>(() => (record.value?.attachmentList || [])
|
||||||
|
.filter(item => !!item.id)
|
||||||
|
.map(item => ({
|
||||||
|
attachment: item,
|
||||||
|
attachType: item.attachType || "PHOTO",
|
||||||
|
previewUrl: getAttachmentReadURL(item.id || ""),
|
||||||
|
url: getAttachmentReadURL(item.id || "", 128)
|
||||||
|
})));
|
||||||
|
const photoURLList = computed(() => photoItemList.value.map(item => item.url));
|
||||||
|
|
||||||
|
function getOperationTheme(value?: number | string): "success" | "danger" | "default" {
|
||||||
|
const amount = Number(value || 0);
|
||||||
|
return 0 < amount ? "success" : amount < 0 ? "danger" : "default";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDeltaText(value?: number | string): string {
|
||||||
|
const amount = Number(value || 0);
|
||||||
|
const text = getProductQuantityText(String(value || "0").replace(/^-/, ""));
|
||||||
|
return `${0 < amount ? "+" : amount < 0 ? "-" : ""}${text}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDeltaClass(value?: number | string): string {
|
||||||
|
const amount = Number(value || 0);
|
||||||
|
return 0 < amount ? "positive" : amount < 0 ? "negative" : "gray";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getQuantityText(item: StockRecord): string {
|
||||||
|
return `${getProductQuantityText(item.quantity)} ${item.unitSnapshot || "业务单位"}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBizTypeText(type?: StockBizType): string {
|
||||||
|
return ({ PRODUCT_STOCK: "产品库存", ORDER_FULFILLMENT: "订单履约" } as Record<string, string>)[type || ""] || "未知";
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAttachments(): Partial<Attachment>[] {
|
||||||
|
return revokePhotoItems.value.flatMap(item => item.tempFileId ? [{ tempFileId: item.tempFileId, attachType: "PHOTO" }] : []);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openManualRevoke(): void {
|
||||||
|
if (!canManualRevoke.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
revokeRemark.value = "";
|
||||||
|
revokeRequestNo.value = "";
|
||||||
|
revokePhotoItems.value = [];
|
||||||
|
revokeVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openRelatedRecord(id?: string): Promise<void> {
|
||||||
|
if (id) {
|
||||||
|
await router.push({ name: "ProductStockRecordDetail", params: { id } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitManualRevoke(): Promise<void> {
|
||||||
|
if (!canManualRevoke.value || !record.value?.id || isOperating.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!revokeRemark.value.trim()) {
|
||||||
|
Message.warning("冲正备注不能为空");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!revokePhotoItems.value.some(item => !!item.tempFileId)) {
|
||||||
|
Message.warning("库存流水冲正必须上传至少一张照片");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isOperating.value = true;
|
||||||
|
try {
|
||||||
|
if (!revokeRequestNo.value) {
|
||||||
|
revokeRequestNo.value = createProductRequestNo();
|
||||||
|
}
|
||||||
|
await ProductAPI.manualRevoke({
|
||||||
|
requestNo: revokeRequestNo.value,
|
||||||
|
sourceRecordId: record.value.id,
|
||||||
|
remark: revokeRemark.value.trim(),
|
||||||
|
attachmentList: buildAttachments()
|
||||||
|
});
|
||||||
|
revokeVisible.value = false;
|
||||||
|
Message.success("手工库存流水已冲正");
|
||||||
|
await loadData();
|
||||||
|
} catch (error) {
|
||||||
|
Message.error(getErrorMessage(error, "库存流水冲正失败,重试会复用同一请求号"));
|
||||||
|
} finally {
|
||||||
|
isOperating.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadData(): Promise<void> {
|
||||||
|
const id = String(route.params.id || "");
|
||||||
|
if (!id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isLoading.value = true;
|
||||||
|
try {
|
||||||
|
const result = await ProductAPI.recordDetail(id);
|
||||||
|
record.value = result;
|
||||||
|
if (result.productId) {
|
||||||
|
product.value = await ProductAPI.detail(result.productId);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Message.error(getErrorMessage(error, "库存流水加载失败"));
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
navBarStore.clearRight();
|
||||||
|
await loadData();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => String(route.params.id || ""), async (id, previousId) => {
|
||||||
|
if (id && id !== previousId) {
|
||||||
|
await loadData();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => navBarStore.clearRight());
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.product-stock-record-detail {
|
||||||
|
gap: 1rem;
|
||||||
|
display: flex;
|
||||||
|
padding: calc(var(--app-nav-height) + 1rem) 0 2rem;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
margin: 3rem auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.positive {
|
||||||
|
color: var(--td-success-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.negative {
|
||||||
|
color: var(--td-error-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance {
|
||||||
|
gap: .25rem;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.arrow,
|
||||||
|
.unit {
|
||||||
|
color: var(--td-text-color-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.revoke-popup {
|
||||||
|
padding: .5rem 1rem calc(1rem + env(safe-area-inset-bottom, 0));
|
||||||
|
|
||||||
|
.tip {
|
||||||
|
margin: 0 0 .75rem;
|
||||||
|
color: var(--td-text-color-secondary);
|
||||||
|
font-size: .875rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -10,10 +10,6 @@
|
|||||||
placeholder="搜索产品名称或编码"
|
placeholder="搜索产品名称或编码"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<t-cell-group class="summary" theme="card">
|
|
||||||
<t-cell title="查询范围" :note="scopeText" />
|
|
||||||
<t-cell title="记录数量" :note="`共 ${total} 条`" />
|
|
||||||
</t-cell-group>
|
|
||||||
<t-pull-down-refresh
|
<t-pull-down-refresh
|
||||||
v-model="isRefreshing"
|
v-model="isRefreshing"
|
||||||
:refresh-timeout="30000"
|
:refresh-timeout="30000"
|
||||||
@@ -25,7 +21,10 @@
|
|||||||
v-for="(record, index) in recordList"
|
v-for="(record, index) in recordList"
|
||||||
:key="record.id || `${record.occurredAt}-${record.requestNo}-${index}`"
|
:key="record.id || `${record.occurredAt}-${record.requestNo}-${index}`"
|
||||||
class="record"
|
class="record"
|
||||||
|
:arrow="!!record.id"
|
||||||
|
:hover="!!record.id"
|
||||||
v-entrance-transition="index * 30"
|
v-entrance-transition="index * 30"
|
||||||
|
@click="toDetail(record)"
|
||||||
>
|
>
|
||||||
<template #leftIcon>
|
<template #leftIcon>
|
||||||
<span class="icon" :class="getDeltaClass(record.onHandDelta)">
|
<span class="icon" :class="getDeltaClass(record.onHandDelta)">
|
||||||
@@ -35,29 +34,31 @@
|
|||||||
<template #title>
|
<template #title>
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<strong v-text="getStockOperationText(record.operationType)" />
|
<strong v-text="getStockOperationText(record.operationType)" />
|
||||||
<span :class="getDeltaClass(record.onHandDelta)" v-text="getDeltaText(record.onHandDelta)" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #note>
|
<template #note>
|
||||||
<span v-text="record.occurredAt ? Time.toDateTime(record.occurredAt) : '时间未知'" />
|
<span
|
||||||
</template>
|
class="delta"
|
||||||
|
:class="getDeltaClass(record.onHandDelta)"
|
||||||
|
v-text="getDeltaText(record.onHandDelta)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
<template #description>
|
<template #description>
|
||||||
<div class="description">
|
<div class="description">
|
||||||
<span v-text="getQuantityText(record)" />
|
<span v-text="record.productNameSnapshot || '未命名产品'" />
|
||||||
<span v-text="getBalanceText(record)" />
|
<pass-time-label :timestamp="record.occurredAt" underline />
|
||||||
<span v-if="record.remark" class="gray" v-text="record.remark" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</t-cell>
|
</t-cell>
|
||||||
</t-cell-group>
|
</t-cell-group>
|
||||||
<div v-else class="empty" v-text="emptyText" />
|
<div v-else class="center-tips" v-text="emptyText" />
|
||||||
<div v-if="recordList.length" class="center-tips" v-text="loadStateText" />
|
<div v-if="recordList.length" class="center-tips" v-text="loadStateText" />
|
||||||
</t-pull-down-refresh>
|
</t-pull-down-refresh>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Time } from "timi-web";
|
import { PassTimeLabel } from "timi-web";
|
||||||
import ProductAPI from "@/api/ProductAPI";
|
import ProductAPI from "@/api/ProductAPI";
|
||||||
import { useNavBarStore } from "@/store/navBarStore";
|
import { useNavBarStore } from "@/store/navBarStore";
|
||||||
import type { StockOperationType, StockRecord } from "@/types/Product";
|
import type { StockOperationType, StockRecord } from "@/types/Product";
|
||||||
@@ -70,6 +71,7 @@ defineOptions({ name: "ProductStockRecordList" });
|
|||||||
|
|
||||||
const pageSize = 15;
|
const pageSize = 15;
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
const navBarStore = useNavBarStore();
|
const navBarStore = useNavBarStore();
|
||||||
const recordList = ref<StockRecord[]>([]);
|
const recordList = ref<StockRecord[]>([]);
|
||||||
const pageIndex = ref(0);
|
const pageIndex = ref(0);
|
||||||
@@ -81,12 +83,6 @@ const productId = computed(() => String(route.query.productId || ""));
|
|||||||
const keyword = ref(getQueryText("keyword"));
|
const keyword = ref(getQueryText("keyword"));
|
||||||
let keywordTimer: number | undefined;
|
let keywordTimer: number | undefined;
|
||||||
let requestToken = 0;
|
let requestToken = 0;
|
||||||
const scopeText = computed(() => {
|
|
||||||
const scope = productId.value
|
|
||||||
? `指定产品 · ${recordList.value[0]?.productNameSnapshot || "当前产品"}`
|
|
||||||
: "全部产品";
|
|
||||||
return keyword.value.trim() ? `${scope} · 搜索:${keyword.value.trim()}` : scope;
|
|
||||||
});
|
|
||||||
const emptyText = computed(() => {
|
const emptyText = computed(() => {
|
||||||
if (loading.value) {
|
if (loading.value) {
|
||||||
return "正在加载库存流水";
|
return "正在加载库存流水";
|
||||||
@@ -102,12 +98,12 @@ function getQueryText(key: string): string {
|
|||||||
|
|
||||||
function getOperationIcon(type?: StockOperationType): string {
|
function getOperationIcon(type?: StockOperationType): string {
|
||||||
return ({
|
return ({
|
||||||
INITIAL_IN: "download",
|
INITIAL_IN: "import",
|
||||||
MANUAL_IN: "add-circle",
|
MANUAL_IN: "import",
|
||||||
MANUAL_OUT: "remove-circle",
|
MANUAL_OUT: "export",
|
||||||
ORDER_RESERVE: "lock-on",
|
ORDER_RESERVE: "lock-on",
|
||||||
ORDER_RELEASE: "unlock",
|
ORDER_RELEASE: "lock-off",
|
||||||
ORDER_OUT: "remove-circle",
|
ORDER_OUT: "task",
|
||||||
ORDER_REVOKE: "rollback",
|
ORDER_REVOKE: "rollback",
|
||||||
STOCKTAKE_GAIN: "arrow-up",
|
STOCKTAKE_GAIN: "arrow-up",
|
||||||
STOCKTAKE_LOSS: "arrow-down"
|
STOCKTAKE_LOSS: "arrow-down"
|
||||||
@@ -115,9 +111,9 @@ function getOperationIcon(type?: StockOperationType): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getDeltaText(value?: number | string): string {
|
function getDeltaText(value?: number | string): string {
|
||||||
const amount = Number(value || 0);
|
const amount = Number(value || 0);
|
||||||
const text = String(value || "0");
|
const text = String(value || "0").replace(/^-/, "");
|
||||||
return `${0 < amount ? "+" : ""}${amount < 0 ? getProductQuantityText(text.slice(1)) : getProductQuantityText(text)}`;
|
return `${0 < amount ? "+" : amount < 0 ? "-" : ""}${getProductQuantityText(text)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDeltaClass(value?: number | string): string {
|
function getDeltaClass(value?: number | string): string {
|
||||||
@@ -125,13 +121,10 @@ function getDeltaClass(value?: number | string): string {
|
|||||||
return 0 < amount ? "positive" : amount < 0 ? "negative" : "gray";
|
return 0 < amount ? "positive" : amount < 0 ? "negative" : "gray";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getQuantityText(record: StockRecord): string {
|
async function toDetail(record: StockRecord): Promise<void> {
|
||||||
return `${record.productNameSnapshot || "未命名产品"} · ${getProductQuantityText(record.quantity)} ${record.unitSnapshot || "业务单位"} · 基础单位 ${getProductQuantityText(record.baseQuantity)}`;
|
if (record.id) {
|
||||||
}
|
await router.push({ name: "ProductStockRecordDetail", params: { id: record.id } });
|
||||||
|
}
|
||||||
function getBalanceText(record: StockRecord): string {
|
|
||||||
const operator = record.operator?.name || record.operatorUserId || "未知操作人";
|
|
||||||
return `库存 ${getProductQuantityText(record.onHandAfter)} · 预占 ${getProductQuantityText(record.reservedAfter)} · ${operator}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadMore(force = false): Promise<void> {
|
async function loadMore(force = false): Promise<void> {
|
||||||
@@ -230,34 +223,35 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list {
|
.list {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
|
||||||
|
|
||||||
.record {
|
.record {
|
||||||
.icon {
|
|
||||||
width: 2.25rem;
|
|
||||||
height: 2.25rem;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
border-radius: 50%;
|
|
||||||
justify-content: center;
|
|
||||||
background: var(--td-bg-color-secondarycontainer);
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
.icon {
|
||||||
gap: .5rem;
|
width: 2.25rem;
|
||||||
display: flex;
|
height: 2.25rem;
|
||||||
align-items: center;
|
display: inline-flex;
|
||||||
}
|
background: var(--td-bg-color-secondarycontainer);
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
.description {
|
.title {
|
||||||
gap: .125rem;
|
gap: .5rem;
|
||||||
display: grid;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delta {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
gap: .125rem;
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,11 +262,5 @@ onUnmounted(() => {
|
|||||||
.negative {
|
.negative {
|
||||||
color: var(--td-error-color);
|
color: var(--td-error-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty {
|
|
||||||
color: var(--td-text-color-secondary);
|
|
||||||
padding: 2rem 1rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import ProductEdit from "@/pages/product/ProductEdit.vue";
|
|||||||
import ProductList from "@/pages/product/ProductList.vue";
|
import ProductList from "@/pages/product/ProductList.vue";
|
||||||
import ProductStockDetail from "@/pages/product/ProductStockDetail.vue";
|
import ProductStockDetail from "@/pages/product/ProductStockDetail.vue";
|
||||||
import ProductStockList from "@/pages/product/ProductStockList.vue";
|
import ProductStockList from "@/pages/product/ProductStockList.vue";
|
||||||
|
import ProductStockRecordDetail from "@/pages/product/ProductStockRecordDetail.vue";
|
||||||
import ProductStockRecordList from "@/pages/product/ProductStockRecordList.vue";
|
import ProductStockRecordList from "@/pages/product/ProductStockRecordList.vue";
|
||||||
|
|
||||||
function pageMeta(title: string, depth = 3, background = "#F5F5F8"): RouteRecordRaw["meta"] {
|
function pageMeta(title: string, depth = 3, background = "#F5F5F8"): RouteRecordRaw["meta"] {
|
||||||
@@ -69,6 +70,12 @@ const productRoutes: RouteRecordRaw[] = [
|
|||||||
component: ProductStockRecordList,
|
component: ProductStockRecordList,
|
||||||
meta: pageMeta("库存流水")
|
meta: pageMeta("库存流水")
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/management/product-stock-records/:id",
|
||||||
|
name: "ProductStockRecordDetail",
|
||||||
|
component: ProductStockRecordDetail,
|
||||||
|
meta: pageMeta("库存流水详情", 4)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/management/product-stocks/:productId",
|
path: "/management/product-stocks/:productId",
|
||||||
name: "ProductStockDetail",
|
name: "ProductStockDetail",
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ export type StockRecord = {
|
|||||||
bizType?: StockBizType;
|
bizType?: StockBizType;
|
||||||
bizId?: string;
|
bizId?: string;
|
||||||
sourceRecordId?: string;
|
sourceRecordId?: string;
|
||||||
|
revokeRecordId?: string;
|
||||||
requestNo?: string;
|
requestNo?: string;
|
||||||
operatorUserId?: string;
|
operatorUserId?: string;
|
||||||
occurredAt?: number;
|
occurredAt?: number;
|
||||||
@@ -139,6 +140,13 @@ export type ProductStockChangeRequest = {
|
|||||||
attachmentList: Partial<Attachment>[];
|
attachmentList: Partial<Attachment>[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ProductStockManualRevokeRequest = {
|
||||||
|
requestNo: string;
|
||||||
|
sourceRecordId: string;
|
||||||
|
remark: string;
|
||||||
|
attachmentList: Partial<Attachment>[];
|
||||||
|
};
|
||||||
|
|
||||||
export type ProductPageResult<T> = PageResult<T> & {
|
export type ProductPageResult<T> = PageResult<T> & {
|
||||||
pages?: number;
|
pages?: number;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -57,7 +57,8 @@ export const permissionCodes = {
|
|||||||
},
|
},
|
||||||
productStock: {
|
productStock: {
|
||||||
create: "GAO:PRODUCT_STOCK:CREATE",
|
create: "GAO:PRODUCT_STOCK:CREATE",
|
||||||
read: "GAO:PRODUCT_STOCK:READ"
|
read: "GAO:PRODUCT_STOCK:READ",
|
||||||
|
manualRevoke: "GAO:PRODUCT_STOCK:MANUAL_REVOKE"
|
||||||
},
|
},
|
||||||
packageConfig: {
|
packageConfig: {
|
||||||
create: "GAO:PACKAGE_CONFIG:CREATE",
|
create: "GAO:PACKAGE_CONFIG:CREATE",
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ export function getStockOperationText(type?: StockOperationType): string {
|
|||||||
INITIAL_IN: "期初入库",
|
INITIAL_IN: "期初入库",
|
||||||
MANUAL_IN: "手工入库",
|
MANUAL_IN: "手工入库",
|
||||||
MANUAL_OUT: "手工出库",
|
MANUAL_OUT: "手工出库",
|
||||||
|
MANUAL_REVOKE: "手工流水冲正",
|
||||||
ORDER_RESERVE: "履约预占",
|
ORDER_RESERVE: "履约预占",
|
||||||
ORDER_RELEASE: "履约释放预占",
|
ORDER_RELEASE: "履约释放预占",
|
||||||
ORDER_OUT: "履约出库",
|
ORDER_OUT: "履约出库",
|
||||||
|
|||||||
Reference in New Issue
Block a user