add date filter for CustomerList.vue
This commit is contained in:
@@ -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 || {}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,19 +21,19 @@
|
|||||||
</t-button>
|
</t-button>
|
||||||
</template>
|
</template>
|
||||||
</t-input>
|
</t-input>
|
||||||
<t-button
|
<t-dropdown-menu ref="filterMenu" class="menus">
|
||||||
v-if="canReadTag"
|
<t-dropdown-item
|
||||||
class="tag-filter"
|
v-model="dateFilterType"
|
||||||
size="small"
|
:label="dateFilterLabel"
|
||||||
theme="primary"
|
:options="dateOptions"
|
||||||
variant="outline"
|
@change="selectDateFilter"
|
||||||
@click="tagSelectorVisible = true"
|
/>
|
||||||
>
|
<t-dropdown-item
|
||||||
<template #icon>
|
v-if="canReadTag"
|
||||||
<t-icon name="tag" />
|
:label="tagFilterText"
|
||||||
</template>
|
@open="handleTagFilterOpen"
|
||||||
<span v-text="tagFilterText" />
|
/>
|
||||||
</t-button>
|
</t-dropdown-menu>
|
||||||
</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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user