add date filter for CustomerList.vue

This commit is contained in:
Timi
2026-09-11 23:34:53 +08:00
parent d036d756f4
commit 3d1c030a6f
3 changed files with 222 additions and 29 deletions
+4
View File
@@ -9,6 +9,8 @@ export type CustomerPage = Page<Customer> & {
keyword?: string; keyword?: string;
storeId?: string; storeId?: string;
tagIdList?: string[]; tagIdList?: string[];
beginCreatedAt?: number;
endCreatedAt?: number;
}; };
async function list(page: CustomerPage): Promise<PageResult<Customer>> { 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, keyword: page.keyword || page.likesExample?.code || page.likesExample?.name || page.likesExample?.telephone,
storeId: page.storeId || req.equalsExample?.storeId, storeId: page.storeId || req.equalsExample?.storeId,
tagIdList: page.tagIdList || [], tagIdList: page.tagIdList || [],
beginCreatedAt: page.beginCreatedAt,
endCreatedAt: page.endCreatedAt,
orderMap: page.orderMap || {} orderMap: page.orderMap || {}
}); });
} }
+215 -26
View File
@@ -21,19 +21,19 @@
</t-button> </t-button>
</template> </template>
</t-input> </t-input>
<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" v-if="canReadTag"
class="tag-filter" :label="tagFilterText"
size="small" @open="handleTagFilterOpen"
theme="primary" />
variant="outline" </t-dropdown-menu>
@click="tagSelectorVisible = true"
>
<template #icon>
<t-icon name="tag" />
</template>
<span v-text="tagFilterText" />
</t-button>
</div> </div>
<div v-if="scannerVisible" class="scanner-wrapper"> <div v-if="scannerVisible" class="scanner-wrapper">
<code-scanner <code-scanner
@@ -54,6 +54,20 @@
/> />
<t-button size="extra-small" variant="text" @click="clearTagFilter">清空</t-button> <t-button size="extra-small" variant="text" @click="clearTagFilter">清空</t-button>
</div> </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 <t-pull-down-refresh
class="list-wrapper fix-last-cell-line" class="list-wrapper fix-last-cell-line"
v-model="isRefreshing" v-model="isRefreshing"
@@ -106,9 +120,34 @@
/> />
</div> </div>
<nav-bar-action-menu v-if="canShowActionMenu" :actions="actionList" /> <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> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { DropdownValue } from "tdesign-mobile-vue";
import { Message } from "@/utils/message"; import { Message } from "@/utils/message";
import { Time, Toolkit, WebToolkit } from "timi-web"; import { Time, Toolkit, WebToolkit } from "timi-web";
import { type Page, type PageResult, useUserStore } from "timi-web-api"; import { type Page, type PageResult, useUserStore } from "timi-web-api";
@@ -128,6 +167,8 @@ defineOptions({
name: "CustomerList" name: "CustomerList"
}); });
type DateFilterType = "today" | "yesterday" | "lastWeek" | "thisMonth" | "lastMonth" | "all" | "custom";
const router = useRouter(); const router = useRouter();
const userStore = useUserStore(); const userStore = useUserStore();
const currentStoreStore = useCurrentStoreStore(); const currentStoreStore = useCurrentStoreStore();
@@ -148,6 +189,23 @@ const canShowActionMenu = computed(() => canCreateCustomer.value || canExportCus
const scannerVisible = ref(false); const scannerVisible = ref(false);
const scanStatus = ref("点击扫码开始识别"); const scanStatus = ref("点击扫码开始识别");
let latestDecodedText = ""; 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> { async function toggleScanner(): Promise<void> {
scannerVisible.value = !scannerVisible.value; scannerVisible.value = !scannerVisible.value;
@@ -205,6 +263,65 @@ const actionList = computed<NavBarAction[]>(() => {
return list; 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[]>([]); const selectedTagList = ref<Tag[]>([]);
@@ -244,11 +361,14 @@ const page = reactive<Page<Customer>>({
}); });
const fetch = Toolkit.debounce(async () => { const fetch = Toolkit.debounce(async () => {
const [beginCreatedAt, endCreatedAt] = buildCreatedAtRange();
const result = await CustomerAPI.list({ const result = await CustomerAPI.list({
...page, ...page,
keyword: keyword.value, keyword: keyword.value,
storeId: currentStoreStore.currentStoreId, storeId: currentStoreStore.currentStoreId,
tagIdList: selectedTagIdList.value tagIdList: selectedTagIdList.value,
beginCreatedAt,
endCreatedAt
}); });
pageResult.total = result.total; pageResult.total = result.total;
pageResult.list = pageResult.list.concat(result.list); pageResult.list = pageResult.list.concat(result.list);
@@ -360,6 +480,65 @@ async function doExport(): Promise<void> {
isExporting.value = false; 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> </script>
<style scoped lang="less"> <style scoped lang="less">
@@ -367,33 +546,38 @@ async function doExport(): Promise<void> {
padding: var(--app-nav-height) 0 1rem 0; padding: var(--app-nav-height) 0 1rem 0;
.filter { .filter {
gap: .5rem;
top: var(--app-nav-height); top: var(--app-nav-height);
left: 0; left: 0;
right: 0; right: 0;
z-index: 10; z-index: 10;
padding: .5rem;
display: flex; display: flex;
padding: .75rem 1rem;
position: sticky; position: sticky;
background: #f5f5f8;
box-shadow: var(--td-shadow-1); box-shadow: var(--td-shadow-1);
align-items: center; flex-direction: column;
.search { .search {
--td-button-border-radius: .875rem; --td-button-border-radius: .75rem;
--td-input-vertical-padding: .25rem 1rem; --td-input-vertical-padding: .25rem 1rem;
width: 100%; border: 1px solid rgba(148, 163, 184, .28);
border: 1px solid var(--td-component-border); z-index: 2;
background: #f8fbff;
padding-right: .3rem; padding-right: .3rem;
border-radius: 1rem; border-radius: 1rem;
} }
.tag-filter { .menus {
--td-button-border-radius: 1rem; --td-spacer-1: 6px;
flex: 0 0 auto; width: 100%;
height: auto; overflow: hidden;
align-self: stretch; 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) { :global(html.theme-dark) {
.customer-list { .customer-list {
.filter-bar { .filter {
background: #10161f; background: #10161f;
.text-input { .search {
color: #e2e8f0; color: #e2e8f0;
background: rgba(15, 23, 42, .72); background: rgba(15, 23, 42, .72);
border-color: rgba(148, 163, 184, .16); border-color: rgba(148, 163, 184, .16);
} }
.menus {
background: #172033;
border-color: rgba(148, 163, 184, .16);
}
} }
.items { .items {
+2 -2
View File
@@ -1,7 +1,7 @@
<template> <template>
<div v-can="permissionCodes.eventRecord.read" class="registration-record-list"> <div v-can="permissionCodes.eventRecord.read" class="registration-record-list">
<div class="search-bar"> <div class="search-bar">
<div class="search-status"> <div class="search-date">
<t-input <t-input
v-model.trim="keyword" v-model.trim="keyword"
class="search-input" class="search-input"
@@ -750,7 +750,7 @@ onUnmounted(() => {
box-shadow: var(--td-shadow-1); box-shadow: var(--td-shadow-1);
flex-direction: column; flex-direction: column;
.search-status { .search-date {
display: flex; display: flex;
.search-input { .search-input {