优化首页报表
This commit is contained in:
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 761 KiB After Width: | Height: | Size: 1.1 MiB |
@@ -58,7 +58,8 @@ export const generatedRoutes: GeneratedRoute[] = [
|
||||
title: 'headphone_brand',
|
||||
i18nKey: 'route.headphone_brand',
|
||||
icon: 'mdi:tag-multiple-outline',
|
||||
order: 1
|
||||
order: 1,
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -69,7 +70,8 @@ export const generatedRoutes: GeneratedRoute[] = [
|
||||
title: 'headphone_model',
|
||||
i18nKey: 'route.headphone_model',
|
||||
icon: 'mdi:earbuds',
|
||||
order: 2
|
||||
order: 2,
|
||||
keepAlive: true
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -82,7 +84,8 @@ export const generatedRoutes: GeneratedRoute[] = [
|
||||
title: 'home',
|
||||
i18nKey: 'route.home',
|
||||
icon: 'mdi:monitor-dashboard',
|
||||
order: 1
|
||||
order: 1,
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -129,7 +132,8 @@ export const generatedRoutes: GeneratedRoute[] = [
|
||||
title: 'share-code_log',
|
||||
i18nKey: 'route.share-code_log',
|
||||
icon: 'mdi:file-document-outline',
|
||||
order: 1
|
||||
order: 1,
|
||||
keepAlive: true
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -155,7 +159,8 @@ export const generatedRoutes: GeneratedRoute[] = [
|
||||
i18nKey: 'route.system_users',
|
||||
icon: 'mdi:account-cog-outline',
|
||||
order: 1,
|
||||
roles: ['R_SUPER']
|
||||
roles: ['R_SUPER'],
|
||||
keepAlive: true
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -179,7 +184,8 @@ export const generatedRoutes: GeneratedRoute[] = [
|
||||
title: 'toolbox_luxsin-controller',
|
||||
i18nKey: 'route.toolbox_luxsin-controller',
|
||||
icon: 'mdi:tune-vertical',
|
||||
order: 1
|
||||
order: 1,
|
||||
keepAlive: true
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -203,7 +209,8 @@ export const generatedRoutes: GeneratedRoute[] = [
|
||||
title: 'upgrade_blacklist',
|
||||
i18nKey: 'route.upgrade_blacklist',
|
||||
icon: 'mdi:account-cancel-outline',
|
||||
order: 3
|
||||
order: 3,
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -214,7 +221,8 @@ export const generatedRoutes: GeneratedRoute[] = [
|
||||
title: 'upgrade_ota',
|
||||
i18nKey: 'route.upgrade_ota',
|
||||
icon: 'mdi:cellphone-arrow-down',
|
||||
order: 1
|
||||
order: 1,
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -225,7 +233,8 @@ export const generatedRoutes: GeneratedRoute[] = [
|
||||
title: 'upgrade_ota-target-device',
|
||||
i18nKey: 'route.upgrade_ota-target-device',
|
||||
icon: 'mdi:crosshairs-gps',
|
||||
order: 2
|
||||
order: 2,
|
||||
keepAlive: true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -103,6 +103,24 @@ export function fetchPushToMeilisearch(modelIds: number[]) {
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchBatchUpdateModels(data: {
|
||||
ids: number[];
|
||||
brand_name?: string;
|
||||
form?: string;
|
||||
source?: string;
|
||||
rig?: string;
|
||||
}) {
|
||||
return request<{
|
||||
updated_count: number;
|
||||
updated_ids: number[];
|
||||
errors: Array<{ id: number; brand_name: string; name: string; error: string }>;
|
||||
}>({
|
||||
url: '/models/batch-update',
|
||||
method: 'patch',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchFromSquigLink(shareUrl: string, selectedFile?: string) {
|
||||
return request({
|
||||
url: '/models/squiglink-fetch',
|
||||
|
||||
Vendored
+2
@@ -67,6 +67,7 @@ declare module 'vue' {
|
||||
NModal: typeof import('naive-ui')['NModal']
|
||||
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
|
||||
NPagination: typeof import('naive-ui')['NPagination']
|
||||
NProgress: typeof import('naive-ui')['NProgress']
|
||||
NRadioButton: typeof import('naive-ui')['NRadioButton']
|
||||
NRadioGroup: typeof import('naive-ui')['NRadioGroup']
|
||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||
@@ -154,6 +155,7 @@ declare global {
|
||||
const NModal: typeof import('naive-ui')['NModal']
|
||||
const NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
|
||||
const NPagination: typeof import('naive-ui')['NPagination']
|
||||
const NProgress: typeof import('naive-ui')['NProgress']
|
||||
const NRadioButton: typeof import('naive-ui')['NRadioButton']
|
||||
const NRadioGroup: typeof import('naive-ui')['NRadioGroup']
|
||||
const NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import type { FormInst } from 'naive-ui';
|
||||
import { fetchBatchUpdateModels } from '@/service/api';
|
||||
import type { BrandOption } from '../composables/useModelList';
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean;
|
||||
brandOptions?: BrandOption[];
|
||||
selectedIds: number[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:show': [value: boolean];
|
||||
success: [];
|
||||
'reload-brands': [];
|
||||
}>();
|
||||
|
||||
const formRef = ref<FormInst | null>(null);
|
||||
const submitLoading = ref(false);
|
||||
|
||||
const progress = reactive({
|
||||
show: false,
|
||||
current: 0,
|
||||
total: 0
|
||||
});
|
||||
|
||||
const formData = reactive({
|
||||
brand_name: '',
|
||||
form: null as string | null,
|
||||
source: '',
|
||||
rig: ''
|
||||
});
|
||||
|
||||
const brandSelectOptions = computed(() => {
|
||||
const list = (props.brandOptions || []).map(b => ({ label: b.name, value: b.name }));
|
||||
const q = (formData.brand_name || '').trim().toLowerCase();
|
||||
if (!q) return list;
|
||||
return list.filter(item => item.label.toLowerCase().includes(q));
|
||||
});
|
||||
|
||||
const hasBrand = computed(() => formData.brand_name.trim().length > 0);
|
||||
const hasForm = computed(() => !!formData.form);
|
||||
const hasSource = computed(() => formData.source.trim().length > 0);
|
||||
const hasRig = computed(() => formData.rig.trim().length > 0);
|
||||
|
||||
const canSubmit = computed(() => {
|
||||
if (!props.selectedIds.length) return false;
|
||||
return hasBrand.value || hasForm.value || hasSource.value || hasRig.value;
|
||||
});
|
||||
|
||||
const progressPercentage = computed(() => {
|
||||
if (!progress.total) return 0;
|
||||
return Math.round((progress.current / progress.total) * 100);
|
||||
});
|
||||
|
||||
function resetForm() {
|
||||
formData.brand_name = '';
|
||||
formData.form = null;
|
||||
formData.source = '';
|
||||
formData.rig = '';
|
||||
progress.show = false;
|
||||
progress.current = 0;
|
||||
progress.total = 0;
|
||||
formRef.value?.restoreValidation();
|
||||
}
|
||||
|
||||
function handleDialogClose() {
|
||||
resetForm();
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
submitLoading.value = true;
|
||||
progress.show = true;
|
||||
progress.total = props.selectedIds.length;
|
||||
progress.current = 0;
|
||||
|
||||
const payloadBase: Record<string, unknown> = {};
|
||||
if (hasBrand.value) payloadBase.brand_name = formData.brand_name.trim();
|
||||
if (hasForm.value) payloadBase.form = formData.form;
|
||||
if (hasSource.value) payloadBase.source = formData.source.trim();
|
||||
if (hasRig.value) payloadBase.rig = formData.rig.trim();
|
||||
|
||||
let successCount = 0;
|
||||
let failCount = 0;
|
||||
|
||||
for (const id of props.selectedIds) {
|
||||
progress.current++;
|
||||
const { error } = await fetchBatchUpdateModels({
|
||||
ids: [id],
|
||||
...payloadBase
|
||||
} as {
|
||||
ids: number[];
|
||||
brand_name?: string;
|
||||
form?: string;
|
||||
source?: string;
|
||||
rig?: string;
|
||||
});
|
||||
if (error) {
|
||||
failCount++;
|
||||
} else {
|
||||
successCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (failCount > 0) {
|
||||
window.$message?.warning(`成功修改 ${successCount} 个,失败 ${failCount} 个`);
|
||||
} else {
|
||||
window.$message?.success(`成功修改 ${successCount} 个型号`);
|
||||
}
|
||||
|
||||
if (hasBrand.value) emit('reload-brands');
|
||||
emit('update:show', false);
|
||||
emit('success');
|
||||
submitLoading.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NModal
|
||||
:show="show"
|
||||
preset="card"
|
||||
title="批量修改"
|
||||
class="w-520px"
|
||||
:mask-closable="false"
|
||||
@update:show="emit('update:show', $event)"
|
||||
@after-leave="handleDialogClose"
|
||||
>
|
||||
<div class="mb-12px text-13px opacity-70">
|
||||
已选择 <b>{{ selectedIds.length }}</b> 个型号,填写需要修改的字段,留空则不修改:
|
||||
</div>
|
||||
|
||||
<NForm ref="formRef" :model="formData" label-placement="left" label-width="100">
|
||||
<NFormItem label="品牌名称" path="brand_name">
|
||||
<NAutoComplete
|
||||
v-model:value="formData.brand_name"
|
||||
:options="brandSelectOptions"
|
||||
clearable
|
||||
placeholder="留空则不修改"
|
||||
/>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem label="佩戴方式" path="form">
|
||||
<NSelect
|
||||
v-model:value="formData.form"
|
||||
clearable
|
||||
placeholder="留空则不修改"
|
||||
:options="[
|
||||
{ label: '入耳式', value: 'in-ear' },
|
||||
{ label: '头戴式', value: 'over-ear' },
|
||||
{ label: '耳塞式', value: 'earbud' }
|
||||
]"
|
||||
/>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem label="来源" path="source">
|
||||
<NInput
|
||||
v-model:value="formData.source"
|
||||
maxlength="100"
|
||||
placeholder="留空则不修改"
|
||||
/>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem label="阻抗" path="rig">
|
||||
<NInput
|
||||
v-model:value="formData.rig"
|
||||
maxlength="100"
|
||||
placeholder="留空则不修改"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
|
||||
<div v-if="progress.show" class="batch-progress">
|
||||
<div class="batch-progress-text">
|
||||
正在修改 {{ progress.current }}/{{ progress.total }}
|
||||
</div>
|
||||
<NProgress
|
||||
type="line"
|
||||
:percentage="progressPercentage"
|
||||
:show-indicator="false"
|
||||
:height="8"
|
||||
border-radius="4"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<NSpace justify="end">
|
||||
<NButton @click="emit('update:show', false)" :disabled="submitLoading">取消</NButton>
|
||||
<NButton type="primary" :loading="submitLoading" :disabled="!canSubmit" @click="handleSubmit">
|
||||
确定修改
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
</NModal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.batch-progress {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.batch-progress-text {
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from '@/service/api';
|
||||
import { useModelList, type ModelRow } from './composables/useModelList';
|
||||
import ModelFormDialog from './components/ModelFormDialog.vue';
|
||||
import BatchEditDialog from './components/BatchEditDialog.vue';
|
||||
import PushViewDialog from './components/PushViewDialog.vue';
|
||||
import EqCacheDialog from './components/EqCacheDialog.vue';
|
||||
import CsvViewerDialog from './components/CsvViewerDialog.vue';
|
||||
@@ -55,6 +56,7 @@ const brandAutoOptions = computed(() => {
|
||||
});
|
||||
|
||||
const formDialogVisible = ref(false);
|
||||
const batchEditDialogVisible = ref(false);
|
||||
const moreActionLoadingId = ref<number | null>(null);
|
||||
|
||||
const pushViewDialogVisible = ref(false);
|
||||
@@ -206,6 +208,14 @@ const columns = computed<DataTableColumns<ModelRow>>(() => [
|
||||
}
|
||||
]);
|
||||
|
||||
function handleBatchEdit() {
|
||||
if (selectedIds.value.length === 0) {
|
||||
window.$message?.warning('请选择要修改的型号');
|
||||
return;
|
||||
}
|
||||
batchEditDialogVisible.value = true;
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
formDialogRef.value?.openForAdd();
|
||||
formDialogVisible.value = true;
|
||||
@@ -373,6 +383,12 @@ onMounted(() => {
|
||||
</template>
|
||||
推送搜索 「{{ selectedIds.length }}」
|
||||
</NButton>
|
||||
<NButton type="info" :disabled="selectedIds.length === 0" @click="handleBatchEdit">
|
||||
<template #icon>
|
||||
<SvgIcon icon="mdi:square-edit-outline" />
|
||||
</template>
|
||||
批量修改 「{{ selectedIds.length }}」
|
||||
</NButton>
|
||||
<NButton type="primary" @click="handleAdd">
|
||||
<template #icon>
|
||||
<SvgIcon icon="mdi:plus" />
|
||||
@@ -424,6 +440,14 @@ onMounted(() => {
|
||||
@reload-brands="loadBrands"
|
||||
/>
|
||||
|
||||
<BatchEditDialog
|
||||
v-model:show="batchEditDialogVisible"
|
||||
:brand-options="brandOptions"
|
||||
:selected-ids="selectedIds"
|
||||
@success="loadData"
|
||||
@reload-brands="loadBrands"
|
||||
/>
|
||||
|
||||
<PushViewDialog
|
||||
v-model:show="pushViewDialogVisible"
|
||||
:title="pushViewDialogTitle"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { computed, onMounted, ref, watch, nextTick } from 'vue';
|
||||
import { fetchGetDashboardToday, fetchGetDeviceDaily, fetchGetActiveDaily } from '@/service/api';
|
||||
import { useAuthStore } from '@/store/modules/auth';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { useEcharts } from '@/hooks/common/echarts';
|
||||
import type { ECOption } from '@/hooks/common/echarts';
|
||||
|
||||
@@ -22,6 +23,8 @@ interface TodayOta {
|
||||
}
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const themeStore = useThemeStore();
|
||||
const isDark = computed(() => themeStore.darkMode);
|
||||
const loading = ref(false);
|
||||
const todayModels = ref<TodayModel[]>([]);
|
||||
const todayOtas = ref<TodayOta[]>([]);
|
||||
@@ -47,6 +50,27 @@ async function loadTodayStats() {
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
// ===== 共享配色 =====
|
||||
const CHART_COLORS = ['#5B8FF9', '#5AD8A6', '#F6BD16', '#E8684A', '#6DC8EC', '#9270CA'];
|
||||
|
||||
// ===== 主题自适应轴线颜色 =====
|
||||
function getAxisColors() {
|
||||
if (isDark.value) {
|
||||
return {
|
||||
splitLine: 'rgba(255,255,255,0.06)',
|
||||
axisLine: 'rgba(255,255,255,0.1)',
|
||||
label: 'rgba(255,255,255,0.45)',
|
||||
pointer: 'rgba(255,255,255,0.15)'
|
||||
};
|
||||
}
|
||||
return {
|
||||
splitLine: '#e8eaed',
|
||||
axisLine: '#dcdfe6',
|
||||
label: '#8492a6',
|
||||
pointer: '#b2bdc6'
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 共享:tooltip 悬浮时追加总计行(可自定义标签)=====
|
||||
function createTooltipFormatter(totalLabel: string) {
|
||||
return (params: any): string => {
|
||||
@@ -71,39 +95,57 @@ const deviceLoading = ref(false);
|
||||
const deviceTotal = ref(0);
|
||||
|
||||
function buildDeviceChartOptions(dates: string[], series: Array<{ name: string; data: number[] }>): ECOption {
|
||||
const labelInterval = deviceDays.value <= 7 ? 0 : deviceDays.value <= 30 ? 2 : 6;
|
||||
const isHourly = deviceDays.value === 1;
|
||||
const labelInterval = isHourly ? 1 : deviceDays.value <= 7 ? 0 : deviceDays.value <= 30 ? 2 : 6;
|
||||
const ac = getAxisColors();
|
||||
return {
|
||||
color: CHART_COLORS,
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'shadow' },
|
||||
formatter: createTooltipFormatter('总新增')
|
||||
},
|
||||
legend: {
|
||||
top: 'bottom'
|
||||
top: 'top',
|
||||
itemGap: 20,
|
||||
icon: 'roundRect',
|
||||
itemWidth: 12,
|
||||
itemHeight: 8
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '12%',
|
||||
bottom: '8%',
|
||||
top: '15%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: dates,
|
||||
axisTick: { show: false },
|
||||
axisLine: { lineStyle: { color: ac.axisLine } },
|
||||
axisLabel: {
|
||||
formatter: (val: string) => val.slice(5),
|
||||
formatter: (val: string) => (isHourly ? val : val.slice(5)),
|
||||
interval: labelInterval,
|
||||
rotate: 45
|
||||
rotate: isHourly ? 0 : 45,
|
||||
color: ac.label
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
minInterval: 1
|
||||
minInterval: 1,
|
||||
axisTick: { show: false },
|
||||
axisLine: { show: false },
|
||||
splitLine: { lineStyle: { type: 'dashed', color: ac.splitLine } },
|
||||
axisLabel: { color: ac.label }
|
||||
},
|
||||
series: series.map(s => ({
|
||||
name: s.name,
|
||||
type: 'bar' as const,
|
||||
stack: 'total',
|
||||
barMaxWidth: 32,
|
||||
itemStyle: { borderRadius: [0, 0, 0, 0] },
|
||||
emphasis: { focus: 'series' },
|
||||
data: s.data
|
||||
}))
|
||||
};
|
||||
@@ -111,11 +153,14 @@ function buildDeviceChartOptions(dates: string[], series: Array<{ name: string;
|
||||
|
||||
const { domRef: deviceChartRef, updateOptions: updateDeviceChart } = useEcharts(() => buildDeviceChartOptions([], []));
|
||||
|
||||
const lastDeviceData = ref<{ dates: string[]; series: Array<{ name: string; data: number[] }> }>({ dates: [], series: [] });
|
||||
|
||||
async function loadDeviceDaily() {
|
||||
deviceLoading.value = true;
|
||||
const { data, error } = await fetchGetDeviceDaily(deviceDays.value);
|
||||
if (!error && data) {
|
||||
deviceTotal.value = data.total;
|
||||
lastDeviceData.value = { dates: data.dates, series: data.series };
|
||||
await updateDeviceChart(() => buildDeviceChartOptions(data.dates, data.series));
|
||||
}
|
||||
deviceLoading.value = false;
|
||||
@@ -130,41 +175,75 @@ const activeDays = ref(30);
|
||||
const activeLoading = ref(false);
|
||||
const activeLatest = ref({ date: '', total: 0 });
|
||||
|
||||
const activeLatestLabel = computed(() => {
|
||||
if (activeDays.value === 1) return `${activeLatest.value.date} 总活跃`;
|
||||
return `${activeLatest.value.date.slice(5)} 总日活`;
|
||||
});
|
||||
|
||||
function buildActiveChartOptions(dates: string[], series: Array<{ name: string; data: number[] }>): ECOption {
|
||||
const labelInterval = activeDays.value <= 7 ? 0 : activeDays.value <= 30 ? 2 : 6;
|
||||
const isHourly = activeDays.value === 1;
|
||||
const labelInterval = isHourly ? 1 : activeDays.value <= 7 ? 0 : activeDays.value <= 30 ? 2 : 6;
|
||||
const ac = getAxisColors();
|
||||
return {
|
||||
color: CHART_COLORS,
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'line',
|
||||
lineStyle: { type: 'dashed', color: ac.pointer }
|
||||
},
|
||||
formatter: createTooltipFormatter('总日活')
|
||||
},
|
||||
legend: {
|
||||
top: 'bottom'
|
||||
top: 'top',
|
||||
itemGap: 20,
|
||||
icon: 'roundRect',
|
||||
itemWidth: 12,
|
||||
itemHeight: 8
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '12%',
|
||||
bottom: '8%',
|
||||
top: '15%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: dates,
|
||||
axisTick: { show: false },
|
||||
axisLine: { lineStyle: { color: ac.axisLine } },
|
||||
axisLabel: {
|
||||
formatter: (val: string) => val.slice(5),
|
||||
formatter: (val: string) => (isHourly ? val : val.slice(5)),
|
||||
interval: labelInterval,
|
||||
rotate: 45
|
||||
rotate: isHourly ? 0 : 45,
|
||||
color: ac.label
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
minInterval: 1
|
||||
minInterval: 1,
|
||||
axisTick: { show: false },
|
||||
axisLine: { show: false },
|
||||
splitLine: { lineStyle: { type: 'dashed', color: ac.splitLine } },
|
||||
axisLabel: { color: ac.label }
|
||||
},
|
||||
series: series.map(s => ({
|
||||
name: s.name,
|
||||
type: 'line' as const,
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 6,
|
||||
showSymbol: false,
|
||||
lineStyle: { width: 2.5 },
|
||||
areaStyle: { opacity: 0.08 },
|
||||
emphasis: {
|
||||
focus: 'series',
|
||||
showSymbol: true,
|
||||
symbolSize: 8,
|
||||
lineStyle: { width: 3 }
|
||||
},
|
||||
data: s.data
|
||||
}))
|
||||
};
|
||||
@@ -172,11 +251,14 @@ function buildActiveChartOptions(dates: string[], series: Array<{ name: string;
|
||||
|
||||
const { domRef: activeChartRef, updateOptions: updateActiveChart } = useEcharts(() => buildActiveChartOptions([], []));
|
||||
|
||||
const lastActiveData = ref<{ dates: string[]; series: Array<{ name: string; data: number[] }> }>({ dates: [], series: [] });
|
||||
|
||||
async function loadActiveDaily() {
|
||||
activeLoading.value = true;
|
||||
const { data, error } = await fetchGetActiveDaily(activeDays.value);
|
||||
if (!error && data) {
|
||||
activeLatest.value = data.latest;
|
||||
lastActiveData.value = { dates: data.dates, series: data.series };
|
||||
await updateActiveChart(() => buildActiveChartOptions(data.dates, data.series));
|
||||
}
|
||||
activeLoading.value = false;
|
||||
@@ -186,6 +268,18 @@ watch(activeDays, () => {
|
||||
loadActiveDaily();
|
||||
});
|
||||
|
||||
// 主题切换时用已有数据重建图表(颜色自适应)
|
||||
watch(isDark, () => {
|
||||
nextTick(() => {
|
||||
if (lastDeviceData.value.dates.length) {
|
||||
updateDeviceChart(() => buildDeviceChartOptions(lastDeviceData.value.dates, lastDeviceData.value.series));
|
||||
}
|
||||
if (lastActiveData.value.dates.length) {
|
||||
updateActiveChart(() => buildActiveChartOptions(lastActiveData.value.dates, lastActiveData.value.series));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
loadTodayStats();
|
||||
loadDeviceDaily();
|
||||
@@ -243,11 +337,12 @@ onMounted(() => {
|
||||
<NGrid cols="1 s:2" responsive="screen" :x-gap="16" :y-gap="16">
|
||||
<NGi>
|
||||
<NCard :bordered="false" class="card-wrapper">
|
||||
<template #header>设备每日新增趋势</template>
|
||||
<template #header>新增趋势</template>
|
||||
<template #header-extra>
|
||||
<NSpace align="center" :size="12">
|
||||
<NTag type="info" size="small">合计 {{ deviceTotal }}</NTag>
|
||||
<NRadioGroup v-model:value="deviceDays" size="small">
|
||||
<NRadioButton :value="1">今天</NRadioButton>
|
||||
<NRadioButton :value="7">近7天</NRadioButton>
|
||||
<NRadioButton :value="30">近1月</NRadioButton>
|
||||
<NRadioButton :value="90">近3月</NRadioButton>
|
||||
@@ -261,11 +356,12 @@ onMounted(() => {
|
||||
</NGi>
|
||||
<NGi>
|
||||
<NCard :bordered="false" class="card-wrapper">
|
||||
<template #header>设备日活趋势</template>
|
||||
<template #header>活跃趋势</template>
|
||||
<template #header-extra>
|
||||
<NSpace align="center" :size="12">
|
||||
<NTag type="success" size="small">{{ activeLatest.date.slice(5) }} 总日活 {{ activeLatest.total }}</NTag>
|
||||
<NTag type="success" size="small">{{ activeLatestLabel }} {{ activeLatest.total }}</NTag>
|
||||
<NRadioGroup v-model:value="activeDays" size="small">
|
||||
<NRadioButton :value="1">今天</NRadioButton>
|
||||
<NRadioButton :value="7">近7天</NRadioButton>
|
||||
<NRadioButton :value="30">近1月</NRadioButton>
|
||||
<NRadioButton :value="90">近3月</NRadioButton>
|
||||
|
||||
Reference in New Issue
Block a user