refactor: 重构项目结构
- 优化 `车站-设备-告警` 轮询机制 - 改进设备卡片的布局 - 支持修改设备 - 告警轮询中获取完整告警数据 - 车站告警详情支持导出完整的 `今日告警列表` - 支持将状态持久化到 `IndexedDB` - 新增轮询控制 (调试模式) - 新增离线开发模式 (调试模式) - 新增 `IndexedDB` 数据控制 (调试模式)
This commit is contained in:
12
src/components/global/global-feedback/global-feedback.vue
Normal file
12
src/components/global/global-feedback/global-feedback.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { useDialog, useLoadingBar, useMessage, useNotification } from 'naive-ui';
|
||||
|
||||
window.$dialog = useDialog();
|
||||
window.$loadingBar = useLoadingBar();
|
||||
window.$message = useMessage();
|
||||
window.$notification = useNotification();
|
||||
</script>
|
||||
|
||||
<template></template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
3
src/components/global/global-feedback/index.ts
Normal file
3
src/components/global/global-feedback/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import GlobalFeedback from './global-feedback.vue';
|
||||
|
||||
export { GlobalFeedback };
|
||||
3
src/components/global/index.ts
Normal file
3
src/components/global/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './global-feedback';
|
||||
export * from './settings-drawer';
|
||||
export * from './theme-switch';
|
||||
3
src/components/global/settings-drawer/index.ts
Normal file
3
src/components/global/settings-drawer/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import SettingsDrawer from './settings-drawer.vue';
|
||||
|
||||
export { SettingsDrawer };
|
||||
277
src/components/global/settings-drawer/settings-drawer.vue
Normal file
277
src/components/global/settings-drawer/settings-drawer.vue
Normal file
@@ -0,0 +1,277 @@
|
||||
<script setup lang="ts">
|
||||
import type { LineAlarms, LineDevices, NdmDeviceResultVO, Station, VersionInfo } from '@/apis';
|
||||
import { ThemeSwitch } from '@/components';
|
||||
import { NDM_ALARM_STORE_ID, NDM_DEVICE_STORE_ID, NDM_STATION_STORE_ID } from '@/constants';
|
||||
import { usePollingStore, useSettingStore } from '@/stores';
|
||||
import { downloadByData, getAppEnvConfig, parseErrorFeedback, sleep } from '@/utils';
|
||||
import { useMutation } from '@tanstack/vue-query';
|
||||
import { DeleteOutlined, ExportOutlined, ImportOutlined } from '@vicons/antd';
|
||||
import { useEventListener } from '@vueuse/core';
|
||||
import axios from 'axios';
|
||||
import destr from 'destr';
|
||||
import { isFunction } from 'es-toolkit';
|
||||
import localforage from 'localforage';
|
||||
import { NButton, NDivider, NDrawer, NDrawerContent, NDropdown, NFlex, NFormItem, NIcon, NInput, NInputNumber, NModal, NSwitch, NText, type DropdownOption } from 'naive-ui';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const show = defineModel<boolean>('show', { default: false });
|
||||
|
||||
const settingsStore = useSettingStore();
|
||||
const { menuCollpased, stationGridCols, debugModeEnabled, offlineDev } = storeToRefs(settingsStore);
|
||||
|
||||
const versionInfo = ref<VersionInfo>({ version: '', buildTime: '' });
|
||||
|
||||
const { mutate: getVersionInfo } = useMutation({
|
||||
mutationFn: async () => {
|
||||
const { data } = await axios.get<VersionInfo>(`/manifest.json?t=${Date.now()}`);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
versionInfo.value = data;
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
const errorFeedback = parseErrorFeedback(error);
|
||||
window.$message.error(errorFeedback);
|
||||
},
|
||||
});
|
||||
|
||||
const showDebugCodeModal = ref(false);
|
||||
const debugCode = ref('');
|
||||
const enableDebugMode = () => {
|
||||
const { debugCode: expectedDebugCode } = getAppEnvConfig();
|
||||
if (debugCode.value !== expectedDebugCode) {
|
||||
window.$message.error('调试授权码错误');
|
||||
return;
|
||||
}
|
||||
showDebugCodeModal.value = false;
|
||||
settingsStore.enableDebugMode();
|
||||
};
|
||||
const disableDebugMode = () => {
|
||||
showDebugCodeModal.value = false;
|
||||
settingsStore.disableDebugMode();
|
||||
};
|
||||
useEventListener('keydown', (event) => {
|
||||
const { ctrlKey, altKey, code } = event;
|
||||
if (ctrlKey && altKey && code === 'KeyD') {
|
||||
showDebugCodeModal.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
const expectToShowDebugCodeInput = ref(false);
|
||||
const onModalAfterEnter = () => {
|
||||
expectToShowDebugCodeInput.value = !debugModeEnabled.value;
|
||||
};
|
||||
const onModalAfterLeave = () => {
|
||||
expectToShowDebugCodeInput.value = false;
|
||||
debugCode.value = '';
|
||||
};
|
||||
|
||||
const pollingStore = usePollingStore();
|
||||
const { pollingEnabled } = storeToRefs(pollingStore);
|
||||
const onPollingEnabledUpdate = (enabled: boolean) => {
|
||||
if (enabled) {
|
||||
pollingStore.startPolling();
|
||||
} else {
|
||||
pollingStore.stopPolling();
|
||||
}
|
||||
};
|
||||
|
||||
type IndexedDbStoreId = typeof NDM_STATION_STORE_ID | typeof NDM_DEVICE_STORE_ID | typeof NDM_ALARM_STORE_ID;
|
||||
type IndexedDbStoreStates = {
|
||||
[NDM_STATION_STORE_ID]: { stations: Station[] };
|
||||
[NDM_DEVICE_STORE_ID]: { lineDevices: LineDevices };
|
||||
[NDM_ALARM_STORE_ID]: { lineAlarms: LineAlarms; unreadLineAlarms: LineAlarms };
|
||||
};
|
||||
const exportFromIndexedDB = async <K extends IndexedDbStoreId>(storeId: K, options?: { errorMsg?: string }) => {
|
||||
const { errorMsg } = options ?? {};
|
||||
const data = await localforage.getItem<IndexedDbStoreStates[K]>(storeId);
|
||||
if (!data) {
|
||||
window.$message.error(errorMsg ?? '导出数据失败');
|
||||
return;
|
||||
}
|
||||
downloadByData(JSON.stringify(data, null, 2), `${storeId}.json`);
|
||||
};
|
||||
const importToIndexedDB = async <K extends IndexedDbStoreId>(storeId: K, options?: { successMsg?: string; errorMsg?: string }) => {
|
||||
const { successMsg, errorMsg } = options ?? {};
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = '.json';
|
||||
fileInput.click();
|
||||
fileInput.onchange = async () => {
|
||||
const file = fileInput.files?.[0];
|
||||
if (!file) {
|
||||
window.$message.error(errorMsg ?? '导入数据失败');
|
||||
return;
|
||||
}
|
||||
const reader = new FileReader();
|
||||
reader.readAsText(file, 'utf-8');
|
||||
reader.onload = async () => {
|
||||
const data = destr<IndexedDbStoreStates[K]>(reader.result as string);
|
||||
await localforage.setItem(storeId, data);
|
||||
window.$message.success(successMsg ?? '导入数据成功');
|
||||
await sleep(2000);
|
||||
window.location.reload();
|
||||
};
|
||||
};
|
||||
};
|
||||
const deleteFromIndexedDB = async (storeId: IndexedDbStoreId) => {
|
||||
await localforage.removeItem(storeId).catch((error) => {
|
||||
window.$message.error(`${error}`);
|
||||
return;
|
||||
});
|
||||
window.$message.success('删除成功');
|
||||
await sleep(2000);
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
const exportDropdownOptions: DropdownOption[] = [
|
||||
{
|
||||
label: '导出车站',
|
||||
key: 'exportStations',
|
||||
onSelect: () => exportFromIndexedDB(NDM_STATION_STORE_ID),
|
||||
},
|
||||
{
|
||||
label: '导出设备',
|
||||
key: 'exportDevices',
|
||||
onSelect: () => exportFromIndexedDB(NDM_DEVICE_STORE_ID),
|
||||
},
|
||||
{
|
||||
label: '导出告警',
|
||||
key: 'exportAlarms',
|
||||
onSelect: () => exportFromIndexedDB(NDM_ALARM_STORE_ID),
|
||||
},
|
||||
];
|
||||
const importDropdownOptions: DropdownOption[] = [
|
||||
{
|
||||
label: '导入车站',
|
||||
key: 'importStations',
|
||||
onSelect: () => importToIndexedDB(NDM_STATION_STORE_ID),
|
||||
},
|
||||
{
|
||||
label: '导入设备',
|
||||
key: 'importDevices',
|
||||
onSelect: () => importToIndexedDB(NDM_DEVICE_STORE_ID),
|
||||
},
|
||||
{
|
||||
label: '导入告警',
|
||||
key: 'importAlarms',
|
||||
onSelect: () => importToIndexedDB(NDM_ALARM_STORE_ID),
|
||||
},
|
||||
];
|
||||
const deleteDropdownOptions: DropdownOption[] = [
|
||||
{
|
||||
label: '删除车站',
|
||||
key: 'deleteStations',
|
||||
onSelect: () => deleteFromIndexedDB(NDM_STATION_STORE_ID),
|
||||
},
|
||||
{
|
||||
label: '删除设备',
|
||||
key: 'deleteDevices',
|
||||
onSelect: () => deleteFromIndexedDB(NDM_DEVICE_STORE_ID),
|
||||
},
|
||||
{
|
||||
label: '删除告警',
|
||||
key: 'deleteAlarms',
|
||||
onSelect: () => deleteFromIndexedDB(NDM_ALARM_STORE_ID),
|
||||
},
|
||||
];
|
||||
|
||||
const onSelectDropdownOption = (key: string, option: DropdownOption) => {
|
||||
const onSelect = option['onSelect'];
|
||||
if (isFunction(onSelect)) {
|
||||
onSelect();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getVersionInfo();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDrawer v-model:show="show" :width="560" :auto-focus="false">
|
||||
<NDrawerContent closable title="系统设置" :native-scrollbar="false">
|
||||
<NFlex vertical>
|
||||
<NDivider>主题</NDivider>
|
||||
<NFormItem label="深色模式" label-placement="left">
|
||||
<ThemeSwitch size="small" />
|
||||
</NFormItem>
|
||||
|
||||
<NDivider>布局</NDivider>
|
||||
<NFormItem label="折叠菜单" label-placement="left">
|
||||
<NSwitch size="small" v-model:value="menuCollpased" />
|
||||
</NFormItem>
|
||||
<template v-if="route.path === '/station'">
|
||||
<NFormItem label="车站列数" label-placement="left">
|
||||
<NInputNumber v-model:value="stationGridCols" :min="1" :max="10" />
|
||||
</NFormItem>
|
||||
</template>
|
||||
|
||||
<template v-if="debugModeEnabled">
|
||||
<NDivider title-placement="center">调试</NDivider>
|
||||
<NFormItem label="启用轮询" label-placement="left">
|
||||
<NSwitch size="small" :value="pollingEnabled" @update:value="onPollingEnabledUpdate" />
|
||||
</NFormItem>
|
||||
<NFormItem label="离线开发" label-placement="left">
|
||||
<NSwitch size="small" v-model:value="offlineDev" />
|
||||
</NFormItem>
|
||||
<NFormItem label="本地数据库" label-placement="left">
|
||||
<NFlex>
|
||||
<NDropdown trigger="click" :options="exportDropdownOptions" @select="onSelectDropdownOption">
|
||||
<NButton secondary size="small">
|
||||
<template #icon>
|
||||
<NIcon :component="ExportOutlined" />
|
||||
</template>
|
||||
<template #default>导出</template>
|
||||
</NButton>
|
||||
</NDropdown>
|
||||
<NDropdown trigger="click" :options="importDropdownOptions" @select="onSelectDropdownOption">
|
||||
<NButton secondary size="small">
|
||||
<template #icon>
|
||||
<NIcon :component="ImportOutlined" />
|
||||
</template>
|
||||
<template #default>导入</template>
|
||||
</NButton>
|
||||
</NDropdown>
|
||||
<NDropdown trigger="click" :options="deleteDropdownOptions" @select="onSelectDropdownOption">
|
||||
<NButton secondary size="small">
|
||||
<template #icon>
|
||||
<NIcon :component="DeleteOutlined" />
|
||||
</template>
|
||||
<template #default>删除</template>
|
||||
</NButton>
|
||||
</NDropdown>
|
||||
</NFlex>
|
||||
</NFormItem>
|
||||
</template>
|
||||
</NFlex>
|
||||
<template #footer>
|
||||
<NFlex vertical justify="flex-end" align="center" style="width: 100%; font-size: 12px; gap: 4px">
|
||||
<NText :depth="3">平台版本: {{ versionInfo.version }} ({{ versionInfo.buildTime }})</NText>
|
||||
</NFlex>
|
||||
</template>
|
||||
</NDrawerContent>
|
||||
</NDrawer>
|
||||
|
||||
<NModal v-model:show="showDebugCodeModal" preset="dialog" type="info" @after-enter="onModalAfterEnter" @after-leave="onModalAfterLeave">
|
||||
<template #header>
|
||||
<NText v-if="!debugModeEnabled">请输入调试码</NText>
|
||||
<NText v-else>确认关闭调试模式</NText>
|
||||
</template>
|
||||
<template #default>
|
||||
<NInput v-if="expectToShowDebugCodeInput" v-model:value="debugCode" placeholder="输入调试码" @keyup.enter="enableDebugMode" />
|
||||
</template>
|
||||
<template #action>
|
||||
<NButton @click="showDebugCodeModal = false">取消</NButton>
|
||||
<NButton v-if="!debugModeEnabled" type="primary" @click="enableDebugMode">启用</NButton>
|
||||
<NButton v-else type="primary" @click="disableDebugMode">确认</NButton>
|
||||
</template>
|
||||
</NModal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
3
src/components/global/theme-switch/index.ts
Normal file
3
src/components/global/theme-switch/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import ThemeSwitch from './theme-switch.vue';
|
||||
|
||||
export { ThemeSwitch };
|
||||
71
src/components/global/theme-switch/theme-switch.vue
Normal file
71
src/components/global/theme-switch/theme-switch.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<script setup lang="ts">
|
||||
import { useSettingStore } from '@/stores';
|
||||
import { NIcon, NSwitch } from 'naive-ui';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import type { ComponentInstance } from 'vue';
|
||||
|
||||
const settingsStore = useSettingStore();
|
||||
const { darkThemeEnabled } = storeToRefs(settingsStore);
|
||||
|
||||
// 使外部能够获取NSwitch的类型提示
|
||||
defineExpose({} as ComponentInstance<typeof NSwitch>);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSwitch v-model:value="darkThemeEnabled">
|
||||
<template #unchecked-icon>
|
||||
<NIcon>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="bzzmode-light" clip-path="url(#clip0_543_2115)">
|
||||
<path id="fill1" d="M19 12C19 15.866 15.866 19 12 19C8.13401 19 5 15.866 5 12C5 8.13401 8.13401 5 12 5C15.866 5 19 8.13401 19 12Z" fill="transparent" />
|
||||
<path
|
||||
id="stroke1"
|
||||
d="M19 12C19 15.866 15.866 19 12 19C8.13401 19 5 15.866 5 12C5 8.13401 8.13401 5 12 5C15.866 5 19 8.13401 19 12Z"
|
||||
stroke-linecap="square"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
/>
|
||||
<g id="bzzstroke2">
|
||||
<path
|
||||
d="M19.7819 19.7762 19.7791 19.779 19.7764 19.7762 19.7791 19.7734 19.7819 19.7762ZM23.0029 11.9961V12H22.999V11.9961H23.0029ZM19.7791 4.2168 19.7819 4.21956 19.7791 4.22232 19.7764 4.21956 19.7791 4.2168ZM11.999.996094H12.0029V1H11.999V.996094ZM4.22525 4.21956 4.22249 4.22232 4.21973 4.21956 4.22249 4.2168 4.22525 4.21956ZM1.00293 11.9961V12H.999023V11.9961H1.00293ZM4.22249 19.7734 4.22525 19.7762 4.22249 19.779 4.21973 19.7762 4.22249 19.7734ZM11.999 22.9961H12.0029V23H11.999V22.9961Z"
|
||||
stroke-linecap="square"
|
||||
id="stroke2"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</NIcon>
|
||||
</template>
|
||||
<template #checked-icon>
|
||||
<NIcon>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="bzxmode-dark">
|
||||
<path
|
||||
id="fill1"
|
||||
d="M20.5387 14.8522C20.0408 14.9492 19.5263 15 19 15C14.5817 15 11 11.4183 11 7C11 5.54296 11.3194 4.17663 12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21C15.9737 21 19.3459 18.4248 20.5387 14.8522Z"
|
||||
fill="transparent"
|
||||
/>
|
||||
<path
|
||||
id="stroke1"
|
||||
d="M20.5387 14.8522C20.0408 14.9492 19.5263 15 19 15C14.5817 15 11 11.4183 11 7C11 5.54296 11.3194 4.17663 12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21C15.9737 21 19.3459 18.4248 20.5387 14.8522Z"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
/>
|
||||
<g id="bzxstroke2">
|
||||
<path
|
||||
d="M16.625 4 16.6692 4.08081 16.75 4.125 16.6692 4.16919 16.625 4.25 16.5808 4.16919 16.5 4.125 16.5808 4.08081 16.625 4ZM20.5 8.5 20.6768 8.82322 21 9 20.6768 9.17678 20.5 9.5 20.3232 9.17678 20 9 20.3232 8.82322 20.5 8.5Z"
|
||||
id="stroke2"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</NIcon>
|
||||
</template>
|
||||
</NSwitch>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
Reference in New Issue
Block a user