add registration event rank
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { axios, type Page, type PageResult } from "timi-web";
|
import { axios, type Page, type PageResult } from "timi-web";
|
||||||
import type {
|
import type {
|
||||||
CustomerRegisterStatView,
|
CustomerRegisterStatView,
|
||||||
|
CustomerRegistrationRankView,
|
||||||
RegistrationEventRecordCalendarView,
|
RegistrationEventRecordCalendarView,
|
||||||
RegistrationEventRecord,
|
RegistrationEventRecord,
|
||||||
RegistrationEventRecordCreateReq,
|
RegistrationEventRecordCreateReq,
|
||||||
@@ -64,6 +65,10 @@ async function allCustomerRangeStat(req: RegistrationEventRecordStatReq): Promis
|
|||||||
return await axios.post(`${BASE_URI}/all-customer/range/stat`, req);
|
return await axios.post(`${BASE_URI}/all-customer/range/stat`, req);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function customerRegistrationRank(req?: Partial<RegistrationEventRecordRangeReq>): Promise<CustomerRegistrationRankView[]> {
|
||||||
|
return await axios.post(`${BASE_URI}/customer/registration-rank`, req);
|
||||||
|
}
|
||||||
|
|
||||||
async function customerRegistrationEventDailyStat(req: RegistrationEventRecordTrendStatReq): Promise<RegistrationEventRecordDailyStatView[]> {
|
async function customerRegistrationEventDailyStat(req: RegistrationEventRecordTrendStatReq): Promise<RegistrationEventRecordDailyStatView[]> {
|
||||||
return await axios.post(`${BASE_URI}/customer/registration-event/daily/stat`, req);
|
return await axios.post(`${BASE_URI}/customer/registration-event/daily/stat`, req);
|
||||||
}
|
}
|
||||||
@@ -84,6 +89,7 @@ export default {
|
|||||||
remove,
|
remove,
|
||||||
customerRangeStat,
|
customerRangeStat,
|
||||||
allCustomerRangeStat,
|
allCustomerRangeStat,
|
||||||
|
customerRegistrationRank,
|
||||||
customerRegistrationEventDailyStat,
|
customerRegistrationEventDailyStat,
|
||||||
allCustomerRegistrationEventDailyStat
|
allCustomerRegistrationEventDailyStat
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,6 +23,24 @@
|
|||||||
@status-change="handleScanStatusChange"
|
@status-change="handleScanStatusChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="!selectedCustomer" class="filter-line">
|
||||||
|
<t-dropdown-menu class="filter-menu">
|
||||||
|
<t-dropdown-item
|
||||||
|
v-model="rankDateFilterType"
|
||||||
|
:label="rankDateFilterLabel"
|
||||||
|
:options="rankDateOptions"
|
||||||
|
@change="selectRankDateFilter"
|
||||||
|
/>
|
||||||
|
<t-dropdown-item
|
||||||
|
v-model="rankRegistrationEventIdList"
|
||||||
|
:label="rankRegistrationEventFilterLabel"
|
||||||
|
:options="rankRegistrationEventOptions"
|
||||||
|
options-columns="2"
|
||||||
|
multiple
|
||||||
|
@change="reloadCustomerRegistrationRank"
|
||||||
|
/>
|
||||||
|
</t-dropdown-menu>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<section v-if="selectedCustomer" class="summary">
|
<section v-if="selectedCustomer" class="summary">
|
||||||
<div v-for="item in summaryList" :key="item.label" class="summary-item">
|
<div v-for="item in summaryList" :key="item.label" class="summary-item">
|
||||||
@@ -48,34 +66,94 @@
|
|||||||
<h2 v-text="item.title" />
|
<h2 v-text="item.title" />
|
||||||
<div :ref="element => setChartRef(item.id, element)" class="chart" />
|
<div :ref="element => setChartRef(item.id, element)" class="chart" />
|
||||||
</section>
|
</section>
|
||||||
<div v-if="!selectedCustomer" class="empty-block">请选择客户后查看专属图表</div>
|
<div v-if="!selectedCustomer" v-text="rankSummaryText"></div>
|
||||||
|
<t-cell-group
|
||||||
|
v-if="!selectedCustomer && customerRegistrationRankList.length"
|
||||||
|
class="rank-list"
|
||||||
|
theme="card"
|
||||||
|
>
|
||||||
|
<t-cell
|
||||||
|
v-for="item in customerRegistrationRankList"
|
||||||
|
:key="item.customerId"
|
||||||
|
class="rank-item"
|
||||||
|
:title="item.customerName || '未知客户'"
|
||||||
|
arrow
|
||||||
|
hover
|
||||||
|
@click="selectRankCustomer(item.customerId || '')"
|
||||||
|
>
|
||||||
|
<template #leftIcon>
|
||||||
|
<span class="rank-left">
|
||||||
|
<span class="rank-no" v-text="item.rank" />
|
||||||
|
<t-image
|
||||||
|
v-if="getRankCustomerPhotoURL(item)"
|
||||||
|
:src="getRankCustomerPhotoURL(item)"
|
||||||
|
:alt="item.customerName || '客户头像'"
|
||||||
|
fit="cover"
|
||||||
|
class="rank-photo"
|
||||||
|
/>
|
||||||
|
<span v-else class="rank-photo fallback" v-text="getRankCustomerInitial(item)" />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template #description>
|
||||||
|
<div v-text="getRankCustomerMeta(item)"></div>
|
||||||
|
<div class="rank-stat">
|
||||||
|
<strong v-text="`${item.totalRegistrationDays || 0} 天`" />
|
||||||
|
<small v-text="`${item.recentContinuousRegistrationDays || 0} 天连续`" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</t-cell>
|
||||||
|
</t-cell-group>
|
||||||
|
<div v-else class="empty-block" v-text="rankLoading ? '正在加载排行榜' : '暂无登记排行数据'" />
|
||||||
<customer-selector
|
<customer-selector
|
||||||
v-model="selectedCustomerId"
|
v-model="selectedCustomerId"
|
||||||
v-model:visible="selectorVisible"
|
v-model:visible="selectorVisible"
|
||||||
@change="handleCustomerSelected"
|
@change="handleCustomerSelected"
|
||||||
/>
|
/>
|
||||||
|
<t-popup v-model="rankStartPickerVisible" placement="bottom">
|
||||||
|
<t-date-time-picker
|
||||||
|
v-model="rankDateRange[0]"
|
||||||
|
:default-value="rankDateRange[0]"
|
||||||
|
format="YYYY-MM-DD HH:mm"
|
||||||
|
:mode="['date', 'minute']"
|
||||||
|
title="开始时间"
|
||||||
|
@confirm="confirmRankStartTime"
|
||||||
|
/>
|
||||||
|
</t-popup>
|
||||||
|
<t-popup v-model="rankEndPickerVisible" placement="bottom">
|
||||||
|
<t-date-time-picker
|
||||||
|
v-model="rankDateRange[1]"
|
||||||
|
:default-value="rankDateRange[1]"
|
||||||
|
format="YYYY-MM-DD HH:mm"
|
||||||
|
:mode="['date', 'minute']"
|
||||||
|
title="结束时间"
|
||||||
|
@confirm="confirmRankEndTime"
|
||||||
|
/>
|
||||||
|
</t-popup>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type {ECharts, EChartsOption} from "echarts";
|
import type {ECharts, EChartsOption} from "echarts";
|
||||||
|
import type {DropdownValue} from "tdesign-mobile-vue";
|
||||||
import {BarChart, LineChart} from "echarts/charts";
|
import {BarChart, LineChart} from "echarts/charts";
|
||||||
import {GridComponent, LegendComponent, TooltipComponent} from "echarts/components";
|
import {GridComponent, LegendComponent, TooltipComponent} from "echarts/components";
|
||||||
import {init, use} from "echarts/core";
|
import {init, use} from "echarts/core";
|
||||||
import {CanvasRenderer} from "echarts/renderers";
|
import {CanvasRenderer} from "echarts/renderers";
|
||||||
|
import {AttachmentAPI, Time} from "timi-web";
|
||||||
import CustomerAPI from "@/api/CustomerAPI";
|
import CustomerAPI from "@/api/CustomerAPI";
|
||||||
import RegistrationEventAPI from "@/api/RegistrationEventAPI";
|
import RegistrationEventAPI from "@/api/RegistrationEventAPI";
|
||||||
import RegistrationEventRecordAPI from "@/api/RegistrationEventRecordAPI";
|
import RegistrationEventRecordAPI from "@/api/RegistrationEventRecordAPI";
|
||||||
import type {Customer} from "@/types/Customer";
|
import type {Customer} from "@/types/Customer";
|
||||||
import type {RegistrationEvent} from "@/types/RegistrationEvent";
|
import type {RegistrationEvent} from "@/types/RegistrationEvent";
|
||||||
import type {RegistrationEventRecord} from "@/types/RegistrationEventRecord";
|
import type {CustomerRegistrationRankView, RegistrationEventRecord, RegistrationEventRecordRangeReq} from "@/types/RegistrationEventRecord";
|
||||||
import CodeScanner from "@/components/CodeScanner.vue";
|
import CodeScanner from "@/components/CodeScanner.vue";
|
||||||
import CustomerSelector from "@/components/CustomerSelector.vue";
|
import CustomerSelector from "@/components/CustomerSelector.vue";
|
||||||
import RegistrationEventRecordCalendar from "@/components/RegistrationEventRecordCalendar.vue";
|
import RegistrationEventRecordCalendar from "@/components/RegistrationEventRecordCalendar.vue";
|
||||||
import {useSettingStore} from "@/store/settingStore";
|
import {useSettingStore} from "@/store/settingStore";
|
||||||
import {Time} from "timi-web";
|
|
||||||
import {gaoPermissionCodes} from "@/utils/gaoPermission";
|
import {gaoPermissionCodes} from "@/utils/gaoPermission";
|
||||||
|
|
||||||
|
type RankDateFilterType = "all" | "today" | "thisMonth" | "custom";
|
||||||
|
|
||||||
interface SummaryItem {
|
interface SummaryItem {
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
@@ -104,6 +182,12 @@ const settingStore = useSettingStore();
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const chartRefs = new Map<string, HTMLElement>();
|
const chartRefs = new Map<string, HTMLElement>();
|
||||||
const chartInstances = new Map<string, ECharts>();
|
const chartInstances = new Map<string, ECharts>();
|
||||||
|
const rankDateOptions: Array<{ label: string; value: RankDateFilterType }> = [
|
||||||
|
{label: "全部", value: "all"},
|
||||||
|
{label: "今天", value: "today"},
|
||||||
|
{label: "本月", value: "thisMonth"},
|
||||||
|
{label: "自定义范围", value: "custom"}
|
||||||
|
];
|
||||||
const routeCustomerId = computed(() => String(route.params.id || ""));
|
const routeCustomerId = computed(() => String(route.params.id || ""));
|
||||||
const routeCustomerCode = computed(() => String(route.query.customerCode || ""));
|
const routeCustomerCode = computed(() => String(route.query.customerCode || ""));
|
||||||
const customerCode = ref(routeCustomerCode.value);
|
const customerCode = ref(routeCustomerCode.value);
|
||||||
@@ -117,6 +201,13 @@ const selectedRegistrationEventIdList = ref<string[]>([]);
|
|||||||
const monthCursor = ref(getMonthStart(new Date()));
|
const monthCursor = ref(getMonthStart(new Date()));
|
||||||
const monthRecords = ref<RegistrationEventRecord[]>([]);
|
const monthRecords = ref<RegistrationEventRecord[]>([]);
|
||||||
const calendarLoading = ref(false);
|
const calendarLoading = ref(false);
|
||||||
|
const customerRegistrationRankList = ref<CustomerRegistrationRankView[]>([]);
|
||||||
|
const rankDateFilterType = ref<RankDateFilterType>("all");
|
||||||
|
const rankDateRange = ref(buildRankPresetRange("all"));
|
||||||
|
const rankRegistrationEventIdList = ref<string[]>([]);
|
||||||
|
const rankLoading = ref(false);
|
||||||
|
const rankStartPickerVisible = ref(false);
|
||||||
|
const rankEndPickerVisible = ref(false);
|
||||||
let latestDecodedText = "";
|
let latestDecodedText = "";
|
||||||
|
|
||||||
const registrationEventNameMap = computed(() => {
|
const registrationEventNameMap = computed(() => {
|
||||||
@@ -180,6 +271,27 @@ const chartList = computed<ChartProfile[]>(() => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
const rankDateFilterLabel = computed(() => rankDateOptions.find(item => item.value === rankDateFilterType.value)?.label || "全部");
|
||||||
|
const rankRegistrationEventOptions = computed(() => registrationEventList.value.map(item => ({
|
||||||
|
label: item.name || "未命名事件",
|
||||||
|
value: item.id || ""
|
||||||
|
})).filter(item => item.value));
|
||||||
|
const rankRegistrationEventFilterLabel = computed(() => {
|
||||||
|
if (!rankRegistrationEventIdList.value.length || rankRegistrationEventIdList.value.length === registrationEventList.value.length) {
|
||||||
|
return "全部事件";
|
||||||
|
}
|
||||||
|
|
||||||
|
return registrationEventList.value
|
||||||
|
.filter(item => item.id && rankRegistrationEventIdList.value.includes(item.id))
|
||||||
|
.map(item => item.name || "未命名事件")
|
||||||
|
.join("、");
|
||||||
|
});
|
||||||
|
const rankSummaryText = computed(() => {
|
||||||
|
if (rankDateFilterType.value === "custom") {
|
||||||
|
return `${rankDateRange.value[0] || "-"} ~ ${rankDateRange.value[1] || "-"}|${rankRegistrationEventFilterLabel.value}`;
|
||||||
|
}
|
||||||
|
return `范围:${rankDateFilterLabel.value}|${rankRegistrationEventFilterLabel.value}`;
|
||||||
|
});
|
||||||
|
|
||||||
watch(chartList, async () => {
|
watch(chartList, async () => {
|
||||||
await nextTick();
|
await nextTick();
|
||||||
@@ -208,6 +320,7 @@ watch([selectedCustomer, monthCursor], async () => {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadActions();
|
await loadActions();
|
||||||
|
await loadCustomerRegistrationRank();
|
||||||
if (routeCustomerId.value) {
|
if (routeCustomerId.value) {
|
||||||
await loadCustomerById(routeCustomerId.value);
|
await loadCustomerById(routeCustomerId.value);
|
||||||
} else if (customerCode.value) {
|
} else if (customerCode.value) {
|
||||||
@@ -262,6 +375,7 @@ function resizeCharts(): void {
|
|||||||
async function loadActions(): Promise<void> {
|
async function loadActions(): Promise<void> {
|
||||||
registrationEventList.value = await RegistrationEventAPI.enabledList();
|
registrationEventList.value = await RegistrationEventAPI.enabledList();
|
||||||
selectedRegistrationEventIdList.value = registrationEventList.value.map(item => item.id).filter((item): item is string => Boolean(item));
|
selectedRegistrationEventIdList.value = registrationEventList.value.map(item => item.id).filter((item): item is string => Boolean(item));
|
||||||
|
rankRegistrationEventIdList.value = selectedRegistrationEventIdList.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadCustomer(code: string): Promise<void> {
|
async function loadCustomer(code: string): Promise<void> {
|
||||||
@@ -309,6 +423,10 @@ async function handleCustomerSelected(id: string): Promise<void> {
|
|||||||
await loadCustomerById(id);
|
await loadCustomerById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function selectRankCustomer(id: string): Promise<void> {
|
||||||
|
await loadCustomerById(id);
|
||||||
|
}
|
||||||
|
|
||||||
async function loadMonthRecords(): Promise<void> {
|
async function loadMonthRecords(): Promise<void> {
|
||||||
const customerId = selectedCustomer.value?.id;
|
const customerId = selectedCustomer.value?.id;
|
||||||
if (!customerId) {
|
if (!customerId) {
|
||||||
@@ -330,6 +448,54 @@ async function loadMonthRecords(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadCustomerRegistrationRank(): Promise<void> {
|
||||||
|
rankLoading.value = true;
|
||||||
|
try {
|
||||||
|
customerRegistrationRankList.value = await RegistrationEventRecordAPI.customerRegistrationRank(buildRankStatReq());
|
||||||
|
} finally {
|
||||||
|
rankLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reloadCustomerRegistrationRank(): Promise<void> {
|
||||||
|
await loadCustomerRegistrationRank();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectRankDateFilter(value: DropdownValue): Promise<void> {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rankDateFilterType.value = String(value) as RankDateFilterType;
|
||||||
|
if (rankDateFilterType.value !== "custom") {
|
||||||
|
rankDateRange.value = buildRankPresetRange(rankDateFilterType.value);
|
||||||
|
await loadCustomerRegistrationRank();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rankDateRange.value[0] || !rankDateRange.value[1]) {
|
||||||
|
rankDateRange.value = buildRankPresetRange("today");
|
||||||
|
}
|
||||||
|
rankStartPickerVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmRankStartTime(): void {
|
||||||
|
rankStartPickerVisible.value = false;
|
||||||
|
rankEndPickerVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmRankEndTime(): Promise<void> {
|
||||||
|
rankEndPickerVisible.value = false;
|
||||||
|
normalizeRankDateRange();
|
||||||
|
await loadCustomerRegistrationRank();
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeRankDateRange(): void {
|
||||||
|
if (rankDateRange.value[0] && rankDateRange.value[1] && rankDateRange.value[1] < rankDateRange.value[0]) {
|
||||||
|
rankDateRange.value = [rankDateRange.value[1], rankDateRange.value[0]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- 扫码 ----------
|
// ---------- 扫码 ----------
|
||||||
|
|
||||||
async function toggleScanner(): Promise<void> {
|
async function toggleScanner(): Promise<void> {
|
||||||
@@ -370,6 +536,24 @@ function getRecordRegistrationEventName(record?: RegistrationEventRecord): strin
|
|||||||
return record.registrationEvent?.name || (record.registrationEventId ? registrationEventNameMap.value.get(record.registrationEventId) : "") || "未知事件";
|
return record.registrationEvent?.name || (record.registrationEventId ? registrationEventNameMap.value.get(record.registrationEventId) : "") || "未知事件";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getRankCustomerMeta(item: CustomerRegistrationRankView): string {
|
||||||
|
const codeText = item.customerCode ? `编码 ${item.customerCode}` : "暂无编码";
|
||||||
|
const countText = `${item.totalRegistrationCount || 0} 次登记`;
|
||||||
|
const latestText = item.latestRegisteredAt ? `最近 ${formatDateTime(item.latestRegisteredAt)}` : "暂无最近登记";
|
||||||
|
return `${codeText}|${countText}|${latestText}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRankCustomerPhotoURL(item: CustomerRegistrationRankView): string {
|
||||||
|
if (!item.customerPhotoAttachmentId) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return AttachmentAPI.getReadURL(item.customerPhotoAttachmentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRankCustomerInitial(item: CustomerRegistrationRankView): string {
|
||||||
|
return (item.customerName || "客").slice(0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- 图表 ----------
|
// ---------- 图表 ----------
|
||||||
|
|
||||||
function buildBaseOption(): EChartsOption {
|
function buildBaseOption(): EChartsOption {
|
||||||
@@ -522,6 +706,52 @@ function getDateEnd(date: Date): Date {
|
|||||||
return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59, 999);
|
return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59, 999);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildRankStatReq(): Partial<RegistrationEventRecordRangeReq> | undefined {
|
||||||
|
const selectedAllEvent = !rankRegistrationEventIdList.value.length || rankRegistrationEventIdList.value.length === registrationEventList.value.length;
|
||||||
|
if (rankDateFilterType.value === "all") {
|
||||||
|
return selectedAllEvent ? undefined : {
|
||||||
|
registrationEventIdList: rankRegistrationEventIdList.value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
beginRegisteredAt: Time.toTimestamp(parseRankDateTime(rankDateRange.value[0], false)),
|
||||||
|
endRegisteredAt: Time.toTimestamp(parseRankDateTime(rankDateRange.value[1], true)),
|
||||||
|
registrationEventIdList: selectedAllEvent ? undefined : rankRegistrationEventIdList.value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRankPresetRange(type: Exclude<RankDateFilterType, "custom">): [string, string] {
|
||||||
|
if (type === "all") {
|
||||||
|
return ["", ""];
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
if (type === "thisMonth") {
|
||||||
|
const monthStart = getMonthStart(now);
|
||||||
|
return [`${Time.formatDate(monthStart)} 00:00`, `${Time.formatDate(getDateEnd(now))} 23:59`];
|
||||||
|
}
|
||||||
|
|
||||||
|
const dayStart = new Date(now);
|
||||||
|
dayStart.setHours(0, 0, 0, 0);
|
||||||
|
return [`${Time.formatDate(dayStart)} 00:00`, `${Time.formatDate(getDateEnd(now))} 23:59`];
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseRankDateTime(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);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateTime(value: number): string {
|
||||||
|
const date = Time.toDateObject(value);
|
||||||
|
return date ? Time.formatDateTime(date) : "-";
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
@@ -556,6 +786,30 @@ function getDateEnd(date: Date): Date {
|
|||||||
width: 3rem;
|
width: 3rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filter-line {
|
||||||
|
.filter-menu {
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid rgba(148, 163, 184, .18);
|
||||||
|
border-radius: .625rem;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.t-dropdown-menu__item) {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.t-dropdown-menu__title) {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary {
|
.summary {
|
||||||
@@ -607,6 +861,120 @@ function getDateEnd(date: Date): Date {
|
|||||||
margin: 0 1rem;
|
margin: 0 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rank-section {
|
||||||
|
gap: .75rem;
|
||||||
|
margin: 0 1rem;
|
||||||
|
display: flex;
|
||||||
|
padding: 1rem;
|
||||||
|
background: rgba(255, 255, 255, .92);
|
||||||
|
box-shadow: var(--td-shadow-1);
|
||||||
|
border-radius: .75rem;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.rank-header {
|
||||||
|
gap: .75rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: .25rem 0 0 0;
|
||||||
|
color: #52607a;
|
||||||
|
font-size: .75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-list {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-item {
|
||||||
|
:deep(.t-cell__title) {
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.t-cell__title-text),
|
||||||
|
:deep(.t-cell__description) {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.t-cell__right) {
|
||||||
|
min-width: 4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-left {
|
||||||
|
gap: .5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-no {
|
||||||
|
width: 2rem;
|
||||||
|
color: #2563eb;
|
||||||
|
height: 2rem;
|
||||||
|
display: flex;
|
||||||
|
background: #dbeafe;
|
||||||
|
font-size: .875rem;
|
||||||
|
align-items: center;
|
||||||
|
font-weight: 700;
|
||||||
|
border-radius: 999px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-photo {
|
||||||
|
width: 2.75rem;
|
||||||
|
height: 2.75rem;
|
||||||
|
border-radius: .5rem;
|
||||||
|
|
||||||
|
&.fallback {
|
||||||
|
color: #2563eb;
|
||||||
|
display: flex;
|
||||||
|
font-size: 1rem;
|
||||||
|
background: #dbeafe;
|
||||||
|
align-items: center;
|
||||||
|
font-weight: 700;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-stat {
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
text-align: right;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
strong,
|
||||||
|
small {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: .9375rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
margin-top: .25rem;
|
||||||
|
color: #52607a;
|
||||||
|
font-size: .75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.chart-section {
|
.chart-section {
|
||||||
h2 {
|
h2 {
|
||||||
margin-bottom: .875rem;
|
margin-bottom: .875rem;
|
||||||
@@ -633,6 +1001,13 @@ function getDateEnd(date: Date): Date {
|
|||||||
.customer-data-charts-page {
|
.customer-data-charts-page {
|
||||||
.selector {
|
.selector {
|
||||||
background: #10161f;
|
background: #10161f;
|
||||||
|
|
||||||
|
.filter-line {
|
||||||
|
.filter-menu {
|
||||||
|
background: #172033;
|
||||||
|
border-color: rgba(148, 163, 184, .16);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary {
|
.summary {
|
||||||
@@ -651,7 +1026,8 @@ function getDateEnd(date: Date): Date {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.calendar-section,
|
.calendar-section,
|
||||||
.chart-section {
|
.chart-section,
|
||||||
|
.rank-section {
|
||||||
background: rgba(15, 23, 42, .88);
|
background: rgba(15, 23, 42, .88);
|
||||||
box-shadow: 0 .75rem 2rem rgba(2, 6, 23, .32);
|
box-shadow: 0 .75rem 2rem rgba(2, 6, 23, .32);
|
||||||
|
|
||||||
@@ -660,6 +1036,40 @@ function getDateEnd(date: Date): Date {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rank-section {
|
||||||
|
.rank-header {
|
||||||
|
p {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-filter {
|
||||||
|
border-color: rgba(148, 163, 184, .16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-no {
|
||||||
|
color: #bfdbfe;
|
||||||
|
background: rgba(37, 99, 235, .28);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-photo {
|
||||||
|
&.fallback {
|
||||||
|
color: #bfdbfe;
|
||||||
|
background: rgba(37, 99, 235, .28);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-stat {
|
||||||
|
strong {
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.empty-block {
|
.empty-block {
|
||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
background: rgba(30, 41, 59, .9);
|
background: rgba(30, 41, 59, .9);
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ const menuGroupList = computed<MenuGroup[]>(() => [
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "图表",
|
title: "统计",
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
icon: "usergroup",
|
icon: "usergroup",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const chartRoutes: RouteRecordRaw[] = [
|
|||||||
depth: 3,
|
depth: 3,
|
||||||
navBarVisible: true,
|
navBarVisible: true,
|
||||||
navBarCanBack: true,
|
navBarCanBack: true,
|
||||||
navBarTitle: "全局数据图表",
|
navBarTitle: "全局数据",
|
||||||
tabBarVisible: false,
|
tabBarVisible: false,
|
||||||
bodyBackground: "#F5F5F8",
|
bodyBackground: "#F5F5F8",
|
||||||
darkBodyBackground: "#10161f"
|
darkBodyBackground: "#10161f"
|
||||||
@@ -23,7 +23,7 @@ const chartRoutes: RouteRecordRaw[] = [
|
|||||||
depth: 3,
|
depth: 3,
|
||||||
navBarVisible: true,
|
navBarVisible: true,
|
||||||
navBarCanBack: true,
|
navBarCanBack: true,
|
||||||
navBarTitle: "客户数据图表",
|
navBarTitle: "客户数据",
|
||||||
tabBarVisible: false,
|
tabBarVisible: false,
|
||||||
bodyBackground: "#F5F5F8",
|
bodyBackground: "#F5F5F8",
|
||||||
darkBodyBackground: "#10161f"
|
darkBodyBackground: "#10161f"
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ export type RegistrationEventRecordRangeReq = {
|
|||||||
|
|
||||||
export type RegistrationEventRecordStatReq = {
|
export type RegistrationEventRecordStatReq = {
|
||||||
customerId?: string;
|
customerId?: string;
|
||||||
beginRegisteredAt: number;
|
beginRegisteredAt?: number;
|
||||||
endRegisteredAt: number;
|
endRegisteredAt?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RegistrationEventRecordTrendStatReq = {
|
export type RegistrationEventRecordTrendStatReq = {
|
||||||
@@ -56,6 +56,18 @@ export type CustomerRegisterStatView = {
|
|||||||
latestRegisteredAt?: number;
|
latestRegisteredAt?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CustomerRegistrationRankView = {
|
||||||
|
rank?: number;
|
||||||
|
customerId?: string;
|
||||||
|
customerCode?: string;
|
||||||
|
customerName?: string;
|
||||||
|
customerPhotoAttachmentId?: string;
|
||||||
|
totalRegistrationDays?: number;
|
||||||
|
totalRegistrationCount?: number;
|
||||||
|
recentContinuousRegistrationDays?: number;
|
||||||
|
latestRegisteredAt?: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type RegistrationEventRecordDailyStatView = {
|
export type RegistrationEventRecordDailyStatView = {
|
||||||
dateValue?: number;
|
dateValue?: number;
|
||||||
dailyCount?: number;
|
dailyCount?: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user