add date filter for CustomerList.vue
This commit is contained in:
@@ -9,6 +9,8 @@ export type CustomerPage = Page<Customer> & {
|
||||
keyword?: string;
|
||||
storeId?: string;
|
||||
tagIdList?: string[];
|
||||
beginCreatedAt?: number;
|
||||
endCreatedAt?: number;
|
||||
};
|
||||
|
||||
async function list(page: CustomerPage): Promise<PageResult<Customer>> {
|
||||
@@ -19,6 +21,8 @@ async function list(page: CustomerPage): Promise<PageResult<Customer>> {
|
||||
keyword: page.keyword || page.likesExample?.code || page.likesExample?.name || page.likesExample?.telephone,
|
||||
storeId: page.storeId || req.equalsExample?.storeId,
|
||||
tagIdList: page.tagIdList || [],
|
||||
beginCreatedAt: page.beginCreatedAt,
|
||||
endCreatedAt: page.endCreatedAt,
|
||||
orderMap: page.orderMap || {}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -21,19 +21,19 @@
|
||||
</t-button>
|
||||
</template>
|
||||
</t-input>
|
||||
<t-button
|
||||
v-if="canReadTag"
|
||||
class="tag-filter"
|
||||
size="small"
|
||||
theme="primary"
|
||||
variant="outline"
|
||||
@click="tagSelectorVisible = true"
|
||||
>
|
||||
<template #icon>
|
||||
<t-icon name="tag" />
|
||||
</template>
|
||||
<span v-text="tagFilterText" />
|
||||
</t-button>
|
||||
<t-dropdown-menu ref="filterMenu" class="menus">
|
||||
<t-dropdown-item
|
||||
v-model="dateFilterType"
|
||||
:label="dateFilterLabel"
|
||||
:options="dateOptions"
|
||||
@change="selectDateFilter"
|
||||
/>
|
||||
<t-dropdown-item
|
||||
v-if="canReadTag"
|
||||
:label="tagFilterText"
|
||||
@open="handleTagFilterOpen"
|
||||
/>
|
||||
</t-dropdown-menu>
|
||||
</div>
|
||||
<div v-if="scannerVisible" class="scanner-wrapper">
|
||||
<code-scanner
|
||||
@@ -54,6 +54,20 @@
|
||||
/>
|
||||
<t-button size="extra-small" variant="text" @click="clearTagFilter">清空</t-button>
|
||||
</div>
|
||||
<t-cell-group v-if="dateFilterType !== 'all'" class="date-summary" theme="card">
|
||||
<t-cell
|
||||
title="开始时间"
|
||||
:note="dateRange[0] || '-'"
|
||||
arrow
|
||||
@click="openStartTime"
|
||||
/>
|
||||
<t-cell
|
||||
title="结束时间"
|
||||
:note="dateRange[1] || '-'"
|
||||
arrow
|
||||
@click="openEndTime"
|
||||
/>
|
||||
</t-cell-group>
|
||||
<t-pull-down-refresh
|
||||
class="list-wrapper fix-last-cell-line"
|
||||
v-model="isRefreshing"
|
||||
@@ -106,9 +120,34 @@
|
||||
/>
|
||||
</div>
|
||||
<nav-bar-action-menu v-if="canShowActionMenu" :actions="actionList" />
|
||||
<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 type { DropdownValue } from "tdesign-mobile-vue";
|
||||
import { Message } from "@/utils/message";
|
||||
import { Time, Toolkit, WebToolkit } from "timi-web";
|
||||
import { type Page, type PageResult, useUserStore } from "timi-web-api";
|
||||
@@ -128,6 +167,8 @@ defineOptions({
|
||||
name: "CustomerList"
|
||||
});
|
||||
|
||||
type DateFilterType = "today" | "yesterday" | "lastWeek" | "thisMonth" | "lastMonth" | "all" | "custom";
|
||||
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const currentStoreStore = useCurrentStoreStore();
|
||||
@@ -148,6 +189,23 @@ const canShowActionMenu = computed(() => canCreateCustomer.value || canExportCus
|
||||
const scannerVisible = ref(false);
|
||||
const scanStatus = ref("点击扫码开始识别");
|
||||
let latestDecodedText = "";
|
||||
const dateOptions: Array<{ label: string; value: DateFilterType }> = [
|
||||
{ label: "全部", value: "all" },
|
||||
{ label: "今天", value: "today" },
|
||||
{ label: "昨天", value: "yesterday" },
|
||||
{ label: "上周", value: "lastWeek" },
|
||||
{ label: "本月", value: "thisMonth" },
|
||||
{ label: "上个月", value: "lastMonth" },
|
||||
{ label: "自定义范围", value: "custom" }
|
||||
];
|
||||
const dateFilterType = ref<DateFilterType>("all");
|
||||
const dateRange = ref<[string, string]>(buildPresetRange("all"));
|
||||
const startDraft = ref("");
|
||||
const endDraft = ref("");
|
||||
const startVisible = ref(false);
|
||||
const endVisible = ref(false);
|
||||
const filterMenu = ref<{ toggle?: (index?: number) => void }>();
|
||||
const dateFilterLabel = computed(() => `录入时间:${dateOptions.find(item => item.value === dateFilterType.value)?.label || "全部"}`);
|
||||
|
||||
async function toggleScanner(): Promise<void> {
|
||||
scannerVisible.value = !scannerVisible.value;
|
||||
@@ -205,6 +263,65 @@ const actionList = computed<NavBarAction[]>(() => {
|
||||
return list;
|
||||
});
|
||||
|
||||
// ---------- 录入时间筛选 ----------
|
||||
|
||||
async function selectDateFilter(value: DropdownValue): Promise<void> {
|
||||
if (Array.isArray(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
dateFilterType.value = String(value) as DateFilterType;
|
||||
if (dateFilterType.value !== "custom") {
|
||||
dateRange.value = buildPresetRange(dateFilterType.value);
|
||||
await refresh();
|
||||
}
|
||||
}
|
||||
|
||||
function openStartTime(): void {
|
||||
startDraft.value = dateRange.value[0] || `${Time.formatDate(new Date())} 00:00`;
|
||||
startVisible.value = true;
|
||||
}
|
||||
|
||||
function openEndTime(): void {
|
||||
endDraft.value = dateRange.value[1] || `${Time.formatDate(new Date())} 23:59`;
|
||||
endVisible.value = true;
|
||||
}
|
||||
|
||||
function cancelStartTime(): void {
|
||||
startVisible.value = false;
|
||||
startDraft.value = dateRange.value[0];
|
||||
}
|
||||
|
||||
function cancelEndTime(): void {
|
||||
endVisible.value = false;
|
||||
endDraft.value = dateRange.value[1];
|
||||
}
|
||||
|
||||
async function confirmStartTime(value?: string | number): Promise<void> {
|
||||
startVisible.value = false;
|
||||
dateRange.value = [String(value || startDraft.value), dateRange.value[1]];
|
||||
await confirmCustomRange();
|
||||
}
|
||||
|
||||
async function confirmEndTime(value?: string | number): Promise<void> {
|
||||
endVisible.value = false;
|
||||
dateRange.value = [dateRange.value[0], String(value || endDraft.value)];
|
||||
await confirmCustomRange();
|
||||
}
|
||||
|
||||
async function confirmCustomRange(): Promise<void> {
|
||||
if (dateRange.value[0] && dateRange.value[1] && dateRange.value[1] < dateRange.value[0]) {
|
||||
dateRange.value = [dateRange.value[1], dateRange.value[0]];
|
||||
}
|
||||
dateFilterType.value = "custom";
|
||||
await refresh();
|
||||
}
|
||||
|
||||
function handleTagFilterOpen(): void {
|
||||
tagSelectorVisible.value = true;
|
||||
filterMenu.value?.toggle?.();
|
||||
}
|
||||
|
||||
// ---------- 标签筛选 ----------
|
||||
|
||||
const selectedTagList = ref<Tag[]>([]);
|
||||
@@ -244,11 +361,14 @@ const page = reactive<Page<Customer>>({
|
||||
});
|
||||
|
||||
const fetch = Toolkit.debounce(async () => {
|
||||
const [beginCreatedAt, endCreatedAt] = buildCreatedAtRange();
|
||||
const result = await CustomerAPI.list({
|
||||
...page,
|
||||
keyword: keyword.value,
|
||||
storeId: currentStoreStore.currentStoreId,
|
||||
tagIdList: selectedTagIdList.value
|
||||
tagIdList: selectedTagIdList.value,
|
||||
beginCreatedAt,
|
||||
endCreatedAt
|
||||
});
|
||||
pageResult.total = result.total;
|
||||
pageResult.list = pageResult.list.concat(result.list);
|
||||
@@ -360,6 +480,65 @@ async function doExport(): Promise<void> {
|
||||
isExporting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function buildCreatedAtRange(): [number | undefined, number | undefined] {
|
||||
if (dateFilterType.value === "all") {
|
||||
return [undefined, undefined];
|
||||
}
|
||||
|
||||
return [
|
||||
dateRange.value[0] ? Time.toTimestamp(parseDateTime(dateRange.value[0], false)) : undefined,
|
||||
dateRange.value[1] ? Time.toTimestamp(parseDateTime(dateRange.value[1], true)) : undefined
|
||||
];
|
||||
}
|
||||
|
||||
function buildPresetRange(type: Exclude<DateFilterType, "custom">): [string, string] {
|
||||
if (type === "all") {
|
||||
return ["", ""];
|
||||
}
|
||||
if (type === "lastWeek") {
|
||||
const current = new Date();
|
||||
current.setHours(0, 0, 0, 0);
|
||||
const day = current.getDay() || 7;
|
||||
const thisWeekMonday = new Date(current);
|
||||
thisWeekMonday.setDate(current.getDate() - day + 1);
|
||||
const lastWeekMonday = new Date(thisWeekMonday);
|
||||
lastWeekMonday.setDate(thisWeekMonday.getDate() - 7);
|
||||
const lastWeekSunday = new Date(thisWeekMonday);
|
||||
lastWeekSunday.setDate(thisWeekMonday.getDate() - 1);
|
||||
return [`${Time.formatDate(lastWeekMonday)} 00:00`, `${Time.formatDate(lastWeekSunday)} 23:59`];
|
||||
}
|
||||
if (type === "thisMonth") {
|
||||
const current = new Date();
|
||||
const begin = new Date(current.getFullYear(), current.getMonth(), 1);
|
||||
return [`${Time.formatDate(begin)} 00:00`, `${Time.formatDate(current)} 23:59`];
|
||||
}
|
||||
if (type === "lastMonth") {
|
||||
const current = new Date();
|
||||
const begin = new Date(current.getFullYear(), current.getMonth() - 1, 1);
|
||||
const end = new Date(current.getFullYear(), current.getMonth(), 0);
|
||||
return [`${Time.formatDate(begin)} 00:00`, `${Time.formatDate(end)} 23:59`];
|
||||
}
|
||||
|
||||
const offsetMap: Record<Exclude<DateFilterType, "all" | "custom" | "lastWeek" | "thisMonth" | "lastMonth">, number> = {
|
||||
today: 0,
|
||||
yesterday: 1
|
||||
};
|
||||
const date = new Date();
|
||||
date.setHours(0, 0, 0, 0);
|
||||
date.setDate(date.getDate() - offsetMap[type]);
|
||||
return [`${Time.formatDate(date)} 00:00`, `${Time.formatDate(date)} 23:59`];
|
||||
}
|
||||
|
||||
function parseDateTime(value: string, isEnd: boolean): Date {
|
||||
if (!value) {
|
||||
return isEnd ? new Date() : new Date(0);
|
||||
}
|
||||
const [dateText, timeText = isEnd ? "23:59" : "00:00"] = value.split(" ");
|
||||
const [year, month, day] = dateText.split("-").map(Number);
|
||||
const [hour, minute] = timeText.split(":").map(Number);
|
||||
return new Date(year, month - 1, day, hour || 0, minute || 0, isEnd ? 59 : 0, 0);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
@@ -367,33 +546,38 @@ async function doExport(): Promise<void> {
|
||||
padding: var(--app-nav-height) 0 1rem 0;
|
||||
|
||||
.filter {
|
||||
gap: .5rem;
|
||||
top: var(--app-nav-height);
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
padding: .5rem;
|
||||
display: flex;
|
||||
padding: .75rem 1rem;
|
||||
position: sticky;
|
||||
background: #f5f5f8;
|
||||
box-shadow: var(--td-shadow-1);
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
|
||||
.search {
|
||||
--td-button-border-radius: .875rem;
|
||||
--td-button-border-radius: .75rem;
|
||||
--td-input-vertical-padding: .25rem 1rem;
|
||||
|
||||
width: 100%;
|
||||
border: 1px solid var(--td-component-border);
|
||||
border: 1px solid rgba(148, 163, 184, .28);
|
||||
z-index: 2;
|
||||
background: #f8fbff;
|
||||
padding-right: .3rem;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
.tag-filter {
|
||||
--td-button-border-radius: 1rem;
|
||||
.menus {
|
||||
--td-spacer-1: 6px;
|
||||
|
||||
flex: 0 0 auto;
|
||||
height: auto;
|
||||
align-self: stretch;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
margin-top: -1rem;
|
||||
padding-top: .8rem;
|
||||
background: #fff;
|
||||
border: 1px solid rgba(148, 163, 184, .18);
|
||||
border-radius: 0 0 1rem 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -431,14 +615,19 @@ async function doExport(): Promise<void> {
|
||||
:global(html.theme-dark) {
|
||||
.customer-list {
|
||||
|
||||
.filter-bar {
|
||||
.filter {
|
||||
background: #10161f;
|
||||
|
||||
.text-input {
|
||||
.search {
|
||||
color: #e2e8f0;
|
||||
background: rgba(15, 23, 42, .72);
|
||||
border-color: rgba(148, 163, 184, .16);
|
||||
}
|
||||
|
||||
.menus {
|
||||
background: #172033;
|
||||
border-color: rgba(148, 163, 184, .16);
|
||||
}
|
||||
}
|
||||
|
||||
.items {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div v-can="permissionCodes.eventRecord.read" class="registration-record-list">
|
||||
<div class="search-bar">
|
||||
<div class="search-status">
|
||||
<div class="search-date">
|
||||
<t-input
|
||||
v-model.trim="keyword"
|
||||
class="search-input"
|
||||
@@ -750,7 +750,7 @@ onUnmounted(() => {
|
||||
box-shadow: var(--td-shadow-1);
|
||||
flex-direction: column;
|
||||
|
||||
.search-status {
|
||||
.search-date {
|
||||
display: flex;
|
||||
|
||||
.search-input {
|
||||
|
||||
Reference in New Issue
Block a user