more filter for sales order
This commit is contained in:
@@ -0,0 +1,49 @@
|
|||||||
|
/** 过滤项支持的值类型。 */
|
||||||
|
export type FilterValue = string | number | boolean;
|
||||||
|
|
||||||
|
/** 一个过滤组的选中值。 */
|
||||||
|
export type FilterValueList = FilterValue[];
|
||||||
|
|
||||||
|
/** 过滤组的选项控件类型。 */
|
||||||
|
export type FilterControl = "checkbox" | "radio";
|
||||||
|
|
||||||
|
/** 过滤弹窗的值模型。 */
|
||||||
|
export type FilterModel = Record<string, FilterValueList>;
|
||||||
|
|
||||||
|
/** 过滤项配置。 */
|
||||||
|
export interface FilterOption {
|
||||||
|
label: string;
|
||||||
|
value: FilterValue;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 过滤值变化上下文。 */
|
||||||
|
export interface FilterChangeContext {
|
||||||
|
group: FilterGroup;
|
||||||
|
groupKey: string;
|
||||||
|
value: FilterValueList;
|
||||||
|
previousValue: FilterValueList;
|
||||||
|
changedValue?: FilterValue;
|
||||||
|
checked?: boolean;
|
||||||
|
modelValue: FilterModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 过滤值变化回调返回值。数组用于替换当前组,对象用于修改一个或多个组。 */
|
||||||
|
export type FilterChangeResult = FilterValueList | Partial<FilterModel> | void;
|
||||||
|
|
||||||
|
/** 过滤值变化回调。 */
|
||||||
|
export type FilterChangeHandler = (context: FilterChangeContext) => FilterChangeResult;
|
||||||
|
|
||||||
|
/** 过滤组配置。 */
|
||||||
|
export interface FilterGroup {
|
||||||
|
key: string;
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
options: FilterOption[];
|
||||||
|
control?: FilterControl;
|
||||||
|
multiple?: boolean;
|
||||||
|
columns: number;
|
||||||
|
readonly?: boolean;
|
||||||
|
readonlyText?: string;
|
||||||
|
onChange?: FilterChangeHandler;
|
||||||
|
}
|
||||||
@@ -0,0 +1,319 @@
|
|||||||
|
<template>
|
||||||
|
<bottom-popup
|
||||||
|
v-model="popupVisible"
|
||||||
|
:title="props.title"
|
||||||
|
:height="props.height"
|
||||||
|
:left-button-text="props.leftButtonText"
|
||||||
|
:right-button-text="props.rightButtonText"
|
||||||
|
:z-index="props.zIndex"
|
||||||
|
:overlay-props="props.overlayProps"
|
||||||
|
@left-button-click="reset"
|
||||||
|
@right-button-click="confirm"
|
||||||
|
>
|
||||||
|
<div class="filter-popup bg-white">
|
||||||
|
<slot name="before" />
|
||||||
|
<section v-for="group in props.groups" :key="group.key" class="section">
|
||||||
|
<div class="title">
|
||||||
|
<strong v-text="group.title" />
|
||||||
|
<span v-if="group.description" v-text="group.description" />
|
||||||
|
</div>
|
||||||
|
<slot :name="`group-${group.key}-before`" :group="group" :value="getGroupValue(group)" />
|
||||||
|
<div v-if="group.readonly" class="fixed-filter">
|
||||||
|
<t-icon name="layers" />
|
||||||
|
<span v-text="group.readonlyText || '当前条件已固定'" />
|
||||||
|
</div>
|
||||||
|
<t-radio-group
|
||||||
|
v-else-if="group.control === 'radio' && group.options.length"
|
||||||
|
:model-value="getSingleGroupValue(group)"
|
||||||
|
class="radio-options"
|
||||||
|
:style="{ gridTemplateColumns: `repeat(${group.columns}, minmax(0, 1fr))` }"
|
||||||
|
@change="value => changeRadioGroup(group, value)"
|
||||||
|
>
|
||||||
|
<t-radio
|
||||||
|
v-for="option in group.options"
|
||||||
|
:key="String(option.value)"
|
||||||
|
class="radio-option"
|
||||||
|
:value="option.value"
|
||||||
|
:disabled="option.disabled"
|
||||||
|
:block="false"
|
||||||
|
borderless
|
||||||
|
>
|
||||||
|
<span v-text="option.label" />
|
||||||
|
</t-radio>
|
||||||
|
</t-radio-group>
|
||||||
|
<t-checkbox-group
|
||||||
|
v-else-if="group.options.length"
|
||||||
|
:model-value="getGroupValue(group)"
|
||||||
|
class="options"
|
||||||
|
:style="{ gridTemplateColumns: `repeat(${group.columns}, minmax(0, 1fr))` }"
|
||||||
|
@change="(value, context) => changeGroup(group, value, context)"
|
||||||
|
>
|
||||||
|
<t-checkbox
|
||||||
|
v-for="option in group.options"
|
||||||
|
:key="String(option.value)"
|
||||||
|
class="option"
|
||||||
|
:value="option.value"
|
||||||
|
:label="option.label"
|
||||||
|
:disabled="option.disabled"
|
||||||
|
icon="line"
|
||||||
|
borderless
|
||||||
|
/>
|
||||||
|
</t-checkbox-group>
|
||||||
|
<slot :name="`group-${group.key}`" :group="group" :value="getGroupValue(group)" />
|
||||||
|
<slot :name="`group-${group.key}-after`" :group="group" :value="getGroupValue(group)" />
|
||||||
|
</section>
|
||||||
|
<slot name="after" />
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</bottom-popup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { BottomPopup } from "timi-tdesign-mobile";
|
||||||
|
import type {
|
||||||
|
FilterChangeContext,
|
||||||
|
FilterChangeHandler,
|
||||||
|
FilterChangeResult,
|
||||||
|
FilterGroup,
|
||||||
|
FilterModel,
|
||||||
|
FilterValue,
|
||||||
|
FilterValueList
|
||||||
|
} from "./FilterPopup";
|
||||||
|
|
||||||
|
defineOptions({ name: "FilterPopup" });
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue: FilterModel;
|
||||||
|
visible: boolean;
|
||||||
|
groups: FilterGroup[];
|
||||||
|
title?: string;
|
||||||
|
height?: string | number;
|
||||||
|
leftButtonText?: string;
|
||||||
|
rightButtonText?: string;
|
||||||
|
resetValue?: FilterModel;
|
||||||
|
zIndex?: number;
|
||||||
|
overlayProps?: Record<string, unknown>;
|
||||||
|
onFilterChange?: FilterChangeHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
title: "筛选",
|
||||||
|
height: "auto",
|
||||||
|
leftButtonText: "重置",
|
||||||
|
rightButtonText: "确定"
|
||||||
|
});
|
||||||
|
const emit = defineEmits<{
|
||||||
|
"update:modelValue": [value: FilterModel];
|
||||||
|
"update:visible": [value: boolean];
|
||||||
|
change: [value: FilterModel, context: FilterChangeContext];
|
||||||
|
reset: [value: FilterModel];
|
||||||
|
confirm: [value: FilterModel];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const popupVisible = computed({
|
||||||
|
get: () => props.visible,
|
||||||
|
set: value => emit("update:visible", value)
|
||||||
|
});
|
||||||
|
|
||||||
|
function getGroupValue(group: FilterGroup): FilterValueList {
|
||||||
|
const value = props.modelValue[group.key] || [];
|
||||||
|
return group.multiple === false ? value.slice(-1) : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSingleGroupValue(group: FilterGroup): FilterValue | undefined {
|
||||||
|
return getGroupValue(group)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeRadioGroup(group: FilterGroup, value: unknown): void {
|
||||||
|
changeGroup(group, isFilterValue(value) ? [value] : [], { current: value, type: "check" });
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeGroup(group: FilterGroup, value: unknown, changeContext: unknown): void {
|
||||||
|
const previousValue = [...getGroupValue(group)];
|
||||||
|
const checkboxValue = toFilterValueList(value);
|
||||||
|
const contextValue = getChangedValue(changeContext);
|
||||||
|
const checked = getCheckedState(changeContext);
|
||||||
|
const nextValue = normalizeGroupValue(group, checkboxValue, contextValue, checked);
|
||||||
|
const nextModel = cloneFilterModel(props.modelValue);
|
||||||
|
nextModel[group.key] = nextValue;
|
||||||
|
const context: FilterChangeContext = {
|
||||||
|
group,
|
||||||
|
groupKey: group.key,
|
||||||
|
value: [...nextValue],
|
||||||
|
previousValue,
|
||||||
|
changedValue: contextValue,
|
||||||
|
checked,
|
||||||
|
modelValue: cloneFilterModel(nextModel)
|
||||||
|
};
|
||||||
|
|
||||||
|
const groupResult = group.onChange?.(context);
|
||||||
|
const groupModel = applyChangeResult(nextModel, group.key, groupResult);
|
||||||
|
const finalContext: FilterChangeContext = {
|
||||||
|
...context,
|
||||||
|
value: [...(groupModel[group.key] || [])],
|
||||||
|
modelValue: cloneFilterModel(groupModel)
|
||||||
|
};
|
||||||
|
const finalModel = applyChangeResult(groupModel, group.key, props.onFilterChange?.(finalContext));
|
||||||
|
emit("update:modelValue", finalModel);
|
||||||
|
emit("change", finalModel, finalContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeGroupValue(
|
||||||
|
group: FilterGroup,
|
||||||
|
value: FilterValueList,
|
||||||
|
changedValue?: FilterValue,
|
||||||
|
checked?: boolean
|
||||||
|
): FilterValueList {
|
||||||
|
if (group.multiple !== false || value.length < 2) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (checked !== false && changedValue !== undefined) {
|
||||||
|
return [changedValue];
|
||||||
|
}
|
||||||
|
return value.slice(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyChangeResult(model: FilterModel, groupKey: string, result: FilterChangeResult): FilterModel {
|
||||||
|
if (!result) {
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
if (Array.isArray(result)) {
|
||||||
|
return { ...model, [groupKey]: toFilterValueList(result) };
|
||||||
|
}
|
||||||
|
const nextModel = cloneFilterModel(model);
|
||||||
|
for (const [key, value] of Object.entries(result)) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
nextModel[key] = toFilterValueList(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nextModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toFilterValueList(value: unknown): FilterValueList {
|
||||||
|
if (!Array.isArray(value)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return value.filter(isFilterValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isFilterValue(value: unknown): value is FilterValue {
|
||||||
|
return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getChangedValue(value: unknown): FilterValue | undefined {
|
||||||
|
if (!value || typeof value !== "object") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const current = (value as { current?: unknown }).current;
|
||||||
|
return isFilterValue(current) ? current : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCheckedState(value: unknown): boolean | undefined {
|
||||||
|
if (!value || typeof value !== "object") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const type = (value as { type?: unknown }).type;
|
||||||
|
return type === "check" ? true : type === "uncheck" ? false : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneFilterModel(model: FilterModel): FilterModel {
|
||||||
|
return Object.fromEntries(Object.entries(model).map(([key, value]) => [key, toFilterValueList(value)]));
|
||||||
|
}
|
||||||
|
|
||||||
|
function createEmptyFilterModel(): FilterModel {
|
||||||
|
return Object.fromEntries(props.groups.map(group => [group.key, []]));
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset(): void {
|
||||||
|
const value = cloneFilterModel(props.resetValue || createEmptyFilterModel());
|
||||||
|
emit("update:modelValue", value);
|
||||||
|
emit("reset", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirm(): void {
|
||||||
|
emit("confirm", cloneFilterModel(props.modelValue));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
:global(.bottom-popup:has(.filter-popup)) {
|
||||||
|
max-height: 86vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-popup {
|
||||||
|
padding: 0 1rem 1rem;
|
||||||
|
|
||||||
|
.section {
|
||||||
|
padding: 1rem 0 0;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
margin-bottom: .75rem;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: var(--td-text-color-secondary);
|
||||||
|
font-size: .75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.options,
|
||||||
|
.radio-options {
|
||||||
|
gap: .5rem;
|
||||||
|
display: grid;
|
||||||
|
padding-bottom: .25rem;
|
||||||
|
|
||||||
|
.option,
|
||||||
|
.radio-option {
|
||||||
|
margin: 0;
|
||||||
|
border: 1px solid var(--td-component-border);
|
||||||
|
padding: .5rem;
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
background: var(--td-bg-color-container);
|
||||||
|
border-radius: .75rem;
|
||||||
|
justify-content: start;
|
||||||
|
|
||||||
|
:deep(input) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.t-checkbox__label),
|
||||||
|
:deep(.t-radio__label) {
|
||||||
|
font-size: .75rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.t-radio__icon),
|
||||||
|
:deep(.t-checkbox__icon) {
|
||||||
|
margin-right: .25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(:deep(.t-checkbox__icon--checked)),
|
||||||
|
&:has(:deep(.t-radio__icon--checked)) {
|
||||||
|
color: var(--td-brand-color);
|
||||||
|
background: var(--td-brand-color-light);
|
||||||
|
border-color: var(--td-brand-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fixed-filter {
|
||||||
|
gap: .5rem;
|
||||||
|
color: var(--td-brand-color);
|
||||||
|
display: flex;
|
||||||
|
padding: .75rem;
|
||||||
|
font-size: .8125rem;
|
||||||
|
background: var(--td-brand-color-light);
|
||||||
|
align-items: center;
|
||||||
|
border-radius: .5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
v-for="(item, index) in result.list"
|
v-for="(item, index) in result.list"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
class="item"
|
class="item"
|
||||||
:title="item.product?.name || item.sku?.name || '未命名产品'"
|
:title="item.product?.name + '长名称长名称长名称长名称长名称长名称长名称长名称' || item.sku?.name || '未命名产品'"
|
||||||
arrow
|
arrow
|
||||||
v-entrance-transition="index * 30"
|
v-entrance-transition="index * 30"
|
||||||
@click="toEdit(item)"
|
@click="toEdit(item)"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-can="permissionCodes.salesOrder.read" class="sales-order-list">
|
<div v-can="permissionCodes.salesOrder.read" class="sales-order-list">
|
||||||
<div class="filters">
|
<div class="filters">
|
||||||
<div class="search-status">
|
<div class="search-row">
|
||||||
<t-input
|
<t-input
|
||||||
v-model.trim="keyword"
|
v-model.trim="keyword"
|
||||||
class="keyword"
|
class="keyword"
|
||||||
@@ -21,36 +21,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</t-input>
|
</t-input>
|
||||||
<t-dropdown-menu class="status">
|
<t-button
|
||||||
<t-dropdown-item
|
class="filter-button"
|
||||||
v-model="status"
|
size="small"
|
||||||
label="订单状态"
|
theme="light"
|
||||||
:options="statusOptions"
|
variant="outline"
|
||||||
@change="selectStatus"
|
@click="filterVisible = true"
|
||||||
/>
|
>
|
||||||
</t-dropdown-menu>
|
<template #icon>
|
||||||
|
<t-icon name="filter-1" />
|
||||||
|
</template>
|
||||||
|
<span>筛选</span>
|
||||||
|
</t-button>
|
||||||
</div>
|
</div>
|
||||||
<t-dropdown-menu class="menus">
|
|
||||||
<t-dropdown-item
|
|
||||||
v-if="!packageConfigId"
|
|
||||||
v-model="orderType"
|
|
||||||
label="订单类型"
|
|
||||||
:options="orderTypeOptions"
|
|
||||||
@change="selectOrderType"
|
|
||||||
/>
|
|
||||||
<t-dropdown-item
|
|
||||||
v-model="paymentStatus"
|
|
||||||
label="收款状态"
|
|
||||||
:options="paymentOptions"
|
|
||||||
@change="selectPaymentStatus"
|
|
||||||
/>
|
|
||||||
<t-dropdown-item
|
|
||||||
v-model="refundStatus"
|
|
||||||
label="退款状态"
|
|
||||||
:options="refundOptions"
|
|
||||||
@change="selectRefundStatus"
|
|
||||||
/>
|
|
||||||
</t-dropdown-menu>
|
|
||||||
</div>
|
</div>
|
||||||
<t-pull-down-refresh
|
<t-pull-down-refresh
|
||||||
class="list-wrap"
|
class="list-wrap"
|
||||||
@@ -60,6 +43,11 @@
|
|||||||
@scrolltolower="loadMore"
|
@scrolltolower="loadMore"
|
||||||
>
|
>
|
||||||
<div class="items">
|
<div class="items">
|
||||||
|
<sales-order-summary
|
||||||
|
v-if="customerId"
|
||||||
|
ref="customerOrderSummaryRef"
|
||||||
|
:customer="selectedCustomer"
|
||||||
|
/>
|
||||||
<t-cell-group v-if="packageConfigId" class="selected-package" theme="card">
|
<t-cell-group v-if="packageConfigId" class="selected-package" theme="card">
|
||||||
<t-cell title="套餐" :note="selectedPackageName">
|
<t-cell title="套餐" :note="selectedPackageName">
|
||||||
<template #left-icon>
|
<template #left-icon>
|
||||||
@@ -67,15 +55,16 @@
|
|||||||
</template>
|
</template>
|
||||||
</t-cell>
|
</t-cell>
|
||||||
</t-cell-group>
|
</t-cell-group>
|
||||||
<sales-order-summary
|
<sales-order-filter-summary
|
||||||
v-if="customerId"
|
:filter="appliedFilter"
|
||||||
ref="customerOrderSummaryRef"
|
:total="total"
|
||||||
:customer="selectedCustomer"
|
:package-config-id="packageConfigId"
|
||||||
|
@confirm="confirmFilter"
|
||||||
/>
|
/>
|
||||||
<sales-order-list-cell
|
<sales-order-list-cell
|
||||||
class="item"
|
|
||||||
v-for="(item, index) in list"
|
v-for="(item, index) in list"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
|
class="item"
|
||||||
:order="item"
|
:order="item"
|
||||||
:can-receive="canReceive"
|
:can-receive="canReceive"
|
||||||
:can-fulfill="canCreateFulfillment"
|
:can-fulfill="canCreateFulfillment"
|
||||||
@@ -101,10 +90,15 @@
|
|||||||
v-model:visible="customerSelectorVisible"
|
v-model:visible="customerSelectorVisible"
|
||||||
@change="handleCustomerChange"
|
@change="handleCustomerChange"
|
||||||
/>
|
/>
|
||||||
|
<sales-order-filter-popup
|
||||||
|
v-model:visible="filterVisible"
|
||||||
|
:model-value="appliedFilter"
|
||||||
|
:package-config-id="packageConfigId"
|
||||||
|
@confirm="confirmFilter"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { type DropdownValue } from "tdesign-mobile-vue";
|
|
||||||
import { Toolkit } from "timi-web";
|
import { Toolkit } from "timi-web";
|
||||||
import { useUserStore } from "timi-web-api";
|
import { useUserStore } from "timi-web-api";
|
||||||
import CustomerAPI from "@/api/CustomerAPI";
|
import CustomerAPI from "@/api/CustomerAPI";
|
||||||
@@ -112,19 +106,40 @@ import PackageAPI from "@/api/PackageAPI";
|
|||||||
import SalesOrderAPI from "@/api/SalesOrderAPI";
|
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 SalesOrderFilterPopup from "@/pages/salesOrder/components/SalesOrderFilterPopup.vue";
|
||||||
|
import SalesOrderFilterSummary from "@/pages/salesOrder/components/SalesOrderFilterSummary.vue";
|
||||||
import SalesOrderListCell from "@/pages/salesOrder/components/SalesOrderListCell.vue";
|
import SalesOrderListCell from "@/pages/salesOrder/components/SalesOrderListCell.vue";
|
||||||
import SalesOrderSummary from "@/pages/salesOrder/components/SalesOrderSummary.vue";
|
import SalesOrderSummary from "@/pages/salesOrder/components/SalesOrderSummary.vue";
|
||||||
|
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
||||||
import { useNavBarStore } from "@/store/navBarStore";
|
import { useNavBarStore } from "@/store/navBarStore";
|
||||||
import type { Customer } from "@/types/Customer";
|
import type { Customer } from "@/types/Customer";
|
||||||
import type { GaoPackageConfig } from "@/types/Package";
|
import type { GaoPackageConfig } from "@/types/Package";
|
||||||
import type { GaoSalesOrderStatus, GaoSalesOrderType, GaoSalesPaymentStatus, GaoSalesRefundStatus, SalesOrder, SalesOrderPage } from "@/types/SalesOrder";
|
import type {
|
||||||
|
GaoSalesOrderStatus,
|
||||||
|
GaoSalesOrderType,
|
||||||
|
GaoSalesPaymentStatus,
|
||||||
|
GaoSalesRefundStatus,
|
||||||
|
SalesOrder,
|
||||||
|
SalesOrderPage
|
||||||
|
} from "@/types/SalesOrder";
|
||||||
|
import {
|
||||||
|
cloneSalesOrderFilter,
|
||||||
|
createDefaultSalesOrderFilter,
|
||||||
|
normalizeSalesOrderFilter,
|
||||||
|
type SalesOrderListFilter,
|
||||||
|
type SalesOrderPaymentFilter
|
||||||
|
} from "@/types/SalesOrderFilter";
|
||||||
|
import { buildPresetDateRange, parseDateTime } from "@/utils/date";
|
||||||
import { getErrorMessage } from "@/utils/error";
|
import { getErrorMessage } from "@/utils/error";
|
||||||
import { Message } from "@/utils/message";
|
import { Message } from "@/utils/message";
|
||||||
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
|
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
|
||||||
|
|
||||||
defineOptions({ name: "SalesOrderList" });
|
defineOptions({ name: "SalesOrderList" });
|
||||||
|
|
||||||
|
const SALES_ORDER_FILTER_STORAGE_KEY = "gao-customer-h5:sales-order-filter";
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const currentStoreStore = useCurrentStoreStore();
|
||||||
const navBarStore = useNavBarStore();
|
const navBarStore = useNavBarStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const canCreate = computed(() => userStore.hasPermissions(permissionCodes.salesOrder.create));
|
const canCreate = computed(() => userStore.hasPermissions(permissionCodes.salesOrder.create));
|
||||||
@@ -150,19 +165,46 @@ const actions = computed(() => [
|
|||||||
action: () => router.push("/management/customer-packages/create")
|
action: () => router.push("/management/customer-packages/create")
|
||||||
}] : [])
|
}] : [])
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// ---------- 顶部搜索和客户 ----------
|
||||||
|
|
||||||
const keyword = ref("");
|
const keyword = ref("");
|
||||||
const customerId = ref("");
|
const customerId = ref("");
|
||||||
const selectedCustomer = ref<Customer>();
|
const selectedCustomer = ref<Customer>();
|
||||||
const customerSelectorVisible = ref(false);
|
const customerSelectorVisible = ref(false);
|
||||||
let customerKeyword = "";
|
let customerKeyword = "";
|
||||||
const selectedPackage = ref<GaoPackageConfig>();
|
const selectedPackage = ref<GaoPackageConfig>();
|
||||||
const orderType = ref("ALL");
|
const packageConfigId = computed(() => queryValue(route.query.packageConfigId));
|
||||||
type PaymentFilter = "ALL" | GaoSalesPaymentStatus | "PENDING_RECEIVE";
|
const selectedPackageName = computed(() => selectedPackage.value?.name || queryValue(route.query.packageConfigName) || "未命名套餐");
|
||||||
type RefundFilter = "ALL" | GaoSalesRefundStatus;
|
|
||||||
|
// ---------- 筛选 ----------
|
||||||
|
|
||||||
|
const filterVisible = ref(false);
|
||||||
|
const appliedFilter = ref<SalesOrderListFilter>(createDefaultSalesOrderFilter());
|
||||||
|
const filterStorageKey = computed(() => `${SALES_ORDER_FILTER_STORAGE_KEY}:${currentStoreStore.currentStoreId || "default"}:${packageConfigId.value || "all"}`);
|
||||||
|
const orderTypeOptions: Array<{ label: string; value: GaoSalesOrderType }> = [
|
||||||
|
{ label: "套餐订单", value: "PACKAGE" },
|
||||||
|
{ label: "产品订单", value: "DIRECT_PRODUCT" }
|
||||||
|
];
|
||||||
|
const statusOptions: Array<{ label: string; value: GaoSalesOrderStatus }> = [
|
||||||
|
{ label: "进行中", value: "ACTIVE" },
|
||||||
|
{ label: "已履约", value: "FULFILLED" },
|
||||||
|
{ label: "已取消", value: "CANCELLED" }
|
||||||
|
];
|
||||||
|
const paymentOptions: Array<{ label: string; value: SalesOrderPaymentFilter }> = [
|
||||||
|
{ label: "未收清", value: "PENDING_RECEIVE" },
|
||||||
|
{ label: "未收款", value: "UNPAID" },
|
||||||
|
{ label: "部分收款", value: "PARTIALLY_PAID" },
|
||||||
|
{ label: "已收款", value: "PAID" }
|
||||||
|
];
|
||||||
|
const paymentStatusValues: GaoSalesPaymentStatus[] = ["UNPAID", "PARTIALLY_PAID", "PAID"];
|
||||||
|
const refundOptions: Array<{ label: string; value: GaoSalesRefundStatus }> = [
|
||||||
|
{ label: "无需退款", value: "NOT_REQUIRED" },
|
||||||
|
{ label: "待退款", value: "PENDING" },
|
||||||
|
{ label: "已退款", value: "REFUNDED" }
|
||||||
|
];
|
||||||
|
// ---------- 列表 ----------
|
||||||
|
|
||||||
const status = ref("ALL");
|
|
||||||
const paymentStatus = ref<PaymentFilter>("ALL");
|
|
||||||
const refundStatus = ref<RefundFilter>("ALL");
|
|
||||||
type SalesOrderSummaryExpose = { refresh: () => Promise<void> };
|
type SalesOrderSummaryExpose = { refresh: () => Promise<void> };
|
||||||
const customerOrderSummaryRef = ref<SalesOrderSummaryExpose>();
|
const customerOrderSummaryRef = ref<SalesOrderSummaryExpose>();
|
||||||
const list = ref<SalesOrder[]>([]);
|
const list = ref<SalesOrder[]>([]);
|
||||||
@@ -172,30 +214,8 @@ const loading = ref(false);
|
|||||||
const refreshing = ref(false);
|
const refreshing = ref(false);
|
||||||
const noMore = ref(false);
|
const noMore = ref(false);
|
||||||
const pageSize = 15;
|
const pageSize = 15;
|
||||||
const orderTypeOptions = [
|
|
||||||
{ label: "全部类型", value: "ALL" },
|
// ---------- 路由和持久化 ----------
|
||||||
{ label: "套餐订单", value: "PACKAGE" },
|
|
||||||
{ label: "产品订单", value: "DIRECT_PRODUCT" }
|
|
||||||
];
|
|
||||||
const statusOptions = [
|
|
||||||
{ label: "全部状态", value: "ALL" },
|
|
||||||
{ label: "进行中", value: "ACTIVE" },
|
|
||||||
{ label: "已履约", value: "FULFILLED" },
|
|
||||||
{ label: "已取消", value: "CANCELLED" }
|
|
||||||
];
|
|
||||||
const paymentOptions = [
|
|
||||||
{ label: "全部收款", value: "ALL" },
|
|
||||||
{ label: "未收清", value: "PENDING_RECEIVE" },
|
|
||||||
{ label: "未收款", value: "UNPAID" },
|
|
||||||
{ label: "部分收款", value: "PARTIALLY_PAID" },
|
|
||||||
{ label: "已收款", value: "PAID" }
|
|
||||||
];
|
|
||||||
const refundOptions = [
|
|
||||||
{ label: "全部退款", value: "ALL" },
|
|
||||||
{ label: "无需退款", value: "NOT_REQUIRED" },
|
|
||||||
{ label: "待退款", value: "PENDING" },
|
|
||||||
{ label: "已退款", value: "REFUNDED" }
|
|
||||||
];
|
|
||||||
|
|
||||||
const routeFilterKey = computed(() => [
|
const routeFilterKey = computed(() => [
|
||||||
route.query.customerId,
|
route.query.customerId,
|
||||||
@@ -207,21 +227,75 @@ const routeFilterKey = computed(() => [
|
|||||||
route.query.refundStatus,
|
route.query.refundStatus,
|
||||||
route.query.pendingReceive
|
route.query.pendingReceive
|
||||||
].map(queryValue).join("|"));
|
].map(queryValue).join("|"));
|
||||||
|
let initialized = false;
|
||||||
|
let lastAppliedRouteFilterKey = "";
|
||||||
|
|
||||||
|
interface PersistedSalesOrderState {
|
||||||
|
filter: SalesOrderListFilter;
|
||||||
|
customerId: string;
|
||||||
|
}
|
||||||
|
|
||||||
function queryValue(value: unknown): string {
|
function queryValue(value: unknown): string {
|
||||||
return Array.isArray(value) ? "" : typeof value === "string" ? value : "";
|
return Array.isArray(value) ? "" : typeof value === "string" ? value : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
const packageConfigId = computed(() => queryValue(route.query.packageConfigId));
|
function readPersistedState(): PersistedSalesOrderState {
|
||||||
const selectedPackageName = computed(() => selectedPackage.value?.name || queryValue(route.query.packageConfigName) || "未命名套餐");
|
try {
|
||||||
|
const value = localStorage.getItem(filterStorageKey.value);
|
||||||
|
const source: unknown = value ? JSON.parse(value) : undefined;
|
||||||
|
const filter = normalizeSalesOrderFilter(source);
|
||||||
|
if (filter.dateFilterType !== "custom") {
|
||||||
|
filter.dateRange = buildPresetDateRange(filter.dateFilterType);
|
||||||
|
}
|
||||||
|
const customerId = source && typeof source === "object" && typeof (source as { customerId?: unknown }).customerId === "string"
|
||||||
|
? (source as { customerId: string }).customerId
|
||||||
|
: "";
|
||||||
|
return { filter, customerId };
|
||||||
|
} catch {
|
||||||
|
return { filter: createDefaultSalesOrderFilter(), customerId: "" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistFilter(): void {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(filterStorageKey.value, JSON.stringify({
|
||||||
|
...appliedFilter.value,
|
||||||
|
customerId: customerId.value || undefined
|
||||||
|
}));
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function applyRouteFilters(): void {
|
function applyRouteFilters(): void {
|
||||||
|
const persistedState = readPersistedState();
|
||||||
|
const nextFilter = persistedState.filter;
|
||||||
|
customerId.value = queryValue(route.query.customerId) || persistedState.customerId;
|
||||||
|
const routeOrderType = queryValue(route.query.orderType);
|
||||||
|
const routeStatus = queryValue(route.query.status);
|
||||||
|
const routePaymentStatus = queryValue(route.query.paymentStatus);
|
||||||
|
const routeRefundStatus = queryValue(route.query.refundStatus);
|
||||||
const pendingReceive = queryValue(route.query.pendingReceive) === "true";
|
const pendingReceive = queryValue(route.query.pendingReceive) === "true";
|
||||||
customerId.value = queryValue(route.query.customerId);
|
if (packageConfigId.value) {
|
||||||
orderType.value = packageConfigId.value ? "PACKAGE" : queryValue(route.query.orderType) || "ALL";
|
nextFilter.orderTypeList = ["PACKAGE"];
|
||||||
status.value = queryValue(route.query.status) || "ALL";
|
} else if (routeOrderType) {
|
||||||
paymentStatus.value = pendingReceive ? "PENDING_RECEIVE" : queryValue(route.query.paymentStatus) as PaymentFilter || "ALL";
|
nextFilter.orderTypeList = routeOrderType === "ALL" ? orderTypeOptions.map(item => item.value) : [routeOrderType as GaoSalesOrderType];
|
||||||
refundStatus.value = queryValue(route.query.refundStatus) as RefundFilter || "ALL";
|
}
|
||||||
|
if (routeStatus) {
|
||||||
|
nextFilter.statusList = routeStatus === "ALL" ? statusOptions.map(item => item.value) : [routeStatus as GaoSalesOrderStatus];
|
||||||
|
}
|
||||||
|
if (pendingReceive) {
|
||||||
|
nextFilter.statusList = ["ACTIVE", "FULFILLED"];
|
||||||
|
nextFilter.paymentStatusList = ["PENDING_RECEIVE"];
|
||||||
|
} else if (routePaymentStatus) {
|
||||||
|
nextFilter.paymentStatusList = routePaymentStatus === "PENDING_RECEIVE"
|
||||||
|
? ["PENDING_RECEIVE"]
|
||||||
|
: routePaymentStatus === "ALL" ? paymentOptions.map(item => item.value) : [routePaymentStatus as GaoSalesPaymentStatus];
|
||||||
|
}
|
||||||
|
if (routeRefundStatus) {
|
||||||
|
nextFilter.refundStatusList = routeRefundStatus === "ALL" ? refundOptions.map(item => item.value) : [routeRefundStatus as GaoSalesRefundStatus];
|
||||||
|
}
|
||||||
|
appliedFilter.value = nextFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadRoutePackage(): Promise<void> {
|
async function loadRoutePackage(): Promise<void> {
|
||||||
@@ -254,27 +328,34 @@ async function loadRouteCustomer(): Promise<void> {
|
|||||||
setCustomerKeyword(selectedCustomer.value);
|
setCustomerKeyword(selectedCustomer.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleCustomerChange(): Promise<void> {
|
// ---------- 请求筛选 ----------
|
||||||
try {
|
|
||||||
await loadRouteCustomer();
|
function getEffectiveList<T>(value: T[], allValues: T[]): T[] | undefined {
|
||||||
await fetchList(true);
|
return value.length && value.length < allValues.length ? value : undefined;
|
||||||
} catch (error) {
|
|
||||||
selectedCustomer.value = undefined;
|
|
||||||
Message.error(getErrorMessage(error, "客户加载失败"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getPaymentStatusValues(valueList: SalesOrderPaymentFilter[]): GaoSalesPaymentStatus[] {
|
||||||
|
const valueSet = new Set<GaoSalesPaymentStatus>(valueList.filter(value => value !== "PENDING_RECEIVE") as GaoSalesPaymentStatus[]);
|
||||||
|
if (valueList.includes("PENDING_RECEIVE")) {
|
||||||
|
valueSet.add("UNPAID");
|
||||||
|
valueSet.add("PARTIALLY_PAID");
|
||||||
|
}
|
||||||
|
return paymentStatusValues.filter(value => valueSet.has(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildPageFilter(): SalesOrderPage {
|
function buildPageFilter(): SalesOrderPage {
|
||||||
const pendingReceive = paymentStatus.value === "PENDING_RECEIVE";
|
const filter = appliedFilter.value;
|
||||||
|
const [beginText, endText] = filter.dateRange;
|
||||||
return {
|
return {
|
||||||
customerId: customerId.value || undefined,
|
customerId: customerId.value || undefined,
|
||||||
packageConfigId: packageConfigId.value || undefined,
|
packageConfigId: packageConfigId.value || undefined,
|
||||||
keyword: keyword.value || undefined,
|
keyword: keyword.value || undefined,
|
||||||
orderType: orderType.value === "ALL" ? undefined : orderType.value as GaoSalesOrderType,
|
orderTypeList: packageConfigId.value ? ["PACKAGE"] : getEffectiveList(filter.orderTypeList, orderTypeOptions.map(item => item.value)),
|
||||||
status: status.value === "ALL" ? undefined : status.value as GaoSalesOrderStatus,
|
statusList: getEffectiveList(filter.statusList, statusOptions.map(item => item.value)),
|
||||||
paymentStatus: pendingReceive || paymentStatus.value === "ALL" ? undefined : paymentStatus.value as GaoSalesPaymentStatus,
|
paymentStatusList: getEffectiveList(getPaymentStatusValues(filter.paymentStatusList), paymentStatusValues),
|
||||||
refundStatus: refundStatus.value === "ALL" ? undefined : refundStatus.value as GaoSalesRefundStatus,
|
refundStatusList: getEffectiveList(filter.refundStatusList, refundOptions.map(item => item.value)),
|
||||||
pendingReceive: pendingReceive ? true : undefined,
|
beginAt: beginText ? parseDateTime(beginText, false).getTime() : undefined,
|
||||||
|
endAt: endText ? parseDateTime(endText, true).getTime() : undefined,
|
||||||
index: pageIndex.value,
|
index: pageIndex.value,
|
||||||
size: pageSize
|
size: pageSize
|
||||||
};
|
};
|
||||||
@@ -282,7 +363,12 @@ function buildPageFilter(): SalesOrderPage {
|
|||||||
|
|
||||||
async function fetchList(reset: boolean): Promise<void> {
|
async function fetchList(reset: boolean): Promise<void> {
|
||||||
if (loading.value || (!reset && noMore.value)) return;
|
if (loading.value || (!reset && noMore.value)) return;
|
||||||
if (reset) { pageIndex.value = 0; total.value = 0; noMore.value = false; list.value = []; }
|
if (reset) {
|
||||||
|
pageIndex.value = 0;
|
||||||
|
total.value = 0;
|
||||||
|
noMore.value = false;
|
||||||
|
list.value = [];
|
||||||
|
}
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const result = await SalesOrderAPI.list(buildPageFilter());
|
const result = await SalesOrderAPI.list(buildPageFilter());
|
||||||
@@ -291,13 +377,19 @@ async function fetchList(reset: boolean): Promise<void> {
|
|||||||
total.value = result.total || 0;
|
total.value = result.total || 0;
|
||||||
pageIndex.value += 1;
|
pageIndex.value += 1;
|
||||||
noMore.value = rows.length < pageSize || total.value <= list.value.length;
|
noMore.value = rows.length < pageSize || total.value <= list.value.length;
|
||||||
} finally { loading.value = false; }
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reloadList(): Promise<void> {
|
||||||
|
await customerOrderSummaryRef.value?.refresh();
|
||||||
|
await fetchList(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refresh(): Promise<void> {
|
async function refresh(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await customerOrderSummaryRef.value?.refresh();
|
await reloadList();
|
||||||
await fetchList(true);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Message.error(getErrorMessage(error, "销售订单加载失败"));
|
Message.error(getErrorMessage(error, "销售订单加载失败"));
|
||||||
} finally {
|
} finally {
|
||||||
@@ -313,31 +405,26 @@ async function loadMore(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function selectOrderType(value: DropdownValue): Promise<void> {
|
async function confirmFilter(value: SalesOrderListFilter): Promise<void> {
|
||||||
if (!Array.isArray(value)) {
|
appliedFilter.value = cloneSalesOrderFilter(value);
|
||||||
orderType.value = String(value);
|
persistFilter();
|
||||||
await fetchList(true);
|
try {
|
||||||
|
await reloadList();
|
||||||
|
} catch (error) {
|
||||||
|
Message.error(getErrorMessage(error, "销售订单加载失败"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function selectStatus(value: DropdownValue): Promise<void> {
|
// ---------- 客户、路由和导航 ----------
|
||||||
if (!Array.isArray(value)) {
|
|
||||||
status.value = String(value);
|
|
||||||
await fetchList(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function selectPaymentStatus(value: DropdownValue): Promise<void> {
|
async function handleCustomerChange(): Promise<void> {
|
||||||
if (!Array.isArray(value)) {
|
try {
|
||||||
paymentStatus.value = String(value) as PaymentFilter;
|
await loadRouteCustomer();
|
||||||
await fetchList(true);
|
persistFilter();
|
||||||
}
|
await reloadList();
|
||||||
}
|
} catch (error) {
|
||||||
|
selectedCustomer.value = undefined;
|
||||||
async function selectRefundStatus(value: DropdownValue): Promise<void> {
|
Message.error(getErrorMessage(error, "客户加载失败"));
|
||||||
if (!Array.isArray(value)) {
|
|
||||||
refundStatus.value = String(value) as RefundFilter;
|
|
||||||
await fetchList(true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,6 +459,7 @@ const search = Toolkit.debounce(async () => {
|
|||||||
Message.error(getErrorMessage(error, "销售订单加载失败"));
|
Message.error(getErrorMessage(error, "销售订单加载失败"));
|
||||||
}
|
}
|
||||||
}, false);
|
}, false);
|
||||||
|
|
||||||
watch(keyword, async () => {
|
watch(keyword, async () => {
|
||||||
const isCustomerKeyword = !!customerId.value && keyword.value.trim() === customerKeyword;
|
const isCustomerKeyword = !!customerId.value && keyword.value.trim() === customerKeyword;
|
||||||
customerKeyword = "";
|
customerKeyword = "";
|
||||||
@@ -380,31 +468,67 @@ watch(keyword, async () => {
|
|||||||
}
|
}
|
||||||
customerId.value = "";
|
customerId.value = "";
|
||||||
selectedCustomer.value = undefined;
|
selectedCustomer.value = undefined;
|
||||||
|
persistFilter();
|
||||||
await search();
|
await search();
|
||||||
});
|
});
|
||||||
watch(routeFilterKey, async () => {
|
|
||||||
|
watch(routeFilterKey, async value => {
|
||||||
|
if (!initialized || route.name !== "SalesOrderList" || value === lastAppliedRouteFilterKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastAppliedRouteFilterKey = value;
|
||||||
applyRouteFilters();
|
applyRouteFilters();
|
||||||
try {
|
try {
|
||||||
await loadRouteCustomer();
|
await loadRouteCustomer();
|
||||||
|
await loadRoutePackage();
|
||||||
|
await fetchList(true);
|
||||||
} catch {
|
} catch {
|
||||||
selectedCustomer.value = undefined;
|
selectedCustomer.value = undefined;
|
||||||
}
|
}
|
||||||
await loadRoutePackage();
|
|
||||||
await fetchList(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(() => currentStoreStore.currentStoreId, async (value, oldValue) => {
|
||||||
|
if (!initialized || value === oldValue || route.name !== "SalesOrderList") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const persistedState = readPersistedState();
|
||||||
|
appliedFilter.value = persistedState.filter;
|
||||||
|
if (!queryValue(route.query.customerId)) {
|
||||||
|
customerId.value = persistedState.customerId;
|
||||||
|
}
|
||||||
|
await loadRouteCustomer();
|
||||||
|
await refresh();
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
navBarStore.clearRight();
|
navBarStore.clearRight();
|
||||||
navBarStore.setCustomRightVisible(!!actions.value.length);
|
navBarStore.setCustomRightVisible(!!actions.value.length);
|
||||||
applyRouteFilters();
|
applyRouteFilters();
|
||||||
|
lastAppliedRouteFilterKey = routeFilterKey.value;
|
||||||
try {
|
try {
|
||||||
await loadRouteCustomer();
|
await loadRouteCustomer();
|
||||||
await loadRoutePackage();
|
await loadRoutePackage();
|
||||||
await fetchList(true);
|
await fetchList(true);
|
||||||
|
initialized = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Message.error(getErrorMessage(error, "销售订单加载失败"));
|
Message.error(getErrorMessage(error, "销售订单加载失败"));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
onUnmounted(() => { navBarStore.clearRight(); search.cancel(); });
|
|
||||||
|
onActivated(() => {
|
||||||
|
navBarStore.clearRight();
|
||||||
|
navBarStore.setCustomRightVisible(!!actions.value.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
onDeactivated(() => {
|
||||||
|
filterVisible.value = false;
|
||||||
|
navBarStore.clearRight();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
navBarStore.clearRight();
|
||||||
|
search.cancel();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
@@ -416,23 +540,24 @@ onUnmounted(() => { navBarStore.clearRight(); search.cancel(); });
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
display: flex;
|
padding: .75rem 1rem .5rem;
|
||||||
padding: .75rem 1rem;
|
|
||||||
position: sticky;
|
position: sticky;
|
||||||
background: #F5F5F8;
|
background: var(--td-bg-color-page);
|
||||||
box-shadow: var(--td-shadow-1);
|
box-shadow: var(--td-shadow-1);
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
.search-status {
|
.search-row {
|
||||||
|
gap: .5rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.keyword {
|
.keyword {
|
||||||
--td-button-border-radius: .75rem;
|
--td-button-border-radius: .75rem;
|
||||||
--td-input-vertical-padding: .25rem 1rem;
|
--td-input-vertical-padding: .25rem 1rem;
|
||||||
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
border: 1px solid var(--td-component-border);
|
border: 1px solid var(--td-component-border);
|
||||||
z-index: 2;
|
|
||||||
background: var(--td-bg-color-container);
|
background: var(--td-bg-color-container);
|
||||||
padding-right: .3rem;
|
padding-right: .3rem;
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
@@ -444,34 +569,12 @@ onUnmounted(() => { navBarStore.clearRight(); search.cancel(); });
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.status {
|
.filter-button {
|
||||||
--td-spacer: 4px;
|
--td-button-border-radius: 99rem;
|
||||||
--td-spacer-1: 6px;
|
flex: none;
|
||||||
|
height: auto;
|
||||||
border: 1px solid rgba(148, 163, 184, .18);
|
padding: 0 1.25rem;
|
||||||
z-index: 1;
|
align-self: stretch;
|
||||||
margin-left: -2.5rem;
|
|
||||||
padding-left: 3rem;
|
|
||||||
padding-right: .7rem;
|
|
||||||
border-radius: 1rem;
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -484,13 +587,6 @@ onUnmounted(() => { navBarStore.clearRight(); search.cancel(); });
|
|||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
.selected-package {
|
|
||||||
|
|
||||||
:deep(.t-cell-group) {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
}
|
}
|
||||||
@@ -503,5 +599,4 @@ onUnmounted(() => { navBarStore.clearRight(); search.cancel(); });
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,353 @@
|
|||||||
|
<template>
|
||||||
|
<filter-popup
|
||||||
|
v-model:visible="popupVisible"
|
||||||
|
:model-value="filterModel"
|
||||||
|
:reset-value="resetFilterModel"
|
||||||
|
:groups="visibleFilterGroups"
|
||||||
|
:title="popupTitle"
|
||||||
|
@update:model-value="updateFilterModel"
|
||||||
|
@reset="reset"
|
||||||
|
@confirm="confirm"
|
||||||
|
>
|
||||||
|
<template #group-date-after>
|
||||||
|
<t-cell-group
|
||||||
|
v-if="draftFilter.dateFilterType === 'custom'"
|
||||||
|
class="date-group"
|
||||||
|
>
|
||||||
|
<t-cell
|
||||||
|
class="date-cell"
|
||||||
|
title="开始时间"
|
||||||
|
:note="draftFilter.dateRange[0] || '请选择'"
|
||||||
|
arrow
|
||||||
|
@click="openStartTime"
|
||||||
|
/>
|
||||||
|
<t-cell
|
||||||
|
class="date-cell"
|
||||||
|
title="结束时间"
|
||||||
|
:note="draftFilter.dateRange[1] || '请选择'"
|
||||||
|
arrow
|
||||||
|
@click="openEndTime"
|
||||||
|
/>
|
||||||
|
</t-cell-group>
|
||||||
|
</template>
|
||||||
|
</filter-popup>
|
||||||
|
<t-popup v-model="startVisible" placement="bottom">
|
||||||
|
<t-date-time-picker
|
||||||
|
v-if="startVisible"
|
||||||
|
v-model="startDraft"
|
||||||
|
format="YYYY-MM-DD HH:mm"
|
||||||
|
:mode="['date', 'minute']"
|
||||||
|
title="开始时间"
|
||||||
|
:default-value="startDraft"
|
||||||
|
@cancel="cancelStartTime"
|
||||||
|
@confirm="confirmStartTime"
|
||||||
|
/>
|
||||||
|
</t-popup>
|
||||||
|
<t-popup v-model="endVisible" placement="bottom">
|
||||||
|
<t-date-time-picker
|
||||||
|
v-if="endVisible"
|
||||||
|
v-model="endDraft"
|
||||||
|
format="YYYY-MM-DD HH:mm"
|
||||||
|
:mode="['date', 'minute']"
|
||||||
|
title="结束时间"
|
||||||
|
:default-value="endDraft"
|
||||||
|
@cancel="cancelEndTime"
|
||||||
|
@confirm="confirmEndTime"
|
||||||
|
/>
|
||||||
|
</t-popup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Time } from "timi-web";
|
||||||
|
import FilterPopup from "@/components/filter-popup/FilterPopup.vue";
|
||||||
|
import type { FilterGroup, FilterModel, FilterValueList } from "@/components/filter-popup/FilterPopup";
|
||||||
|
import { buildPresetDateRange } from "@/utils/date";
|
||||||
|
import type { GaoSalesOrderStatus, GaoSalesOrderType, GaoSalesRefundStatus } from "@/types/SalesOrder";
|
||||||
|
import {
|
||||||
|
cloneSalesOrderFilter,
|
||||||
|
createDefaultSalesOrderFilter,
|
||||||
|
type SalesOrderDateFilterType,
|
||||||
|
type SalesOrderFilterGroupKey,
|
||||||
|
type SalesOrderListFilter,
|
||||||
|
type SalesOrderPaymentFilter
|
||||||
|
} from "@/types/SalesOrderFilter";
|
||||||
|
import { Message } from "@/utils/message";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue: SalesOrderListFilter;
|
||||||
|
visible: boolean;
|
||||||
|
packageConfigId?: string;
|
||||||
|
filterKey?: SalesOrderFilterGroupKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), { packageConfigId: "" });
|
||||||
|
const emit = defineEmits<{
|
||||||
|
"update:visible": [value: boolean];
|
||||||
|
confirm: [value: SalesOrderListFilter];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const popupVisible = computed({
|
||||||
|
get: () => props.visible,
|
||||||
|
set: value => emit("update:visible", value)
|
||||||
|
});
|
||||||
|
const draftFilter = ref<SalesOrderListFilter>(cloneSalesOrderFilter(props.modelValue));
|
||||||
|
const startVisible = ref(false);
|
||||||
|
const endVisible = ref(false);
|
||||||
|
const startDraft = ref("");
|
||||||
|
const endDraft = ref("");
|
||||||
|
const dateOptions: Array<{ label: string; value: SalesOrderDateFilterType }> = [
|
||||||
|
{ label: "全部时间", value: "all" },
|
||||||
|
{ label: "今天", value: "today" },
|
||||||
|
{ label: "昨天", value: "yesterday" },
|
||||||
|
{ label: "本周", value: "thisWeek" },
|
||||||
|
{ label: "本月", value: "thisMonth" },
|
||||||
|
{ label: "最近 30 天", value: "last30" },
|
||||||
|
{ label: "自定义范围", value: "custom" }
|
||||||
|
];
|
||||||
|
const orderTypeOptions: Array<{ label: string; value: GaoSalesOrderType }> = [
|
||||||
|
{ label: "套餐订单", value: "PACKAGE" },
|
||||||
|
{ label: "产品订单", value: "DIRECT_PRODUCT" }
|
||||||
|
];
|
||||||
|
const statusOptions: Array<{ label: string; value: GaoSalesOrderStatus }> = [
|
||||||
|
{ label: "进行中", value: "ACTIVE" },
|
||||||
|
{ label: "已履约", value: "FULFILLED" },
|
||||||
|
{ label: "已取消", value: "CANCELLED" }
|
||||||
|
];
|
||||||
|
|
||||||
|
const paymentOptions: Array<{ label: string; value: SalesOrderPaymentFilter }> = [
|
||||||
|
{ label: "已收款", value: "PAID" },
|
||||||
|
{ label: "部分收款", value: "PARTIALLY_PAID" },
|
||||||
|
{ label: "未收清", value: "PENDING_RECEIVE" },
|
||||||
|
{ label: "未收款", value: "UNPAID" }
|
||||||
|
];
|
||||||
|
const refundOptions: Array<{ label: string; value: GaoSalesRefundStatus }> = [
|
||||||
|
{ label: "无需退款", value: "NOT_REQUIRED" },
|
||||||
|
{ label: "待退款", value: "PENDING" },
|
||||||
|
{ label: "已退款", value: "REFUNDED" }
|
||||||
|
];
|
||||||
|
const filterGroups = computed<FilterGroup[]>(() => [
|
||||||
|
{
|
||||||
|
key: "date",
|
||||||
|
title: "下单时间",
|
||||||
|
options: dateOptions,
|
||||||
|
control: "radio",
|
||||||
|
multiple: false,
|
||||||
|
columns: 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "orderTypeList",
|
||||||
|
title: "订单类型",
|
||||||
|
description: props.packageConfigId ? "当前套餐固定" : "",
|
||||||
|
options: orderTypeOptions,
|
||||||
|
multiple: true,
|
||||||
|
columns: 2,
|
||||||
|
readonly: Boolean(props.packageConfigId),
|
||||||
|
readonlyText: "套餐订单(已由当前套餐固定)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "statusList",
|
||||||
|
title: "订单状态",
|
||||||
|
options: statusOptions,
|
||||||
|
multiple: true,
|
||||||
|
columns: 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "paymentStatusList",
|
||||||
|
title: "收款状态",
|
||||||
|
options: paymentOptions,
|
||||||
|
multiple: true,
|
||||||
|
columns: 2,
|
||||||
|
onChange: normalizePaymentFilter
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "refundStatusList",
|
||||||
|
title: "退款状态",
|
||||||
|
options: refundOptions,
|
||||||
|
multiple: true,
|
||||||
|
columns: 2
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
const visibleFilterGroups = computed(() => props.filterKey
|
||||||
|
? filterGroups.value.filter(group => group.key === props.filterKey)
|
||||||
|
: filterGroups.value);
|
||||||
|
const popupTitle = computed(() => {
|
||||||
|
if (!props.filterKey) {
|
||||||
|
return "筛选销售订单";
|
||||||
|
}
|
||||||
|
const group = visibleFilterGroups.value[0];
|
||||||
|
return group ? `筛选${group.title}` : "筛选销售订单";
|
||||||
|
});
|
||||||
|
const filterModel = computed<FilterModel>(() => ({
|
||||||
|
date: [draftFilter.value.dateFilterType],
|
||||||
|
orderTypeList: [...draftFilter.value.orderTypeList],
|
||||||
|
statusList: [...draftFilter.value.statusList],
|
||||||
|
paymentStatusList: [...draftFilter.value.paymentStatusList],
|
||||||
|
refundStatusList: [...draftFilter.value.refundStatusList]
|
||||||
|
}));
|
||||||
|
const resetFilterModel = computed<FilterModel>(() => {
|
||||||
|
if (!props.filterKey) {
|
||||||
|
return toFilterModel(createResetFilter());
|
||||||
|
}
|
||||||
|
const currentModel = toFilterModel(draftFilter.value);
|
||||||
|
const resetModel = toFilterModel(createResetFilter());
|
||||||
|
currentModel[props.filterKey] = resetModel[props.filterKey];
|
||||||
|
return currentModel;
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => props.visible, visible => {
|
||||||
|
if (visible) {
|
||||||
|
draftFilter.value = cloneSalesOrderFilter(props.modelValue);
|
||||||
|
if (props.packageConfigId) {
|
||||||
|
draftFilter.value.orderTypeList = ["PACKAGE"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function selectDateFilter(value: unknown): void {
|
||||||
|
const dateFilterType = String(value) as SalesOrderDateFilterType;
|
||||||
|
draftFilter.value.dateFilterType = dateFilterType;
|
||||||
|
if (dateFilterType !== "custom") {
|
||||||
|
draftFilter.value.dateRange = buildPresetDateRange(dateFilterType);
|
||||||
|
}
|
||||||
|
if (dateFilterType === "custom" && !draftFilter.value.dateRange[0] && !draftFilter.value.dateRange[1]) {
|
||||||
|
const today = Time.formatDate(new Date());
|
||||||
|
draftFilter.value.dateRange = [`${today} 00:00`, `${today} 23:59`];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFilterModel(value: FilterModel): void {
|
||||||
|
const dateFilterType = getStringList<SalesOrderDateFilterType>(value.date)[0];
|
||||||
|
if (dateFilterType && dateFilterType !== draftFilter.value.dateFilterType) {
|
||||||
|
selectDateFilter(dateFilterType);
|
||||||
|
}
|
||||||
|
draftFilter.value.orderTypeList = getStringList<GaoSalesOrderType>(value.orderTypeList);
|
||||||
|
draftFilter.value.statusList = getStringList<GaoSalesOrderStatus>(value.statusList);
|
||||||
|
draftFilter.value.paymentStatusList = getStringList<SalesOrderPaymentFilter>(value.paymentStatusList);
|
||||||
|
draftFilter.value.refundStatusList = getStringList<GaoSalesRefundStatus>(value.refundStatusList);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toFilterModel(filter: SalesOrderListFilter): FilterModel {
|
||||||
|
return {
|
||||||
|
date: [filter.dateFilterType],
|
||||||
|
orderTypeList: [...filter.orderTypeList],
|
||||||
|
statusList: [...filter.statusList],
|
||||||
|
paymentStatusList: [...filter.paymentStatusList],
|
||||||
|
refundStatusList: [...filter.refundStatusList]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createResetFilter(): SalesOrderListFilter {
|
||||||
|
const filter = createDefaultSalesOrderFilter();
|
||||||
|
if (props.packageConfigId) {
|
||||||
|
filter.orderTypeList = ["PACKAGE"];
|
||||||
|
}
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStringList<T extends string>(value?: FilterValueList): T[] {
|
||||||
|
return (value || []).filter(item => typeof item === "string") as T[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePaymentFilter(context: { value: FilterValueList; changedValue?: string | number | boolean; checked?: boolean }): FilterValueList {
|
||||||
|
const value = [...context.value];
|
||||||
|
if (context.checked && context.changedValue === "PENDING_RECEIVE") {
|
||||||
|
return value.filter(item => item !== "UNPAID" && item !== "PARTIALLY_PAID");
|
||||||
|
}
|
||||||
|
if (context.checked && (context.changedValue === "UNPAID" || context.changedValue === "PARTIALLY_PAID")) {
|
||||||
|
return value.filter(item => item !== "PENDING_RECEIVE");
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openStartTime(): void {
|
||||||
|
startDraft.value = draftFilter.value.dateRange[0] || `${Time.formatDate(new Date())} 00:00`;
|
||||||
|
startVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEndTime(): void {
|
||||||
|
endDraft.value = draftFilter.value.dateRange[1] || `${Time.formatDate(new Date())} 23:59`;
|
||||||
|
endVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelStartTime(): void {
|
||||||
|
startVisible.value = false;
|
||||||
|
startDraft.value = draftFilter.value.dateRange[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelEndTime(): void {
|
||||||
|
endVisible.value = false;
|
||||||
|
endDraft.value = draftFilter.value.dateRange[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmStartTime(value?: string | number): void {
|
||||||
|
startVisible.value = false;
|
||||||
|
draftFilter.value.dateRange = [String(value || startDraft.value), draftFilter.value.dateRange[1]];
|
||||||
|
checkDateRangeOrder();
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmEndTime(value?: string | number): void {
|
||||||
|
endVisible.value = false;
|
||||||
|
draftFilter.value.dateRange = [draftFilter.value.dateRange[0], String(value || endDraft.value)];
|
||||||
|
checkDateRangeOrder();
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkDateRangeOrder(): void {
|
||||||
|
if (draftFilter.value.dateRange[0] && draftFilter.value.dateRange[1] && draftFilter.value.dateRange[1] < draftFilter.value.dateRange[0]) {
|
||||||
|
draftFilter.value.dateRange = [draftFilter.value.dateRange[1], draftFilter.value.dateRange[0]];
|
||||||
|
}
|
||||||
|
draftFilter.value.dateFilterType = "custom";
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset(): void {
|
||||||
|
const resetFilter = createResetFilter();
|
||||||
|
if (!props.filterKey) {
|
||||||
|
draftFilter.value = resetFilter;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const nextFilter = cloneSalesOrderFilter(draftFilter.value);
|
||||||
|
resetFilterGroup(nextFilter, resetFilter, props.filterKey);
|
||||||
|
draftFilter.value = nextFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetFilterGroup(target: SalesOrderListFilter, source: SalesOrderListFilter, filterKey: SalesOrderFilterGroupKey): void {
|
||||||
|
switch (filterKey) {
|
||||||
|
case "date":
|
||||||
|
target.dateFilterType = source.dateFilterType;
|
||||||
|
target.dateRange = [...source.dateRange];
|
||||||
|
break;
|
||||||
|
case "orderTypeList":
|
||||||
|
target.orderTypeList = [...source.orderTypeList];
|
||||||
|
break;
|
||||||
|
case "statusList":
|
||||||
|
target.statusList = [...source.statusList];
|
||||||
|
break;
|
||||||
|
case "paymentStatusList":
|
||||||
|
target.paymentStatusList = [...source.paymentStatusList];
|
||||||
|
break;
|
||||||
|
case "refundStatusList":
|
||||||
|
target.refundStatusList = [...source.refundStatusList];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirm(): void {
|
||||||
|
if ((!props.filterKey || props.filterKey === "date")
|
||||||
|
&& draftFilter.value.dateFilterType === "custom"
|
||||||
|
&& (!draftFilter.value.dateRange[0] || !draftFilter.value.dateRange[1])) {
|
||||||
|
Message.warning("请选择完整的时间范围");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emit("confirm", cloneSalesOrderFilter(draftFilter.value));
|
||||||
|
popupVisible.value = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.date-group {
|
||||||
|
border: 1px solid var(--td-component-border);
|
||||||
|
overflow: auto;
|
||||||
|
margin-top: .25rem;
|
||||||
|
box-shadow: var(--td-shadow-1);
|
||||||
|
border-radius: .75rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,250 @@
|
|||||||
|
<template>
|
||||||
|
<t-cell-group class="filter-summary" theme="card">
|
||||||
|
<t-cell title="开始时间" :note="filterBeginAtText" arrow @click="openStartTime" />
|
||||||
|
<t-cell title="结束时间" :note="filterEndAtText" arrow @click="openEndTime" />
|
||||||
|
<t-cell title="状态" class="filter-cell" arrow @click="openFilterPopup('statusList')">
|
||||||
|
<template #note>
|
||||||
|
<div class="filter-tags">
|
||||||
|
<t-tag
|
||||||
|
v-for="item in selectedStatusTags"
|
||||||
|
:key="item"
|
||||||
|
theme="primary"
|
||||||
|
variant="light-outline"
|
||||||
|
:content="item"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</t-cell>
|
||||||
|
<t-cell title="订单条件" class="filter-cell" arrow @click="openFilterPopup('orderTypeList')">
|
||||||
|
<template #note>
|
||||||
|
<div class="filter-tags">
|
||||||
|
<t-tag
|
||||||
|
v-for="item in selectedOrderTypeTags"
|
||||||
|
:key="item"
|
||||||
|
theme="primary"
|
||||||
|
variant="light-outline"
|
||||||
|
:content="item"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</t-cell>
|
||||||
|
<t-cell title="资金条件" class="filter-cell" arrow @click="openFilterPopup('paymentStatusList')">
|
||||||
|
<template #note>
|
||||||
|
<div class="filter-tags">
|
||||||
|
<t-tag
|
||||||
|
v-for="item in selectedPaymentTags"
|
||||||
|
:key="item"
|
||||||
|
theme="primary"
|
||||||
|
variant="light-outline"
|
||||||
|
:content="item"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</t-cell>
|
||||||
|
<t-cell title="退款条件" class="filter-cell" arrow @click="openFilterPopup('refundStatusList')">
|
||||||
|
<template #note>
|
||||||
|
<div class="filter-tags">
|
||||||
|
<t-tag
|
||||||
|
v-for="item in selectedRefundTags"
|
||||||
|
:key="item"
|
||||||
|
theme="primary"
|
||||||
|
variant="light-outline"
|
||||||
|
:content="item"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</t-cell>
|
||||||
|
<t-cell title="订单数量" :note="String(total)" />
|
||||||
|
</t-cell-group>
|
||||||
|
<sales-order-filter-popup
|
||||||
|
v-model:visible="filterPopupVisible"
|
||||||
|
:model-value="props.filter"
|
||||||
|
:filter-key="activeFilterKey"
|
||||||
|
:package-config-id="props.packageConfigId"
|
||||||
|
@confirm="confirmFilter"
|
||||||
|
/>
|
||||||
|
<t-popup v-model="startVisible" placement="bottom">
|
||||||
|
<t-date-time-picker
|
||||||
|
v-if="startVisible"
|
||||||
|
v-model="startDraft"
|
||||||
|
format="YYYY-MM-DD HH:mm"
|
||||||
|
:mode="['date', 'minute']"
|
||||||
|
title="开始时间"
|
||||||
|
:default-value="startDraft"
|
||||||
|
@cancel="cancelStartTime"
|
||||||
|
@confirm="confirmStartTime"
|
||||||
|
/>
|
||||||
|
</t-popup>
|
||||||
|
<t-popup v-model="endVisible" placement="bottom">
|
||||||
|
<t-date-time-picker
|
||||||
|
v-if="endVisible"
|
||||||
|
v-model="endDraft"
|
||||||
|
format="YYYY-MM-DD HH:mm"
|
||||||
|
:mode="['date', 'minute']"
|
||||||
|
title="结束时间"
|
||||||
|
:default-value="endDraft"
|
||||||
|
@cancel="cancelEndTime"
|
||||||
|
@confirm="confirmEndTime"
|
||||||
|
/>
|
||||||
|
</t-popup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Time } from "timi-web";
|
||||||
|
import SalesOrderFilterPopup from "@/pages/salesOrder/components/SalesOrderFilterPopup.vue";
|
||||||
|
import type { GaoSalesOrderStatus, GaoSalesOrderType, GaoSalesRefundStatus } from "@/types/SalesOrder";
|
||||||
|
import {
|
||||||
|
cloneSalesOrderFilter,
|
||||||
|
type SalesOrderFilterGroupKey,
|
||||||
|
type SalesOrderListFilter,
|
||||||
|
type SalesOrderPaymentFilter
|
||||||
|
} from "@/types/SalesOrderFilter";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
filter: SalesOrderListFilter;
|
||||||
|
total: number;
|
||||||
|
packageConfigId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineOptions({ name: "SalesOrderFilterSummary" });
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
const emit = defineEmits<{
|
||||||
|
confirm: [value: SalesOrderListFilter];
|
||||||
|
}>();
|
||||||
|
const orderTypeOptions: Array<{ label: string; value: GaoSalesOrderType }> = [
|
||||||
|
{ label: "套餐订单", value: "PACKAGE" },
|
||||||
|
{ label: "产品订单", value: "DIRECT_PRODUCT" }
|
||||||
|
];
|
||||||
|
const statusOptions: Array<{ label: string; value: GaoSalesOrderStatus }> = [
|
||||||
|
{ label: "进行中", value: "ACTIVE" },
|
||||||
|
{ label: "已履约", value: "FULFILLED" },
|
||||||
|
{ label: "已取消", value: "CANCELLED" }
|
||||||
|
];
|
||||||
|
const paymentOptions: Array<{ label: string; value: SalesOrderPaymentFilter }> = [
|
||||||
|
{ label: "未收清", value: "PENDING_RECEIVE" },
|
||||||
|
{ label: "未收款", value: "UNPAID" },
|
||||||
|
{ label: "部分收款", value: "PARTIALLY_PAID" },
|
||||||
|
{ label: "已收款", value: "PAID" }
|
||||||
|
];
|
||||||
|
const refundOptions: Array<{ label: string; value: GaoSalesRefundStatus }> = [
|
||||||
|
{ label: "无需退款", value: "NOT_REQUIRED" },
|
||||||
|
{ label: "待退款", value: "PENDING" },
|
||||||
|
{ label: "已退款", value: "REFUNDED" }
|
||||||
|
];
|
||||||
|
const filterBeginAtText = computed(() => props.filter.dateRange[0] || "不限");
|
||||||
|
const filterEndAtText = computed(() => props.filter.dateRange[1] || "不限");
|
||||||
|
const selectedStatusTags = computed(() => getSelectedOptionLabels(props.filter.statusList, statusOptions));
|
||||||
|
const selectedOrderTypeTags = computed(() => getSelectedOptionLabels(props.filter.orderTypeList, orderTypeOptions));
|
||||||
|
const selectedPaymentTags = computed(() => getSelectedOptionLabels(props.filter.paymentStatusList, paymentOptions));
|
||||||
|
const selectedRefundTags = computed(() => getSelectedOptionLabels(props.filter.refundStatusList, refundOptions));
|
||||||
|
const filterPopupVisible = ref(false);
|
||||||
|
const activeFilterKey = ref<SalesOrderFilterGroupKey>();
|
||||||
|
const startVisible = ref(false);
|
||||||
|
const endVisible = ref(false);
|
||||||
|
const startDraft = ref("");
|
||||||
|
const endDraft = ref("");
|
||||||
|
|
||||||
|
function getSelectedOptionLabels<T extends string>(valueList: T[], options: Array<{ label: string; value: T }>): string[] {
|
||||||
|
const labels = options.filter(item => valueList.includes(item.value)).map(item => item.label);
|
||||||
|
return labels.length ? labels : ["未选择"];
|
||||||
|
}
|
||||||
|
|
||||||
|
function openFilterPopup(filterKey: Exclude<SalesOrderFilterGroupKey, "date">): void {
|
||||||
|
activeFilterKey.value = filterKey;
|
||||||
|
filterPopupVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openStartTime(): void {
|
||||||
|
startDraft.value = props.filter.dateRange[0] || `${Time.formatDate(new Date())} 00:00`;
|
||||||
|
startVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEndTime(): void {
|
||||||
|
endDraft.value = props.filter.dateRange[1] || `${Time.formatDate(new Date())} 23:59`;
|
||||||
|
endVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelStartTime(): void {
|
||||||
|
startVisible.value = false;
|
||||||
|
startDraft.value = props.filter.dateRange[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelEndTime(): void {
|
||||||
|
endVisible.value = false;
|
||||||
|
endDraft.value = props.filter.dateRange[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmStartTime(value?: string | number): void {
|
||||||
|
startVisible.value = false;
|
||||||
|
confirmDateRange(0, String(value || startDraft.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmEndTime(value?: string | number): void {
|
||||||
|
endVisible.value = false;
|
||||||
|
confirmDateRange(1, String(value || endDraft.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmDateRange(index: 0 | 1, value: string): void {
|
||||||
|
const nextFilter = cloneSalesOrderFilter(props.filter);
|
||||||
|
const nextRange = [...nextFilter.dateRange] as [string, string];
|
||||||
|
nextRange[index] = value;
|
||||||
|
if (nextRange[0] && nextRange[1] && nextRange[1] < nextRange[0]) {
|
||||||
|
nextFilter.dateRange = [nextRange[1], nextRange[0]];
|
||||||
|
} else {
|
||||||
|
nextFilter.dateRange = nextRange;
|
||||||
|
}
|
||||||
|
nextFilter.dateFilterType = "custom";
|
||||||
|
emit("confirm", nextFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmFilter(value: SalesOrderListFilter): void {
|
||||||
|
emit("confirm", value);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.filter-summary {
|
||||||
|
gap: 1rem;
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
:deep(.t-cell-group) {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.t-cell__title) {
|
||||||
|
flex: 0 0 4.5rem;
|
||||||
|
margin-right: .75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.t-cell__note) {
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: right;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-cell {
|
||||||
|
.filter-tags {
|
||||||
|
gap: .4rem;
|
||||||
|
display: flex;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
justify-content: flex-start;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.t-tag) {
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -78,9 +78,13 @@ export type SalesOrderPage = {
|
|||||||
customerId?: string;
|
customerId?: string;
|
||||||
packageConfigId?: string;
|
packageConfigId?: string;
|
||||||
orderType?: GaoSalesOrderType;
|
orderType?: GaoSalesOrderType;
|
||||||
|
orderTypeList?: GaoSalesOrderType[];
|
||||||
status?: GaoSalesOrderStatus;
|
status?: GaoSalesOrderStatus;
|
||||||
|
statusList?: GaoSalesOrderStatus[];
|
||||||
paymentStatus?: GaoSalesPaymentStatus;
|
paymentStatus?: GaoSalesPaymentStatus;
|
||||||
|
paymentStatusList?: GaoSalesPaymentStatus[];
|
||||||
refundStatus?: GaoSalesRefundStatus;
|
refundStatus?: GaoSalesRefundStatus;
|
||||||
|
refundStatusList?: GaoSalesRefundStatus[];
|
||||||
pendingReceive?: boolean;
|
pendingReceive?: boolean;
|
||||||
fulfilledButUnpaid?: boolean;
|
fulfilledButUnpaid?: boolean;
|
||||||
keyword?: string;
|
keyword?: string;
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import type {
|
||||||
|
GaoSalesOrderStatus,
|
||||||
|
GaoSalesOrderType,
|
||||||
|
GaoSalesPaymentStatus,
|
||||||
|
GaoSalesRefundStatus
|
||||||
|
} from "@/types/SalesOrder";
|
||||||
|
import type { DateTimeRange } from "@/utils/date";
|
||||||
|
|
||||||
|
/** 销售订单时间筛选类型。 */
|
||||||
|
export type SalesOrderDateFilterType = "all" | "today" | "yesterday" | "thisWeek" | "thisMonth" | "last30" | "custom";
|
||||||
|
|
||||||
|
/** 销售订单资金筛选类型,未收清为未收款和部分收款的组合条件。 */
|
||||||
|
export type SalesOrderPaymentFilter = GaoSalesPaymentStatus | "PENDING_RECEIVE";
|
||||||
|
|
||||||
|
/** 销售订单筛选组。 */
|
||||||
|
export type SalesOrderFilterGroupKey = "date" | "orderTypeList" | "statusList" | "paymentStatusList" | "refundStatusList";
|
||||||
|
|
||||||
|
/** 销售订单列表筛选条件。 */
|
||||||
|
export type SalesOrderListFilter = {
|
||||||
|
dateFilterType: SalesOrderDateFilterType;
|
||||||
|
dateRange: DateTimeRange;
|
||||||
|
orderTypeList: GaoSalesOrderType[];
|
||||||
|
statusList: GaoSalesOrderStatus[];
|
||||||
|
paymentStatusList: SalesOrderPaymentFilter[];
|
||||||
|
refundStatusList: GaoSalesRefundStatus[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 创建销售订单筛选默认值。 */
|
||||||
|
export function createDefaultSalesOrderFilter(): SalesOrderListFilter {
|
||||||
|
return {
|
||||||
|
dateFilterType: "all",
|
||||||
|
dateRange: ["", ""],
|
||||||
|
orderTypeList: ["PACKAGE", "DIRECT_PRODUCT"],
|
||||||
|
statusList: ["ACTIVE", "FULFILLED"],
|
||||||
|
paymentStatusList: ["UNPAID", "PARTIALLY_PAID", "PAID"],
|
||||||
|
refundStatusList: ["NOT_REQUIRED", "PENDING", "REFUNDED"]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 复制销售订单筛选条件。 */
|
||||||
|
export function cloneSalesOrderFilter(filter: SalesOrderListFilter): SalesOrderListFilter {
|
||||||
|
return {
|
||||||
|
dateFilterType: filter.dateFilterType,
|
||||||
|
dateRange: [...filter.dateRange] as DateTimeRange,
|
||||||
|
orderTypeList: [...filter.orderTypeList],
|
||||||
|
statusList: [...filter.statusList],
|
||||||
|
paymentStatusList: [...filter.paymentStatusList],
|
||||||
|
refundStatusList: [...filter.refundStatusList]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将本地存储内容归一化为合法筛选条件。 */
|
||||||
|
export function normalizeSalesOrderFilter(value: unknown): SalesOrderListFilter {
|
||||||
|
const defaultFilter = createDefaultSalesOrderFilter();
|
||||||
|
if (!value || typeof value !== "object") {
|
||||||
|
return defaultFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
const source = value as Partial<SalesOrderListFilter>;
|
||||||
|
const dateFilterType = ["all", "today", "yesterday", "thisWeek", "thisMonth", "last30", "custom"].includes(source.dateFilterType || "")
|
||||||
|
? source.dateFilterType!
|
||||||
|
: defaultFilter.dateFilterType;
|
||||||
|
const dateRange = Array.isArray(source.dateRange) && 2 <= source.dateRange.length
|
||||||
|
? [String(source.dateRange[0] || ""), String(source.dateRange[1] || "")] as DateTimeRange
|
||||||
|
: defaultFilter.dateRange;
|
||||||
|
|
||||||
|
return {
|
||||||
|
dateFilterType,
|
||||||
|
dateRange,
|
||||||
|
orderTypeList: normalizeList(source.orderTypeList, defaultFilter.orderTypeList),
|
||||||
|
statusList: normalizeList(source.statusList, defaultFilter.statusList),
|
||||||
|
paymentStatusList: normalizeList(source.paymentStatusList, defaultFilter.paymentStatusList),
|
||||||
|
refundStatusList: normalizeList(source.refundStatusList, defaultFilter.refundStatusList)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeList<T>(value: unknown, fallback: T[]): T[] {
|
||||||
|
return Array.isArray(value) ? value.filter(item => typeof item === "string") as T[] : [...fallback];
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ export type DateFilterPreset =
|
|||||||
| "all"
|
| "all"
|
||||||
| "today"
|
| "today"
|
||||||
| "yesterday"
|
| "yesterday"
|
||||||
|
| "thisWeek"
|
||||||
| "lastWeek"
|
| "lastWeek"
|
||||||
| "thisMonth"
|
| "thisMonth"
|
||||||
| "lastMonth"
|
| "lastMonth"
|
||||||
@@ -33,6 +34,13 @@ export function buildPresetDateRange(type: DateFilterPreset, now = new Date()):
|
|||||||
return buildDayRange(lastWeekMonday, lastWeekSunday);
|
return buildDayRange(lastWeekMonday, lastWeekSunday);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === "thisWeek") {
|
||||||
|
const current = getDateStart(now);
|
||||||
|
const day = current.getDay() || 7;
|
||||||
|
const thisWeekMonday = addDays(current, 1 - day);
|
||||||
|
return buildDayRange(thisWeekMonday, current);
|
||||||
|
}
|
||||||
|
|
||||||
if (type === "thisMonth") {
|
if (type === "thisMonth") {
|
||||||
const current = new Date(now);
|
const current = new Date(now);
|
||||||
const begin = new Date(current.getFullYear(), current.getMonth(), 1);
|
const begin = new Date(current.getFullYear(), current.getMonth(), 1);
|
||||||
|
|||||||
Reference in New Issue
Block a user