add sku filter
This commit is contained in:
@@ -31,13 +31,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</t-input>
|
</t-input>
|
||||||
<t-dropdown-menu class="menus">
|
<t-dropdown-menu ref="filterMenuRef" class="menus">
|
||||||
<t-dropdown-item
|
<t-dropdown-item
|
||||||
v-model="dateFilterType"
|
v-model="dateFilterType"
|
||||||
:label="dateFilterLabel"
|
:label="dateFilterLabel"
|
||||||
:options="dateOptions"
|
:options="dateOptions"
|
||||||
@change="selectDateFilter"
|
@change="selectDateFilter"
|
||||||
/>
|
/>
|
||||||
|
<t-dropdown-item
|
||||||
|
:label="skuFilterLabel"
|
||||||
|
:options="[]"
|
||||||
|
@open="openSkuFilter"
|
||||||
|
/>
|
||||||
<t-dropdown-item
|
<t-dropdown-item
|
||||||
v-model="status"
|
v-model="status"
|
||||||
label="履约状态"
|
label="履约状态"
|
||||||
@@ -53,7 +58,8 @@
|
|||||||
@scrolltolower="loadMore"
|
@scrolltolower="loadMore"
|
||||||
>
|
>
|
||||||
<div class="list-wrap">
|
<div class="list-wrap">
|
||||||
<t-cell-group v-if="salesOrder" class="order-info" theme="card">
|
<t-cell-group v-if="salesOrder || selectedSkuOptionList.length" class="filter-info" theme="card">
|
||||||
|
<template v-if="salesOrder">
|
||||||
<t-cell title="订单号" :note="salesOrder.orderNo || salesOrder.id || '未知订单'" />
|
<t-cell title="订单号" :note="salesOrder.orderNo || salesOrder.id || '未知订单'" />
|
||||||
<t-cell title="订单类型" :note="getSalesOrderTypeText(salesOrder.orderType)" />
|
<t-cell title="订单类型" :note="getSalesOrderTypeText(salesOrder.orderType)" />
|
||||||
<t-cell title="订单状态">
|
<t-cell title="订单状态">
|
||||||
@@ -65,6 +71,21 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</t-cell>
|
</t-cell>
|
||||||
|
</template>
|
||||||
|
<t-cell v-if="selectedSkuOptionList.length" class="sku-filter-cell" title="已筛选 SKU">
|
||||||
|
<template #note>
|
||||||
|
<div class="selected-sku-list">
|
||||||
|
<t-tag
|
||||||
|
v-for="sku in selectedSkuOptionList"
|
||||||
|
:key="sku.skuId"
|
||||||
|
size="small"
|
||||||
|
theme="primary"
|
||||||
|
variant="outline"
|
||||||
|
:content="formatSelectedSkuName(sku.productName, sku.skuName)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</t-cell>
|
||||||
</t-cell-group>
|
</t-cell-group>
|
||||||
<t-cell-group v-if="isCustomDate" class="date-summary" theme="card">
|
<t-cell-group v-if="isCustomDate" class="date-summary" theme="card">
|
||||||
<t-cell
|
<t-cell
|
||||||
@@ -96,6 +117,12 @@
|
|||||||
</t-pull-down-refresh>
|
</t-pull-down-refresh>
|
||||||
</div>
|
</div>
|
||||||
<nav-bar-action-menu v-if="canCreate" :actions="actions" />
|
<nav-bar-action-menu v-if="canCreate" :actions="actions" />
|
||||||
|
<sku-filter-popup
|
||||||
|
v-model="skuFilterVisible"
|
||||||
|
:sku-id-list="selectedSkuIdList"
|
||||||
|
@options-loaded="handleSkuFilterOptionsLoaded"
|
||||||
|
@confirm="confirmSkuFilter"
|
||||||
|
/>
|
||||||
<customer-selector
|
<customer-selector
|
||||||
v-model="customerId"
|
v-model="customerId"
|
||||||
v-model:visible="customerSelectorVisible"
|
v-model:visible="customerSelectorVisible"
|
||||||
@@ -137,6 +164,7 @@ import SalesOrderAPI from "@/api/SalesOrderAPI";
|
|||||||
import CustomerSelector from "@/components/CustomerSelector.vue";
|
import CustomerSelector from "@/components/CustomerSelector.vue";
|
||||||
import NavBarActionMenu from "@/components/NavBarActionMenu.vue";
|
import NavBarActionMenu from "@/components/NavBarActionMenu.vue";
|
||||||
import OrderFulfillmentListCell from "@/pages/fulfillment/components/OrderFulfillmentListCell.vue";
|
import OrderFulfillmentListCell from "@/pages/fulfillment/components/OrderFulfillmentListCell.vue";
|
||||||
|
import SkuFilterPopup from "@/pages/fulfillment/components/SkuFilterPopup.vue";
|
||||||
import { useNavBarStore } from "@/store/navBarStore";
|
import { useNavBarStore } from "@/store/navBarStore";
|
||||||
import type { GaoFulfillmentStatus, OrderFulfillment } from "@/types/Fulfillment";
|
import type { GaoFulfillmentStatus, OrderFulfillment } from "@/types/Fulfillment";
|
||||||
import type { NavBarAction } from "@/types/NavBarAction";
|
import type { NavBarAction } from "@/types/NavBarAction";
|
||||||
@@ -152,6 +180,7 @@ defineOptions({ name: "OrderFulfillmentList" });
|
|||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const filterMenuRef = ref<{ collapseMenu: () => void }>();
|
||||||
const navBarStore = useNavBarStore();
|
const navBarStore = useNavBarStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const canCreate = computed(() => userStore.hasPermissions(permissionCodes.orderFulfillment.create));
|
const canCreate = computed(() => userStore.hasPermissions(permissionCodes.orderFulfillment.create));
|
||||||
@@ -172,6 +201,7 @@ const actions = computed<NavBarAction[]>(() => [
|
|||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
type DateFilterType = "today" | "yesterday" | "thisWeek" | "thisMonth" | "lastMonth" | "all" | "custom";
|
type DateFilterType = "today" | "yesterday" | "thisWeek" | "thisMonth" | "lastMonth" | "all" | "custom";
|
||||||
|
type SkuFilterOption = { skuId: string; productName: string; skuName: string };
|
||||||
const dateOptions: Array<{ label: string; value: DateFilterType }> = [
|
const dateOptions: Array<{ label: string; value: DateFilterType }> = [
|
||||||
{ label: "今天", value: "today" },
|
{ label: "今天", value: "today" },
|
||||||
{ label: "昨天", value: "yesterday" },
|
{ label: "昨天", value: "yesterday" },
|
||||||
@@ -183,11 +213,14 @@ const dateOptions: Array<{ label: string; value: DateFilterType }> = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const customerId = ref(getQueryText(route.query.customerId));
|
const customerId = ref(getQueryText(route.query.customerId));
|
||||||
|
const selectedSkuIdList = ref(getSkuIdList(route.query.skuIds));
|
||||||
const selectedCustomer = ref<Customer>();
|
const selectedCustomer = ref<Customer>();
|
||||||
const salesOrder = ref<SalesOrder>();
|
const salesOrder = ref<SalesOrder>();
|
||||||
|
const skuFilterOptionList = ref<SkuFilterOption[]>([]);
|
||||||
|
const skuFilterVisible = ref(false);
|
||||||
const customerSelectorVisible = ref(false);
|
const customerSelectorVisible = ref(false);
|
||||||
const customerSearchKeyword = ref("");
|
const customerSearchKeyword = ref("");
|
||||||
const dateFilterType = ref<DateFilterType>(parseDateFilterType(getQueryText(route.query.dateType), dateOptions, "today"));
|
const dateFilterType = ref<DateFilterType>(parseDateFilterType(getQueryText(route.query.dateType), dateOptions, "all"));
|
||||||
const dateRange = ref<[string, string]>(parseDateRange(dateFilterType.value));
|
const dateRange = ref<[string, string]>(parseDateRange(dateFilterType.value));
|
||||||
const startDraft = ref("");
|
const startDraft = ref("");
|
||||||
const endDraft = ref("");
|
const endDraft = ref("");
|
||||||
@@ -211,12 +244,27 @@ const statusOptions = [
|
|||||||
{ label: "已冲正", value: "REVOKED" }
|
{ label: "已冲正", value: "REVOKED" }
|
||||||
];
|
];
|
||||||
const dateFilterLabel = computed(() => dateOptions.find(item => item.value === dateFilterType.value)?.label || "时间范围");
|
const dateFilterLabel = computed(() => dateOptions.find(item => item.value === dateFilterType.value)?.label || "时间范围");
|
||||||
|
const selectedSkuOptionList = computed(() => {
|
||||||
|
const optionMap = new Map(skuFilterOptionList.value.map(option => [option.skuId, option]));
|
||||||
|
return selectedSkuIdList.value
|
||||||
|
.map(skuId => optionMap.get(skuId))
|
||||||
|
.filter((option): option is SkuFilterOption => !!option);
|
||||||
|
});
|
||||||
|
const skuFilterLabel = computed(() => selectedSkuIdList.value.length ? `SKU (${selectedSkuIdList.value.length})` : "产品 SKU");
|
||||||
const isCustomDate = computed(() => dateFilterType.value === "custom");
|
const isCustomDate = computed(() => dateFilterType.value === "custom");
|
||||||
|
|
||||||
function getQueryText(value: unknown): string {
|
function getQueryText(value: unknown): string {
|
||||||
return Array.isArray(value) ? "" : typeof value === "string" ? value : "";
|
return Array.isArray(value) ? "" : typeof value === "string" ? value : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSkuIdList(value: unknown): string[] {
|
||||||
|
const queryValueList = Array.isArray(value) ? value : [value];
|
||||||
|
return [...new Set(queryValueList
|
||||||
|
.flatMap(item => typeof item === "string" ? item.split(",") : [])
|
||||||
|
.map(item => item.trim())
|
||||||
|
.filter(Boolean))];
|
||||||
|
}
|
||||||
|
|
||||||
function getInitialStatus(value: unknown): "ALL" | GaoFulfillmentStatus {
|
function getInitialStatus(value: unknown): "ALL" | GaoFulfillmentStatus {
|
||||||
const statusValue = getQueryText(value);
|
const statusValue = getQueryText(value);
|
||||||
if (["WAITING_STOCK", "PENDING", "VERIFIED", "REJECTED", "CANCELLED", "REVOKED"].includes(statusValue)) {
|
if (["WAITING_STOCK", "PENDING", "VERIFIED", "REJECTED", "CANCELLED", "REVOKED"].includes(statusValue)) {
|
||||||
@@ -250,6 +298,7 @@ async function fetchList(reset: boolean): Promise<void> {
|
|||||||
size: pageSize,
|
size: pageSize,
|
||||||
salesOrderId: getQueryText(route.query.salesOrderId) || undefined,
|
salesOrderId: getQueryText(route.query.salesOrderId) || undefined,
|
||||||
customerId: customerId.value || undefined,
|
customerId: customerId.value || undefined,
|
||||||
|
skuIdList: selectedSkuIdList.value.length ? selectedSkuIdList.value : undefined,
|
||||||
beginAt,
|
beginAt,
|
||||||
endAt,
|
endAt,
|
||||||
status: status.value === "ALL" ? undefined : status.value
|
status: status.value === "ALL" ? undefined : status.value
|
||||||
@@ -297,6 +346,29 @@ async function selectStatus(value: DropdownValue): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function openSkuFilter(): Promise<void> {
|
||||||
|
skuFilterVisible.value = true;
|
||||||
|
await nextTick();
|
||||||
|
filterMenuRef.value?.collapseMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSkuFilterOptionsLoaded(optionList: SkuFilterOption[]): void {
|
||||||
|
skuFilterOptionList.value = optionList;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmSkuFilter(skuIdList: string[]): Promise<void> {
|
||||||
|
selectedSkuIdList.value = skuIdList;
|
||||||
|
await replaceFilterQuery();
|
||||||
|
await reloadList();
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSelectedSkuName(productName: string, skuName: string) {
|
||||||
|
if (productName === skuName) {
|
||||||
|
return productName;
|
||||||
|
}
|
||||||
|
return `${productName}(${skuName})`;
|
||||||
|
}
|
||||||
|
|
||||||
async function handleCustomerChange(value: string | string[]): Promise<void> {
|
async function handleCustomerChange(value: string | string[]): Promise<void> {
|
||||||
customerId.value = Array.isArray(value) ? String(value[0] || "") : value;
|
customerId.value = Array.isArray(value) ? String(value[0] || "") : value;
|
||||||
try {
|
try {
|
||||||
@@ -417,6 +489,7 @@ function buildFilterQuery(transitionDepth = getQueryText(route.query.transitionD
|
|||||||
const query = {
|
const query = {
|
||||||
salesOrderId: getQueryText(route.query.salesOrderId),
|
salesOrderId: getQueryText(route.query.salesOrderId),
|
||||||
customerId: customerId.value,
|
customerId: customerId.value,
|
||||||
|
skuIds: selectedSkuIdList.value.join(","),
|
||||||
dateType: dateFilterType.value,
|
dateType: dateFilterType.value,
|
||||||
beginAt: dateRange.value[0] || "",
|
beginAt: dateRange.value[0] || "",
|
||||||
endAt: dateRange.value[1] || "",
|
endAt: dateRange.value[1] || "",
|
||||||
@@ -541,7 +614,7 @@ onUnmounted(() => navBarStore.clearRight());
|
|||||||
--td-spacer-1: 6px;
|
--td-spacer-1: 6px;
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 1px solid rgba(148, 163, 184, .18);
|
border: 1px solid var(--td-component-border);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin-top: -1rem;
|
margin-top: -1rem;
|
||||||
border-top: none;
|
border-top: none;
|
||||||
@@ -556,6 +629,42 @@ onUnmounted(() => navBarStore.clearRight());
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
|
.filter-info {
|
||||||
|
.selected-sku-list {
|
||||||
|
gap: .4rem;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
justify-content: flex-start;
|
||||||
|
white-space: nowrap;
|
||||||
|
touch-action: pan-x;
|
||||||
|
scrollbar-width: none;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.t-tag) {
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sku-filter-cell {
|
||||||
|
:deep(.t-cell__title) {
|
||||||
|
flex: 0 0 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.t-cell__note) {
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.date-summary {
|
.date-summary {
|
||||||
|
|
||||||
:deep(.t-cell__title) {
|
:deep(.t-cell__title) {
|
||||||
|
|||||||
@@ -0,0 +1,367 @@
|
|||||||
|
<template>
|
||||||
|
<bottom-popup
|
||||||
|
v-model="popupVisible"
|
||||||
|
title="筛选产品 SKU"
|
||||||
|
height="min(80vh, 48rem)"
|
||||||
|
left-button-text="重置"
|
||||||
|
right-button-text="确定"
|
||||||
|
@left-button-click="resetSelection"
|
||||||
|
@right-button-click="confirmSelection"
|
||||||
|
>
|
||||||
|
<div class="sku-filter-popup">
|
||||||
|
<div class="filters glass">
|
||||||
|
<t-input
|
||||||
|
class="search"
|
||||||
|
v-model="keyword"
|
||||||
|
borderless
|
||||||
|
clearable
|
||||||
|
placeholder="搜索产品名称、拼音、编码或 SKU"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-if="singleSkuProductList.length" class="single-list">
|
||||||
|
<t-cell-group theme="card">
|
||||||
|
<t-cell
|
||||||
|
class="sku-cell"
|
||||||
|
v-for="product in singleSkuProductList"
|
||||||
|
:key="product.id"
|
||||||
|
@click="toggleSkuSelected(getSkuId(getProductSkuList(product)[0]))"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
<div @click.stop>
|
||||||
|
<t-checkbox
|
||||||
|
:model-value="draftSkuIdList.includes(getSkuId(getProductSkuList(product)[0]))"
|
||||||
|
icon="line"
|
||||||
|
:block="false"
|
||||||
|
borderless
|
||||||
|
@update:model-value="checked => setSkuSelected(getSkuId(getProductSkuList(product)[0]), checked)"
|
||||||
|
>
|
||||||
|
<span v-text="product.name || '未命名产品'" />
|
||||||
|
</t-checkbox>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</t-cell>
|
||||||
|
</t-cell-group>
|
||||||
|
</div>
|
||||||
|
<t-collapse
|
||||||
|
v-if="multiSkuProductList.length"
|
||||||
|
v-model="expandedProductIdList"
|
||||||
|
class="multi-list"
|
||||||
|
:expand-mutex="false"
|
||||||
|
>
|
||||||
|
<t-collapse-panel
|
||||||
|
v-for="product in multiSkuProductList"
|
||||||
|
:key="product.id"
|
||||||
|
:value="product.id || ''"
|
||||||
|
class="collapse-panel"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="header">
|
||||||
|
<div class="group-checkbox" @click.stop>
|
||||||
|
<t-checkbox
|
||||||
|
:model-value="0 < getVisibleSelectedSkuCount(product)"
|
||||||
|
:indeterminate="isProductPartiallySelected(product)"
|
||||||
|
icon="line"
|
||||||
|
:block="false"
|
||||||
|
borderless
|
||||||
|
@update:model-value="toggleProductSkuSelection(product)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span class="clip-text" v-text="product.name || '未命名产品'" />
|
||||||
|
<span
|
||||||
|
v-if="getSelectedSkuCount(product)"
|
||||||
|
class="tips"
|
||||||
|
v-text="`已选 ${getSelectedSkuCount(product)} 个`"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<t-cell-group theme="card" class="sku-choice-list">
|
||||||
|
<t-cell
|
||||||
|
v-for="sku in getVisibleProductSkuList(product)"
|
||||||
|
:key="sku.id"
|
||||||
|
class="sku-cell"
|
||||||
|
@click="toggleSkuSelected(getSkuId(sku))"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
<div class="title">
|
||||||
|
<div @click.stop>
|
||||||
|
<t-checkbox
|
||||||
|
:model-value="draftSkuIdList.includes(getSkuId(sku))"
|
||||||
|
icon="line"
|
||||||
|
:block="false"
|
||||||
|
borderless
|
||||||
|
@update:model-value="checked => setSkuSelected(getSkuId(sku), checked)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span class="clip-text" v-text="getSkuText(sku)" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</t-cell>
|
||||||
|
</t-cell-group>
|
||||||
|
</t-collapse-panel>
|
||||||
|
</t-collapse>
|
||||||
|
<div
|
||||||
|
v-if="!singleSkuProductList.length && !multiSkuProductList.length"
|
||||||
|
class="center-tips"
|
||||||
|
v-text="loadingProducts ? '正在加载产品 SKU' : keyword ? '没有匹配的产品或 SKU' : '暂无可筛选的产品 SKU'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</bottom-popup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { BottomPopup } from "timi-tdesign-mobile";
|
||||||
|
import ProductAPI from "@/api/ProductAPI";
|
||||||
|
import type { Product, ProductSku } from "@/types/Product";
|
||||||
|
import { getErrorMessage } from "@/utils/error";
|
||||||
|
import { Message } from "@/utils/message";
|
||||||
|
|
||||||
|
defineOptions({ name: "SkuFilterPopup" });
|
||||||
|
|
||||||
|
type SkuFilterOption = { skuId: string; productName: string; skuName: string };
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: boolean;
|
||||||
|
skuIdList: string[];
|
||||||
|
}>();
|
||||||
|
const emit = defineEmits<{
|
||||||
|
"update:modelValue": [value: boolean];
|
||||||
|
"options-loaded": [optionList: SkuFilterOption[]];
|
||||||
|
confirm: [skuIdList: string[]];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const popupVisible = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: value => emit("update:modelValue", value)
|
||||||
|
});
|
||||||
|
const draftSkuIdList = ref<string[]>([...props.skuIdList]);
|
||||||
|
const keyword = ref("");
|
||||||
|
const productList = ref<Product[]>([]);
|
||||||
|
const expandedProductIdList = ref<Array<string | number>>([]);
|
||||||
|
const loadingProducts = ref(false);
|
||||||
|
const normalizedKeyword = computed(() => keyword.value.trim().toLocaleLowerCase());
|
||||||
|
const filterableProductList = computed(() => productList.value.filter(product => !!product.id && 0 < getProductSkuList(product).length));
|
||||||
|
const singleSkuProductList = computed(() => filterableProductList.value.filter(product => getProductSkuList(product).length === 1 && matchesProductSearch(product)));
|
||||||
|
const multiSkuProductList = computed(() => filterableProductList.value.filter(product => 1 < getProductSkuList(product).length && matchesProductSearch(product)));
|
||||||
|
const skuFilterOptionList = computed<SkuFilterOption[]>(() => filterableProductList.value.flatMap(product => getProductSkuList(product).map(sku => ({
|
||||||
|
skuId: getSkuId(sku),
|
||||||
|
productName: product.name || "未命名产品",
|
||||||
|
skuName: getSkuText(sku)
|
||||||
|
}))));
|
||||||
|
|
||||||
|
watch(() => props.modelValue, visible => {
|
||||||
|
if (!visible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
draftSkuIdList.value = [...props.skuIdList];
|
||||||
|
expandSelectedProducts();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => props.skuIdList, skuIdList => {
|
||||||
|
if (!props.modelValue) {
|
||||||
|
draftSkuIdList.value = [...skuIdList];
|
||||||
|
}
|
||||||
|
}, { deep: true });
|
||||||
|
|
||||||
|
watch(normalizedKeyword, value => {
|
||||||
|
if (value) {
|
||||||
|
expandedProductIdList.value = multiSkuProductList.value.map(product => product.id || "");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function getProductSkuList(product: Product): ProductSku[] {
|
||||||
|
return (product.skuList || []).filter((sku): sku is ProductSku & { id: string } => !!sku.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSkuId(sku?: ProductSku): string {
|
||||||
|
return sku?.id || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSkuText(sku?: ProductSku): string {
|
||||||
|
return sku?.specSummary || sku?.name || "默认 SKU";
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesSearchText(value?: string | null): boolean {
|
||||||
|
return !!normalizedKeyword.value && !!value?.toLocaleLowerCase().includes(normalizedKeyword.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesProductName(product: Product): boolean {
|
||||||
|
return [
|
||||||
|
product.name,
|
||||||
|
product.namePinyinFull,
|
||||||
|
product.namePinyinInitial,
|
||||||
|
product.namePinyinMixed,
|
||||||
|
product.code
|
||||||
|
].some(matchesSearchText);
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesSku(sku: ProductSku): boolean {
|
||||||
|
return [sku.name, sku.specSummary, sku.code, sku.barcode].some(matchesSearchText);
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesProductSearch(product: Product): boolean {
|
||||||
|
return !normalizedKeyword.value
|
||||||
|
|| matchesProductName(product)
|
||||||
|
|| getProductSkuList(product).some(matchesSku);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVisibleProductSkuList(product: Product): ProductSku[] {
|
||||||
|
const skuList = getProductSkuList(product);
|
||||||
|
if (!normalizedKeyword.value || matchesProductName(product)) {
|
||||||
|
return skuList;
|
||||||
|
}
|
||||||
|
return skuList.filter(matchesSku);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectedSkuCount(product: Product): number {
|
||||||
|
const skuIdSet = new Set(getProductSkuList(product).map(getSkuId));
|
||||||
|
return draftSkuIdList.value.filter(skuId => skuIdSet.has(skuId)).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVisibleSelectedSkuCount(product: Product): number {
|
||||||
|
const skuIdSet = new Set(getVisibleProductSkuList(product).map(getSkuId));
|
||||||
|
return draftSkuIdList.value.filter(skuId => skuIdSet.has(skuId)).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isProductPartiallySelected(product: Product): boolean {
|
||||||
|
const selectedCount = getVisibleSelectedSkuCount(product);
|
||||||
|
return 0 < selectedCount && selectedCount < getVisibleProductSkuList(product).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleProductSkuSelection(product: Product): void {
|
||||||
|
const skuIdList = getVisibleProductSkuList(product).map(getSkuId);
|
||||||
|
const skuIdSet = new Set(draftSkuIdList.value);
|
||||||
|
const shouldSelectAll = !skuIdList.every(skuId => skuIdSet.has(skuId));
|
||||||
|
for (const skuId of skuIdList) {
|
||||||
|
if (shouldSelectAll) {
|
||||||
|
skuIdSet.add(skuId);
|
||||||
|
} else {
|
||||||
|
skuIdSet.delete(skuId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
draftSkuIdList.value = [...skuIdSet];
|
||||||
|
}
|
||||||
|
|
||||||
|
function expandSelectedProducts(): void {
|
||||||
|
if (normalizedKeyword.value) {
|
||||||
|
expandedProductIdList.value = multiSkuProductList.value.map(product => product.id || "");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
expandedProductIdList.value = multiSkuProductList.value
|
||||||
|
.filter(product => getProductSkuList(product).some(sku => props.skuIdList.includes(getSkuId(sku))))
|
||||||
|
.map(product => product.id || "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSkuSelected(skuId: string, checked: boolean): void {
|
||||||
|
if (!skuId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const skuIdSet = new Set(draftSkuIdList.value);
|
||||||
|
if (checked) {
|
||||||
|
skuIdSet.add(skuId);
|
||||||
|
} else {
|
||||||
|
skuIdSet.delete(skuId);
|
||||||
|
}
|
||||||
|
draftSkuIdList.value = [...skuIdSet];
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSkuSelected(skuId: string): void {
|
||||||
|
setSkuSelected(skuId, !draftSkuIdList.value.includes(skuId));
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetSelection(): void {
|
||||||
|
draftSkuIdList.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmSelection(): void {
|
||||||
|
emit("confirm", [...new Set(draftSkuIdList.value)]);
|
||||||
|
popupVisible.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadFilterProducts(): Promise<void> {
|
||||||
|
loadingProducts.value = true;
|
||||||
|
try {
|
||||||
|
const result = await ProductAPI.list({ index: 0, size: 1000 });
|
||||||
|
productList.value = result.list || [];
|
||||||
|
emit("options-loaded", skuFilterOptionList.value);
|
||||||
|
expandSelectedProducts();
|
||||||
|
} catch (error) {
|
||||||
|
Message.error(getErrorMessage(error, "产品 SKU 筛选项加载失败"));
|
||||||
|
} finally {
|
||||||
|
loadingProducts.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadFilterProducts);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.sku-filter-popup {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
:deep(.t-cell-group) {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters {
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
padding: .5rem 1rem;
|
||||||
|
position: sticky;
|
||||||
|
|
||||||
|
.search {
|
||||||
|
border: 1px solid var(--td-component-border);
|
||||||
|
padding: .5rem 1rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.single-list,
|
||||||
|
.multi-list {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-list {
|
||||||
|
border-top: 1px solid var(--td-component-stroke);
|
||||||
|
|
||||||
|
.collapse-panel {
|
||||||
|
|
||||||
|
:deep(.t-collapse-panel__content) {
|
||||||
|
padding: 0 0 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.group-checkbox {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sku-choice-list {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sku-cell,
|
||||||
|
:deep(.t-collapse-panel__title) {
|
||||||
|
padding: .5rem;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
gap: .125rem;
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips {
|
||||||
|
color: var(--td-text-color-secondary);
|
||||||
|
font-size: .75rem;
|
||||||
|
margin-left: .5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -5,7 +5,7 @@ function meta(title: string, depth = 3): RouteRecordRaw["meta"] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
{ path: "/management/order-fulfillments", name: "OrderFulfillmentList", component: () => import("@/pages/fulfillment/OrderFulfillmentList.vue"), meta: meta("统一履约") },
|
{ path: "/management/order-fulfillments", name: "OrderFulfillmentList", component: () => import("@/pages/fulfillment/OrderFulfillmentList.vue"), meta: meta("履约记录") },
|
||||||
{ path: "/management/order-fulfillments/create", name: "OrderFulfillmentCreate", component: () => import("@/pages/fulfillment/OrderFulfillmentCreate.vue"), meta: meta("创建履约单", 4) },
|
{ path: "/management/order-fulfillments/create", name: "OrderFulfillmentCreate", component: () => import("@/pages/fulfillment/OrderFulfillmentCreate.vue"), meta: meta("创建履约单", 4) },
|
||||||
{ path: "/management/order-fulfillments/:id", name: "OrderFulfillmentDetail", component: () => import("@/pages/fulfillment/OrderFulfillmentDetail.vue"), meta: meta("履约单详情", 4) }
|
{ path: "/management/order-fulfillments/:id", name: "OrderFulfillmentDetail", component: () => import("@/pages/fulfillment/OrderFulfillmentDetail.vue"), meta: meta("履约单详情", 4) }
|
||||||
] as RouteRecordRaw[];
|
] as RouteRecordRaw[];
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export type FulfillmentPage = {
|
|||||||
storeId?: string;
|
storeId?: string;
|
||||||
salesOrderId?: string;
|
salesOrderId?: string;
|
||||||
customerId?: string;
|
customerId?: string;
|
||||||
|
skuIdList?: string[];
|
||||||
status?: GaoFulfillmentStatus;
|
status?: GaoFulfillmentStatus;
|
||||||
beginAt?: number;
|
beginAt?: number;
|
||||||
endAt?: number;
|
endAt?: number;
|
||||||
|
|||||||
@@ -82,6 +82,9 @@ export type Product = {
|
|||||||
categoryId?: string;
|
categoryId?: string;
|
||||||
code: string;
|
code: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
namePinyinFull?: string;
|
||||||
|
namePinyinInitial?: string;
|
||||||
|
namePinyinMixed?: string;
|
||||||
productType: ProductType;
|
productType: ProductType;
|
||||||
stockEnabled: boolean;
|
stockEnabled: boolean;
|
||||||
status?: ProductStatus;
|
status?: ProductStatus;
|
||||||
|
|||||||
Reference in New Issue
Block a user