add filter for ProductStockRecordList.vue

This commit is contained in:
Timi
2026-09-05 15:14:14 +08:00
parent eb8f4778a5
commit 52813fcce8
3 changed files with 110 additions and 10 deletions
+105 -8
View File
@@ -1,6 +1,6 @@
<template>
<div v-can="permissionCodes.productStock.read" class="product-stock-record-list">
<div class="search bg-white">
<div class="filters bg-white">
<t-input
v-model.trim="keyword"
class="keyword"
@@ -9,6 +9,10 @@
clearable
placeholder="搜索产品名称或编码"
/>
<t-dropdown-menu class="menus">
<t-dropdown-item v-model="operationType" label="操作类型" :options="operationOptions" @change="selectOperationType" />
<t-dropdown-item v-model="direction" label="出入库" :options="directionOptions" @change="selectDirection" />
</t-dropdown-menu>
</div>
<t-pull-down-refresh
v-model="isRefreshing"
@@ -34,9 +38,14 @@
<template #title>
<div class="title">
<strong v-text="getStockOperationText(record.operationType)" />
<span
v-if="record.quantity !== undefined && record.quantity !== null && record.quantity !== ''"
class="quantity"
v-text="getStockQuantityText(record)"
/>
</div>
</template>
<template #note>
<template #note>
<span
class="delta"
:class="getDeltaClass(record.onHandDelta)"
@@ -58,10 +67,11 @@
</template>
<script setup lang="ts">
import { type DropdownValue } from "tdesign-mobile-vue";
import { PassTimeLabel } from "timi-web";
import ProductAPI from "@/api/ProductAPI";
import { useNavBarStore } from "@/store/navBarStore";
import type { StockOperationType, StockRecord } from "@/types/Product";
import type { StockOperationType, StockRecord, StockRecordDirection } from "@/types/Product";
import { getErrorMessage } from "@/utils/error";
import { getProductQuantityText, getStockOperationText } from "@/utils/product";
import { Message } from "@/utils/message";
@@ -69,6 +79,9 @@ import { permissionCodes } from "@/utils/permission";
defineOptions({ name: "ProductStockRecordList" });
type OperationFilter = "ALL" | StockOperationType;
type DirectionFilter = "ALL" | StockRecordDirection;
const pageSize = 15;
const route = useRoute();
const router = useRouter();
@@ -79,7 +92,9 @@ const total = ref(0);
const loading = ref(false);
const isRefreshing = ref(false);
const noMore = ref(false);
const productId = computed(() => String(route.query.productId || ""));
const productId = computed(() => getQueryText("productId"));
const operationType = ref<OperationFilter>(getQueryText("operationType") as OperationFilter || "ALL");
const direction = ref<DirectionFilter>(getQueryText("direction") as DirectionFilter || "ALL");
const keyword = ref(getQueryText("keyword"));
let keywordTimer: number | undefined;
let requestToken = 0;
@@ -87,9 +102,27 @@ const emptyText = computed(() => {
if (loading.value) {
return "正在加载库存流水";
}
return keyword.value.trim() ? "未找到匹配的库存流水" : "暂无库存流水";
return keyword.value.trim() || productId.value || operationType.value !== "ALL" || direction.value !== "ALL" ? "未找到匹配的库存流水" : "暂无库存流水";
});
const loadStateText = computed(() => loading.value ? "正在加载" : noMore.value ? "已经到底了" : "上拉加载更多");
const operationOptions = [
{ label: "全部操作", value: "ALL" },
{ label: "期初入库", value: "INITIAL_IN" },
{ label: "手工入库", value: "MANUAL_IN" },
{ label: "手工出库", value: "MANUAL_OUT" },
{ label: "手工流水冲正", value: "MANUAL_REVOKE" },
{ label: "履约预占", value: "ORDER_RESERVE" },
{ label: "履约释放预占", value: "ORDER_RELEASE" },
{ label: "履约出库", value: "ORDER_OUT" },
{ label: "履约冲正入库", value: "ORDER_REVOKE" },
{ label: "盘盈", value: "STOCKTAKE_GAIN" },
{ label: "盘亏", value: "STOCKTAKE_LOSS" }
];
const directionOptions = [
{ label: "全部流水", value: "ALL" },
{ label: "入库", value: "IN" },
{ label: "出库", value: "OUT" }
];
function getQueryText(key: string): string {
const value = route.query[key];
@@ -101,6 +134,7 @@ function getOperationIcon(type?: StockOperationType): string {
INITIAL_IN: "import",
MANUAL_IN: "import",
MANUAL_OUT: "export",
MANUAL_REVOKE: "rollback",
ORDER_RESERVE: "lock-on",
ORDER_RELEASE: "lock-off",
ORDER_OUT: "task",
@@ -110,6 +144,14 @@ function getOperationIcon(type?: StockOperationType): string {
} as Record<string, string>)[type || ""] || "history";
}
function getStockQuantityText(record: StockRecord): string {
if (record.quantity === undefined || record.quantity === null || record.quantity === "") {
return "";
}
const unitText = record.unitSnapshot ? ` ${record.unitSnapshot}` : "";
return `${getProductQuantityText(record.quantity)}${unitText}`;
}
function getDeltaText(value?: number | string): string {
const amount = Number(value || 0);
const text = String(value || "0").replace(/^-/, "");
@@ -152,6 +194,8 @@ async function loadMore(force = false): Promise<void> {
const result = await ProductAPI.recordList({
productId: productId.value || undefined,
keyword: keyword.value.trim() || undefined,
operationType: operationType.value === "ALL" ? undefined : operationType.value,
direction: direction.value === "ALL" ? undefined : direction.value,
index: pageIndex.value,
size: pageSize
});
@@ -170,6 +214,20 @@ async function loadMore(force = false): Promise<void> {
}
}
async function selectOperationType(value: DropdownValue): Promise<void> {
if (!Array.isArray(value)) {
operationType.value = String(value) as OperationFilter;
await reload();
}
}
async function selectDirection(value: DropdownValue): Promise<void> {
if (!Array.isArray(value)) {
direction.value = String(value) as DirectionFilter;
await reload();
}
}
async function reload(): Promise<void> {
pageIndex.value = 0;
recordList.value = [];
@@ -222,20 +280,53 @@ onUnmounted(() => {
padding: var(--app-nav-height) 0 1rem;
flex-direction: column;
.search {
.filters {
top: var(--app-nav-height);
left: 0;
right: 0;
z-index: 10;
display: flex;
padding: .75rem 1rem;
position: sticky;
background: var(--td-bg-color-page);
background: #F5F5F8;
box-shadow: var(--td-shadow-1);
flex-direction: column;
.keyword {
--td-input-vertical-padding: .25rem 1rem;
flex: 1;
border: 1px solid var(--td-component-border);
padding: .5rem 1rem;
z-index: 2;
background: var(--td-bg-color-container);
border-radius: 1rem;
}
.menus {
--td-spacer-1: 6px;
width: 100%;
border: 1px solid rgba(148, 163, 184, .18);
overflow: hidden;
margin-top: -1rem;
background: #fff;
border-top: none;
padding-top: .8rem;
border-radius: 0 0 1rem 1rem;
&:after {
display: none;
}
:deep(.t-dropdown-menu__item) {
min-width: 0;
}
:deep(.t-dropdown-menu__title) {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}
.list {
@@ -257,6 +348,12 @@ onUnmounted(() => {
gap: .5rem;
display: flex;
align-items: center;
.quantity {
color: var(--td-text-color-secondary);
font-size: .875rem;
font-weight: 400;
}
}
.delta {
+2 -2
View File
@@ -83,8 +83,8 @@ const menuGroupList = computed<MenuGroup[]>(() => [
title: "交易管理",
items: [
{ icon: "order", path: "/management/sales-orders", title: "销售订单", visible: userStore.hasPermissions(permissionCodes.salesOrder.read) },
{ icon: "wallet", path: "/management/sales-orders?view=pending-receive", title: "待收款", visible: userStore.hasPermissions(permissionCodes.salesOrder.read) },
{ icon: "discount", path: "/management/sales-orders?view=pending-refund", title: "待退款", visible: userStore.hasPermissions(permissionCodes.salesOrder.read) },
{ icon: "wallet", path: "/management/sales-orders?pendingReceive=true", title: "待收款", visible: userStore.hasPermissions(permissionCodes.salesOrder.read) },
{ icon: "discount", path: "/management/sales-orders?status=CANCELLED&refundStatus=PENDING", title: "待退款", visible: userStore.hasPermissions(permissionCodes.salesOrder.read) },
{ icon: "wallet", path: "/management/fund-records", title: "资金流水", visible: userStore.hasPermissions(permissionCodes.fundRecord.read) },
{ icon: "task", path: "/management/order-fulfillments", title: "统一履约", visible: userStore.hasPermissions(permissionCodes.orderFulfillment.read) }
]