update customer charts style

This commit is contained in:
Timi
2026-08-12 16:47:19 +08:00
parent 826f1b2dd0
commit 0f7871250e
9 changed files with 158 additions and 180 deletions
+1 -1
View File
@@ -198,7 +198,7 @@ function clamp(value: number, min: number, max: number): number {
} }
.selection { .selection {
height: .25rem; height: .5rem;
position: absolute; position: absolute;
background: var(--td-brand-color); background: var(--td-brand-color);
border-radius: 999rem; border-radius: 999rem;
+2 -2
View File
@@ -1,5 +1,5 @@
<template> <template>
<div class="customer-profile-card"> <div v-if="customer" class="customer-profile-card">
<button v-if="photoURL" type="button" class="avatar-trigger" @click="handlePhotoClick"> <button v-if="photoURL" type="button" class="avatar-trigger" @click="handlePhotoClick">
<t-image :src="photoURL" :alt="customerName" fit="cover" class="avatar" /> <t-image :src="photoURL" :alt="customerName" fit="cover" class="avatar" />
</button> </button>
@@ -8,7 +8,7 @@
<div class="header"> <div class="header">
<div class="name"> <div class="name">
<span v-text="customerName"></span> <span v-text="customerName"></span>
<gender-icon v-if="customer?.gender" :value="customer.gender" size="1rem" /> <gender-icon v-if="customer.gender" :value="customer.gender" size="1rem" />
</div> </div>
<span class="gray" v-text="customerCode" /> <span class="gray" v-text="customerCode" />
</div> </div>
+47 -112
View File
@@ -1,24 +1,18 @@
<template> <template>
<div v-can="permissionCodes.stat.read" class="customer-data-charts-page"> <div v-can="permissionCodes.stat.read" class="customer-data-charts-page">
<div class="header"> <div class="header bg-white">
<div class="selector bg-white" :class="{ 'selected' : selectedCustomer }"> <div class="buttons">
<div class="line buttons"> <t-button class="selector" size="small" theme="primary" variant="outline" @click="selectorVisible = true">
<t-button
v-if="selectedCustomer"
theme="primary"
@click="clearCustomerSelection"
>清除</t-button>
<t-button class="customer" block theme="primary" variant="outline" @click="selectorVisible = true">
<template #icon> <template #icon>
<t-icon name="user-search" /> <t-icon name="user-search" />
</template> </template>
<span v-text="selectedCustomer ? selectedCustomer.name : '选择客户'" /> <span>选择客户</span>
</t-button> </t-button>
<t-button theme="primary" @click="toggleScanner"> <t-button size="small" theme="primary" @click="toggleScanner">
<template #icon> <template #icon>
<t-icon name="scan" /> <t-icon name="scan" />
</template> </template>
<span>扫码</span> <span v-text="scannerVisible ? '关闭扫码' : '扫码'" />
</t-button> </t-button>
</div> </div>
<div v-if="scannerVisible" class="line scanner-wrapper"> <div v-if="scannerVisible" class="line scanner-wrapper">
@@ -31,14 +25,14 @@
/> />
</div> </div>
</div> </div>
</div> <customer-profile-card class="customer" :customer="selectedCustomer" />
<section v-if="selectedCustomer" class="summary"> <div class="charts">
<customer-chart-summary <customer-chart-summary
v-if="selectedCustomer"
:customer="selectedCustomer" :customer="selectedCustomer"
:month-cursor="monthCursor" :month-cursor="monthCursor"
:selected-event-id-list="selectedEventIdList" :selected-event-id-list="selectedEventIdList"
/> />
</section>
<customer-event-record-calendar-section <customer-event-record-calendar-section
v-if="selectedCustomer" v-if="selectedCustomer"
v-model="monthCursor" v-model="monthCursor"
@@ -64,6 +58,7 @@
@change="handleCustomerSelected" @change="handleCustomerSelected"
/> />
</div> </div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -83,25 +78,15 @@ defineOptions({
name: "CustomerDataCharts" name: "CustomerDataCharts"
}); });
const settingStore = useSettingStore();
const router = useRouter();
const route = useRoute(); const route = useRoute();
const settingStore = useSettingStore();
// ---------- 路由 ----------
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);
const selectedCustomerId = ref(routeCustomerId.value); const selectedCustomerId = ref(routeCustomerId.value);
const selectorVisible = ref(false);
const scannerVisible = ref(false);
const scanStatus = ref("点击扫码开始识别。");
const selectedCustomer = ref<Customer>();
const selectedEventIdList = ref<string[]>([]);
const monthCursor = ref(getMonthStart(new Date()));
const chartBeginDate = getDateStart(addMonths(new Date(), -1));
const chartEndDate = getDateEnd(new Date());
const chartDateTextList = buildDateTextList(chartBeginDate, chartEndDate);
let latestDecodedText = "";
const monthDateTextList = computed(() => buildMonthDateTextList(monthCursor.value));
watch(routeCustomerCode, (value) => { watch(routeCustomerCode, (value) => {
if (value) { if (value) {
@@ -115,10 +100,6 @@ watch(routeCustomerId, async (value) => {
} }
}); });
watch(customerCode, async (value) => {
await loadCustomer(value);
});
onMounted(async () => { onMounted(async () => {
if (routeCustomerId.value) { if (routeCustomerId.value) {
await loadCustomerById(routeCustomerId.value); await loadCustomerById(routeCustomerId.value);
@@ -129,6 +110,14 @@ onMounted(async () => {
// ---------- 数据 ---------- // ---------- 数据 ----------
const selectorVisible = ref(false);
const selectedCustomer = ref<Customer>();
const selectedEventIdList = ref<string[]>([]);
watch(customerCode, async (value) => {
await loadCustomer(value);
});
async function loadCustomer(code: string): Promise<void> { async function loadCustomer(code: string): Promise<void> {
const normalizedCode = code.trim(); const normalizedCode = code.trim();
if (!normalizedCode) { if (!normalizedCode) {
@@ -166,15 +155,6 @@ async function handleCustomerSelected(id: string): Promise<void> {
await loadCustomerById(id); await loadCustomerById(id);
} }
async function clearCustomerSelection(): Promise<void> {
customerCode.value = "";
clearLoadedCustomer();
if (route.name !== "CustomerDataCharts" || Object.keys(route.query).length) {
await router.replace({ name: "CustomerDataCharts" });
}
}
function clearLoadedCustomer(): void { function clearLoadedCustomer(): void {
selectedCustomer.value = undefined; selectedCustomer.value = undefined;
selectedCustomerId.value = ""; selectedCustomerId.value = "";
@@ -182,11 +162,15 @@ function clearLoadedCustomer(): void {
// ---------- 扫码 ---------- // ---------- 扫码 ----------
const scannerVisible = ref(false);
const scanStatus = ref("点击扫码开始识别");
let latestDecodedText = "";
async function toggleScanner(): Promise<void> { async function toggleScanner(): Promise<void> {
scannerVisible.value = !scannerVisible.value; scannerVisible.value = !scannerVisible.value;
if (scannerVisible.value) { if (scannerVisible.value) {
latestDecodedText = ""; latestDecodedText = "";
scanStatus.value = "正在启动扫码预览。"; scanStatus.value = "正在启动扫码预览";
} }
} }
@@ -195,11 +179,10 @@ async function handleDecodedText(rawText: string): Promise<void> {
if (!normalizedText || normalizedText === latestDecodedText) { if (!normalizedText || normalizedText === latestDecodedText) {
return; return;
} }
latestDecodedText = normalizedText; latestDecodedText = normalizedText;
await settingStore.playScanSuccessSound(); await settingStore.playScanSuccessSound();
customerCode.value = normalizedText; customerCode.value = normalizedText;
scanStatus.value = `已识别 ${normalizedText},正在加载客户档案。`; scanStatus.value = `已识别 ${normalizedText},正在加载客户档案`;
scannerVisible.value = false; scannerVisible.value = false;
} }
@@ -207,7 +190,13 @@ function handleScanStatusChange(value: string): void {
scanStatus.value = value; scanStatus.value = value;
} }
// ---------- 日期 ---------- // ---------- 图表日期 ----------
const monthCursor = ref(getMonthStart(new Date()));
const chartBeginDate = getDateStart(addMonths(new Date(), -1));
const chartEndDate = getDateEnd(new Date());
const chartDateTextList = buildDateTextList(chartBeginDate, chartEndDate);
const monthDateTextList = computed(() => buildMonthDateTextList(monthCursor.value));
function getMonthStart(date: Date): Date { function getMonthStart(date: Date): Date {
return new Date(date.getFullYear(), date.getMonth(), 1, 0, 0, 0, 0); return new Date(date.getFullYear(), date.getMonth(), 1, 0, 0, 0, 0);
@@ -261,72 +250,33 @@ function getDateEnd(date: Date): Date {
.header { .header {
top: var(--app-nav-height); top: var(--app-nav-height);
z-index: 10; z-index: 10;
padding: 1rem 1rem .875rem 1rem; padding: .5rem 1rem;
position: sticky; position: sticky;
box-shadow: var(--td-shadow-1);
background: #F5F5F8; .buttons {
gap: 1rem;
display: flex;
.selector { .selector {
border: 1px solid rgba(148, 163, 184, .18);
display: flex;
overflow: hidden;
border-radius: .35rem .35rem 1rem 1rem;
flex-direction: column;
&.selected {
border: 1px solid transparent;
background: transparent;
border-radius: 0;
}
.line {
gap: .75rem;
display: flex;
&.buttons {
z-index: 1;
.customer {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
} }
} }
&.scanner-wrapper { .scanner-wrapper {
margin: 1rem .5rem; margin-top: 1rem;
}
}
} }
} }
.summary { .customer {
gap: .75rem;
margin: 0 1rem; margin: 0 1rem;
display: grid; }
grid-template-columns: repeat(3, minmax(0, 1fr));
.summary-item { .charts {
padding: .875rem .625rem; gap: 1rem;
padding: 0 1rem;
display: flex; display: flex;
min-width: 0;
background: rgba(255, 255, 255, .92);
box-shadow: var(--td-shadow-1);
align-items: center;
border-radius: .75rem;
flex-direction: column; flex-direction: column;
span {
color: #52607a;
font-size: .75rem;
}
strong {
margin-top: .375rem;
color: #0f172a;
font-size: 1rem;
font-weight: 700;
}
}
} }
} }
@@ -335,21 +285,6 @@ function getDateEnd(date: Date): Date {
.selector { .selector {
background: #10161f; background: #10161f;
} }
.summary {
.summary-item {
background: rgba(15, 23, 42, .88);
box-shadow: 0 .75rem 2rem rgba(2, 6, 23, .32);
span {
color: #94a3b8;
}
strong {
color: #e2e8f0;
}
}
}
} }
} }
</style> </style>
@@ -1,8 +1,14 @@
<template> <template>
<div v-for="item in summaryList" :key="item.label" class="summary-item"> <section class="summary">
<div
v-for="item in summaryList"
:key="item.label"
class="item"
>
<span v-text="item.label" /> <span v-text="item.label" />
<strong v-text="item.value" /> <strong v-text="item.value" />
</div> </div>
</section>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -44,10 +50,6 @@ const summaryList = computed<SummaryItem[]>(() => [
label: "本月登记", label: "本月登记",
value: `${monthRecordList.value.length} 次` value: `${monthRecordList.value.length} 次`
}, },
{
label: "转介绍",
value: `${props.customer.invitedCount || 0} 人`
},
{ {
label: "最近登记", label: "最近登记",
value: getRecordEventName(monthRecordList.value[0]) || "暂无" value: getRecordEventName(monthRecordList.value[0]) || "暂无"
@@ -98,7 +100,6 @@ function getRecordEventName(record?: StoreEventRecordCalendarView): string {
if (!record) { if (!record) {
return ""; return "";
} }
return record.eventId ? eventNameMap.value.get(record.eventId) || "未知事件" : "未知事件"; return record.eventId ? eventNameMap.value.get(record.eventId) || "未知事件" : "未知事件";
} }
@@ -122,3 +123,51 @@ function addDays(date: Date, days: number): Date {
return value; return value;
} }
</script> </script>
<style scoped lang="less">
.summary {
gap: .75rem;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
.item {
padding: .875rem .625rem;
display: flex;
min-width: 0;
background: rgba(255, 255, 255, .92);
box-shadow: var(--td-shadow-1);
align-items: center;
border-radius: 1rem;
flex-direction: column;
span {
color: #52607a;
font-size: .75rem;
}
strong {
margin-top: .375rem;
color: #0f172a;
font-size: 1.125rem;
font-weight: 700;
}
}
}
:global(html.theme-dark) {
.summary {
.item {
background: rgba(15, 23, 42, .88);
box-shadow: 0 .75rem 2rem rgba(2, 6, 23, .32);
span {
color: #94a3b8;
}
strong {
color: #e2e8f0;
}
}
}
}
</style>
@@ -137,10 +137,6 @@ function addDays(date: Date, days: number): Date {
</script> </script>
<style scoped lang="less"> <style scoped lang="less">
.calendar-section {
margin: 0 1rem;
}
:global(html.theme-dark) { :global(html.theme-dark) {
.calendar-section { .calendar-section {
background: rgba(15, 23, 42, .88); background: rgba(15, 23, 42, .88);
@@ -130,7 +130,6 @@ function getDateEnd(dateText: string): Date {
<style scoped lang="less"> <style scoped lang="less">
.chart-section { .chart-section {
margin: 0 1rem;
padding: 1rem; padding: 1rem;
background: rgba(255, 255, 255, .92); background: rgba(255, 255, 255, .92);
box-shadow: var(--td-shadow-1); box-shadow: var(--td-shadow-1);
@@ -120,7 +120,6 @@ function getDateEnd(dateText: string): Date {
<style scoped lang="less"> <style scoped lang="less">
.chart-section { .chart-section {
margin: 0 1rem;
padding: 1rem; padding: 1rem;
background: rgba(255, 255, 255, .92); background: rgba(255, 255, 255, .92);
box-shadow: var(--td-shadow-1); box-shadow: var(--td-shadow-1);
+2 -2
View File
@@ -72,7 +72,7 @@ function buildDateTextList(begin: Date, end: Date): string[] {
display: grid; display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
.summary-item { .item {
padding: .875rem .625rem; padding: .875rem .625rem;
display: flex; display: flex;
min-width: 0; min-width: 0;
@@ -100,7 +100,7 @@ function buildDateTextList(begin: Date, end: Date): string[] {
:global(html.theme-dark) { :global(html.theme-dark) {
.data-charts-page { .data-charts-page {
.summary { .summary {
.summary-item { .item {
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);
@@ -1,6 +1,6 @@
<template> <template>
<section class="summary"> <section class="summary">
<div v-for="item in summaryList" :key="item.label" class="summary-item"> <div v-for="item in summaryList" :key="item.label" class="item">
<span v-text="item.label" /> <span v-text="item.label" />
<strong v-text="item.value" /> <strong v-text="item.value" />
</div> </div>
@@ -159,7 +159,7 @@ function toDateValue(date: Date): number {
display: grid; display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
.summary-item { .item {
padding: .875rem .625rem; padding: .875rem .625rem;
display: flex; display: flex;
min-width: 0; min-width: 0;
@@ -185,7 +185,7 @@ function toDateValue(date: Date): number {
:global(html.theme-dark) { :global(html.theme-dark) {
.summary { .summary {
.summary-item { .item {
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);