refactor: 重构项目结构

- 优化 `车站-设备-告警`  轮询机制
- 改进设备卡片的布局
- 支持修改设备
- 告警轮询中获取完整告警数据
- 车站告警详情支持导出完整的 `今日告警列表`
- 支持将状态持久化到 `IndexedDB`
- 新增轮询控制 (调试模式)
- 新增离线开发模式 (调试模式)
- 新增 `IndexedDB` 数据控制 (调试模式)
This commit is contained in:
yangsy
2025-12-11 13:42:22 +08:00
commit 37781216b2
278 changed files with 17988 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
export * from './use-alarm-action-column';
export * from './use-camera-snap-column';

View File

@@ -0,0 +1,149 @@
import { deleteCameraIgnoreApi, pageCameraIgnoreApi, saveCameraIgnoreApi, updateDeviceAlarmLogApi, type NdmDeviceAlarmLogResultVO } from '@/apis';
import { DEVICE_TYPE_LITERALS, tryGetDeviceType } from '@/enums';
import { parseErrorFeedback } from '@/utils';
import { useMutation } from '@tanstack/vue-query';
import { NButton, NFlex, NPopconfirm, type DataTableColumn, type DataTableRowData } from 'naive-ui';
import { h, type Ref } from 'vue';
export const useAlarmActionColumn = (tableData: Ref<DataTableRowData[]>) => {
const { mutate: confirmAlarm } = useMutation({
mutationFn: async (params: { id: string | null }) => {
const { id } = params;
if (!id) return;
const alarmLog = tableData.value.find((item) => item.id === id);
if (alarmLog) {
alarmLog['alarmConfirm'] = '1';
}
await updateDeviceAlarmLogApi({ id, alarmConfirm: '1' });
},
onError: (error, variables) => {
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
const { id } = variables;
if (id) {
const alarmLog = tableData.value.find((item) => item.id === id);
if (alarmLog) {
alarmLog['alarmConfirm'] = '2';
}
}
},
});
const { mutate: ignoreCamera } = useMutation({
mutationFn: async (params: { id: string | null }) => {
const { id } = params;
if (!id) return;
const alarmLog = tableData.value.find((item) => item.id === id);
if (!alarmLog) return;
const { records } = await pageCameraIgnoreApi({
model: { deviceId: alarmLog.deviceId },
extra: {},
current: 1,
size: 10,
sort: 'id',
order: 'descending',
});
const ignoredCamera = records.at(0);
if (ignoredCamera) {
window.$message.info('设备已被忽略');
return;
}
await saveCameraIgnoreApi({ deviceId: alarmLog.deviceId });
window.$message.success('忽略设备成功');
},
onError: (error) => {
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
// const { mutate: noticeCamera } = useMutation({
// mutationFn: async (params: { id: string | null }) => {
// const { id } = params;
// if (!id) return;
// const alarmLog = tableData.value.find((item) => item.id === id);
// if (!alarmLog) return;
// const { records } = await pageCameraIgnoreApi({
// model: { deviceId: alarmLog.deviceId },
// extra: {},
// current: 1,
// size: 10,
// sort: 'id',
// order: 'descending',
// });
// if (records.length === 0) {
// window.$message.info('设备未被忽略');
// return;
// }
// await deleteCameraIgnoreApi([...records.map((record) => record.id ?? '')]);
// window.$message.success('取消忽略设备成功');
// },
// onError: (error) => {
// console.error(error);
// const errorFeedback = parseErrorFeedback(error);
// window.$message.error(errorFeedback);
// },
// });
const alarmActionColumn: DataTableColumn<NdmDeviceAlarmLogResultVO> = {
title: '操作',
key: 'action',
width: 120,
align: 'center',
render: (rowData) => {
const { id } = rowData;
return h(
NFlex,
{
size: 'small',
justify: 'center',
align: 'center',
},
{
default: () => [
rowData.alarmConfirm === '1'
? h(NButton, { disabled: true, secondary: true, type: 'info', size: 'tiny' }, { default: () => '确认' })
: h(
NPopconfirm,
{
onPositiveClick: () => confirmAlarm({ id }),
},
{
trigger: () => h(NButton, { secondary: true, type: 'info', size: 'tiny' }, { default: () => '确认' }),
default: () => '确认告警?',
},
),
tryGetDeviceType(rowData.deviceType) === DEVICE_TYPE_LITERALS.ndmCamera && [
h(
NPopconfirm,
{
onPositiveClick: () => ignoreCamera({ id }),
},
{
trigger: () => h(NButton, { tertiary: true, type: 'info', size: 'tiny' }, { default: () => '忽略' }),
default: () => '忽略设备?',
},
),
// h(
// NPopconfirm,
// {
// onPositiveClick: () => noticeCamera({ id }),
// },
// {
// trigger: () => h(NButton, { text: true, type: 'info', size: 'small' }, { icon: () => h(EyeOutlined) }),
// default: () => '取消忽略设备?',
// },
// ),
],
],
},
);
},
};
return {
alarmActionColumn,
};
};

View File

@@ -0,0 +1,66 @@
import { getCameraSnapApi, type NdmDeviceAlarmLogResultVO } from '@/apis';
import { tryGetDeviceType, DEVICE_TYPE_LITERALS } from '@/enums';
import { parseErrorFeedback } from '@/utils';
import { useMutation } from '@tanstack/vue-query';
import { NButton, NImage, type DataTableColumn, type DataTableRowData } from 'naive-ui';
import { h, ref, watch, type Ref } from 'vue';
export const useCameraSnapColumn = (tableData: Ref<DataTableRowData[]>) => {
const { mutateAsync: getSnapByDeviceId } = useMutation({
mutationFn: async (params: { deviceAlarmLog: NdmDeviceAlarmLogResultVO }) => {
const { deviceAlarmLog } = params;
const { deviceId } = deviceAlarmLog;
if (!deviceId) throw new Error('设备ID不能为空');
const snap = await getCameraSnapApi(deviceId);
return snap;
},
onError: (error) => {
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
// 控制每一行的查看按钮loading状态
const loadingMap = ref<Record<string, boolean>>({});
watch(tableData, () => {
loadingMap.value = {};
});
const cameraSnapColumn: DataTableColumn<NdmDeviceAlarmLogResultVO & { snapUrl?: string }> = {
title: '实时画面截图',
key: 'snapUrl',
align: 'center',
render: (rowData) => {
const { deviceType: deviceTypeCode, snapUrl } = rowData;
const deviceType = tryGetDeviceType(deviceTypeCode);
if (deviceType !== DEVICE_TYPE_LITERALS.ndmCamera) return null;
if (!snapUrl) {
const id = rowData.id ?? '';
return h(
NButton,
{
type: 'info',
size: 'small',
loading: !!loadingMap.value[id],
onClick: async () => {
loadingMap.value[id] = true;
try {
const snap = await getSnapByDeviceId({ deviceAlarmLog: rowData });
rowData.snapUrl = snap.url;
} finally {
loadingMap.value[id] = false;
}
},
},
{ default: () => '查看' },
);
} else {
return h(NImage, { src: snapUrl, previewDisabled: false, showToolbar: false });
}
},
};
return {
cameraSnapColumn,
};
};

View File

@@ -0,0 +1 @@
export * from './use-device-tree';

View File

@@ -0,0 +1,89 @@
import type { LineDevices, NdmDeviceResultVO } from '@/apis';
import { tryGetDeviceType, type DeviceType } from '@/enums';
import { ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
export const useDeviceTree = () => {
const route = useRoute();
const router = useRouter();
const selectedStationCode = ref<string>();
const selectedDeviceType = ref<DeviceType>();
const selectedDevice = ref<NdmDeviceResultVO>();
const initFromRoute = (lineDevices: LineDevices) => {
const { stationCode, deviceType, deviceDbId } = route.query;
if (stationCode) {
selectedStationCode.value = stationCode as string;
}
if (deviceType) {
selectedDeviceType.value = deviceType as DeviceType;
}
if (deviceDbId && selectedStationCode.value && selectedDeviceType.value) {
const selectedDeviceDbId = deviceDbId as string;
const stationDevices = lineDevices[selectedStationCode.value];
if (stationDevices) {
const devices = stationDevices[selectedDeviceType.value];
if (devices) {
const device = devices.find((device) => device.id === selectedDeviceDbId);
if (device) {
selectedDevice.value = device;
}
}
}
}
};
const selectDevice = (device: NdmDeviceResultVO, stationCode: string) => {
selectedDevice.value = device;
selectedStationCode.value = stationCode;
const deviceType = tryGetDeviceType(device.deviceType);
if (deviceType) {
selectedDeviceType.value = deviceType;
}
};
const routeDevice = (device: NdmDeviceResultVO, stationCode: string, to: { path: string }) => {
const deviceDbId = device.id;
const deviceType = tryGetDeviceType(device.deviceType);
router.push({
path: to.path,
query: {
stationCode,
deviceType,
deviceDbId,
from: route.path,
},
});
};
const syncToRoute = () => {
const query = { ...route.query };
// 当选中的设备发生变化时删除from参数
if (selectedDevice.value?.id && route.query.deviceDbId !== selectedDevice.value.id) {
delete query['from'];
}
if (selectedStationCode.value) {
query['stationCode'] = selectedStationCode.value;
}
if (selectedDeviceType.value) {
query['deviceType'] = selectedDeviceType.value;
}
if (selectedDevice.value?.id) {
query['deviceDbId'] = selectedDevice.value.id;
}
router.replace({ query });
};
watch(selectedDevice, syncToRoute);
return {
selectedStationCode,
selectedDeviceType,
selectedDevice,
initFromRoute,
selectDevice,
routeDevice,
};
};

4
src/composables/index.ts Normal file
View File

@@ -0,0 +1,4 @@
export * from './alarm';
export * from './device';
export * from './query';
export * from './stomp';

View File

@@ -0,0 +1,4 @@
export * from './use-line-alarms-query';
export * from './use-line-devices-query';
export * from './use-line-stations-query';
export * from './use-version-check-query';

View File

@@ -0,0 +1,88 @@
import { initStationAlarms, pageDeviceAlarmLogApi, type Station } from '@/apis';
import { LINE_ALARMS_QUERY_KEY, STATION_ALARMS_MUTATION_KEY } from '@/constants';
import { tryGetDeviceType } from '@/enums';
import { useAlarmStore, useStationStore } from '@/stores';
import { parseErrorFeedback } from '@/utils';
import { CancelledError, useMutation, useQuery } from '@tanstack/vue-query';
import { isCancel } from 'axios';
import dayjs from 'dayjs';
import { storeToRefs } from 'pinia';
import { computed } from 'vue';
export const useStationAlarmsMutation = () => {
const alarmStore = useAlarmStore();
return useMutation({
mutationKey: [STATION_ALARMS_MUTATION_KEY],
mutationFn: async (params: { station: Station; signal?: AbortSignal }) => {
const { station, signal } = params;
const stationAlarms = initStationAlarms();
if (!station.online) {
return stationAlarms;
}
const now = dayjs();
const todayStart = now.startOf('date').valueOf();
const todayEnd = now.endOf('date').valueOf();
const { records } = await pageDeviceAlarmLogApi(
{
model: {
stationCode: station.code,
},
extra: {
alarmDate_ge: todayStart,
alarmDate_le: todayEnd,
},
size: 50000,
current: 1,
sort: 'alarmDate',
order: 'descending',
},
{
stationCode: station.code,
signal,
},
);
for (const alarm of records) {
const { deviceType: deviceTypeCode } = alarm;
const deviceType = tryGetDeviceType(deviceTypeCode);
if (!!deviceType) {
stationAlarms[deviceType].unshift(alarm);
}
}
stationAlarms['unclassified'] = records;
return stationAlarms;
},
onSuccess: (stationAlarms, { station }) => {
alarmStore.setStationAlarms(station.code, stationAlarms);
},
onError: (error) => {
console.error(error);
if (isCancel(error) || error instanceof CancelledError) return;
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
};
/**
* 由 `useLineStationsQuery` 显式触发
* @see [use-line-stations-query.ts](./use-line-stations-query.ts)
*/
export const useLineAlarmsQuery = () => {
const stationStore = useStationStore();
const { stations } = storeToRefs(stationStore);
const { mutateAsync: getStationAlarms } = useStationAlarmsMutation();
return useQuery({
queryKey: computed(() => [LINE_ALARMS_QUERY_KEY]),
enabled: false,
queryFn: async ({ signal }) => {
console.time(LINE_ALARMS_QUERY_KEY);
for (const station of stations.value) {
await getStationAlarms({ station, signal }).catch(() => {});
}
console.timeEnd(LINE_ALARMS_QUERY_KEY);
return null;
},
});
};

View File

@@ -0,0 +1,55 @@
import { getAllDevicesApi, initStationDevices, type Station } from '@/apis';
import { LINE_DEVICES_QUERY_KEY, STATION_DEVICES_MUTATION_KEY } from '@/constants';
import { useDeviceStore, useStationStore } from '@/stores';
import { parseErrorFeedback } from '@/utils';
import { CancelledError, useMutation, useQuery } from '@tanstack/vue-query';
import { isCancel } from 'axios';
import { storeToRefs } from 'pinia';
import { computed } from 'vue';
export const useStationDevicesMutation = () => {
const deviceStore = useDeviceStore();
return useMutation({
mutationKey: [STATION_DEVICES_MUTATION_KEY],
mutationFn: async (params: { station: Station; signal?: AbortSignal }) => {
const { station, signal } = params;
if (!station.online) {
return initStationDevices();
}
return await getAllDevicesApi({ stationCode: station.code, signal });
},
onSuccess: (devices, { station }) => {
deviceStore.setStationDevices(station.code, devices);
},
onError: (error) => {
console.error(error);
if (isCancel(error) || error instanceof CancelledError) return;
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
};
/**
* 由 `useLineStationsQuery` 显式触发
* @see [use-line-stations-query.ts](./use-line-stations-query.ts)
*/
export const useLineDevicesQuery = () => {
const stationStore = useStationStore();
const { stations } = storeToRefs(stationStore);
const { mutateAsync: getStationDevices } = useStationDevicesMutation();
return useQuery({
queryKey: computed(() => [LINE_DEVICES_QUERY_KEY]),
enabled: false,
queryFn: async ({ signal }) => {
console.time(LINE_DEVICES_QUERY_KEY);
for (const station of stations.value) {
await getStationDevices({ station, signal }).catch(() => {});
}
console.timeEnd(LINE_DEVICES_QUERY_KEY);
return null;
},
});
};

View File

@@ -0,0 +1,73 @@
import { batchVerifyApi, type Station } from '@/apis';
import { LINE_STATIONS_MUTATION_KEY, LINE_STATIONS_QUERY_KEY } from '@/constants';
import { usePollingStore, useStationStore } from '@/stores';
import { getAppEnvConfig, parseErrorFeedback } from '@/utils';
import { CancelledError, useMutation, useQuery } from '@tanstack/vue-query';
import axios, { isCancel } from 'axios';
import dayjs from 'dayjs';
import { storeToRefs } from 'pinia';
import { computed } from 'vue';
import { useLineDevicesQuery } from './use-line-devices-query';
import { useLineAlarmsQuery } from './use-line-alarms-query';
export const useLineStationsMutation = () => {
const stationStore = useStationStore();
return useMutation({
mutationKey: [LINE_STATIONS_MUTATION_KEY],
mutationFn: async (params: { signal?: AbortSignal }) => {
const { signal } = params;
const { data: ndmStationList } = await axios.get<{ code: string; name: string }[]>(`/minio/ndm/ndm-stations.json?_t=${dayjs().unix()}`, { signal });
const stations = ndmStationList.map<Station>((station) => ({
code: station.code ?? '',
name: station.name ?? '',
online: false,
ip: '',
}));
const verifyList = await batchVerifyApi({ signal });
return stations.map((station) => ({
...station,
online: !!verifyList.find((verify) => verify.stationCode === station.code)?.onlineState,
ip: verifyList.find((verify) => verify.stationCode === station.code)?.ipAddress ?? '',
}));
},
onSuccess: (stations) => {
stationStore.setStations(stations);
},
onError: (error) => {
console.error(error);
if (isCancel(error) || error instanceof CancelledError) return;
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
};
export const useLineStationsQuery = () => {
const pollingStore = usePollingStore();
const { pollingEnabled } = storeToRefs(pollingStore);
const { requestInterval } = getAppEnvConfig();
const { mutateAsync: getLineStations } = useLineStationsMutation();
const { refetch: refetchLineDevicesQuery } = useLineDevicesQuery();
const { refetch: refetchLineAlarmsQuery } = useLineAlarmsQuery();
return useQuery({
queryKey: computed(() => [LINE_STATIONS_QUERY_KEY]),
enabled: computed(() => pollingEnabled.value),
refetchInterval: requestInterval * 1000,
staleTime: (requestInterval * 1000) / 2,
queryFn: async ({ signal }) => {
console.time(LINE_STATIONS_QUERY_KEY);
await getLineStations({ signal }).catch(() => {});
console.timeEnd(LINE_STATIONS_QUERY_KEY);
if (!pollingEnabled.value) return null;
await refetchLineDevicesQuery();
if (!pollingEnabled.value) return null;
await refetchLineAlarmsQuery();
return null;
},
});
};

View File

@@ -0,0 +1,75 @@
import { verifyApi, type VersionInfo } from '@/apis';
import { VERSION_CHECK_QUERY_KEY } from '@/constants';
import { useSettingStore, useUserStore } from '@/stores';
import { useQuery, useQueryClient } from '@tanstack/vue-query';
import axios from 'axios';
import { useThemeVars } from 'naive-ui';
import { storeToRefs } from 'pinia';
import { computed, h, ref, watch } from 'vue';
export const useVersionCheckQuery = () => {
const localVersionInfo = ref<VersionInfo>();
const showDialog = ref<boolean>(false);
const themeVars = useThemeVars();
const queryClient = useQueryClient();
const userStore = useUserStore();
const { userLoginResult } = storeToRefs(userStore);
const settingStore = useSettingStore();
const { offlineDev } = storeToRefs(settingStore);
const { data: remoteVersionInfo, dataUpdatedAt } = useQuery({
queryKey: [VERSION_CHECK_QUERY_KEY],
enabled: computed(() => !offlineDev.value),
refetchInterval: 10 * 1000,
queryFn: async ({ signal }) => {
if (!!userLoginResult.value?.token) await verifyApi({ signal });
const { data } = await axios.get<VersionInfo>(`/manifest.json?t=${Date.now()}`, { signal });
return data;
},
});
watch(offlineDev, (offline) => {
if (offline) {
queryClient.cancelQueries({ queryKey: [VERSION_CHECK_QUERY_KEY] });
}
});
watch(dataUpdatedAt, () => {
const newVersionInfo = remoteVersionInfo.value;
if (!newVersionInfo) return;
if (!localVersionInfo.value) {
localVersionInfo.value = newVersionInfo;
return;
}
const { version: localVersion, buildTime: localBuildTime } = localVersionInfo.value;
const { version: remoteVersion, buildTime: remoteBuildTime } = newVersionInfo;
if ((localVersion !== remoteVersion || localBuildTime !== remoteBuildTime) && !showDialog.value) {
showDialog.value = true;
window.$dialog.info({
title: '发现新版本',
content: () =>
h('div', {}, [
h('div', {}, { default: () => `当前版本:${localVersionInfo.value?.version}` }),
h('div', {}, { default: () => `最新版本:${newVersionInfo.version}` }),
h('div', {}, { default: () => '请点击刷新页面以更新' }),
h('div', { style: { marginTop: '8px', fontWeight: '700', color: themeVars.value.warningColor } }, { default: () => '⚠️ 注意,更新后可能需要重新登录或调整系统设置!' }),
]),
positiveText: '刷新页面',
maskClosable: false,
onPositiveClick: () => {
window.location.reload();
},
negativeText: '稍后检查',
onNegativeClick: () => {
showDialog.value = false;
},
onClose: () => {
showDialog.value = false;
},
});
}
});
};

View File

@@ -0,0 +1 @@
export * from './use-stomp-client';

View File

@@ -0,0 +1,123 @@
import type { NdmDeviceAlarmLogResultVO, Station, SyncCameraResult } from '@/apis';
import { ALARM_TOPIC, SYNC_CAMERA_STATUS_TOPIC } from '@/constants';
import { useAlarmStore, useSettingStore, useStationStore } from '@/stores';
import { Client } from '@stomp/stompjs';
import { watchDebounced } from '@vueuse/core';
import destr from 'destr';
import { storeToRefs } from 'pinia';
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { useStationAlarmsMutation } from '../query';
const getBrokerUrl = () => {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const { host } = window.location;
const endpoint = '/ws';
const brokerURL = `${protocol}//${host}${endpoint}`;
return brokerURL;
};
export const useStompClient = () => {
const stationStore = useStationStore();
const { stations } = storeToRefs(stationStore);
const alarmStore = useAlarmStore();
const { unreadLineAlarms } = storeToRefs(alarmStore);
const settingStore = useSettingStore();
const { offlineDev } = storeToRefs(settingStore);
const { mutate: refreshStationAlarms } = useStationAlarmsMutation();
const stompClient = ref<Client | null>(null);
const syncCameraResult = ref<Record<Station['code'], SyncCameraResult>>({});
onMounted(() => {
stompClient.value = new Client({
brokerURL: getBrokerUrl(),
reconnectDelay: 5000,
heartbeatIncoming: 10000,
heartbeatOutgoing: 10000,
onConnect: () => {
console.log('Stomp连接成功');
stompClient.value?.subscribe(ALARM_TOPIC, (message) => {
const alarm = destr<NdmDeviceAlarmLogResultVO>(message.body);
if (alarm.alarmCategory === '1') {
alarmStore.pushUnreadAlarm(alarm);
}
});
stompClient.value?.subscribe(SYNC_CAMERA_STATUS_TOPIC, (message) => {
const { stationCode, startTime, endTime, insertList, updateList, deleteList } = destr<SyncCameraResult>(message.body);
syncCameraResult.value[stationCode] = { stationCode, startTime, endTime, insertList, updateList, deleteList };
});
},
onDisconnect: () => {
console.log('Stomp连接断开');
stompClient.value?.unsubscribe(ALARM_TOPIC);
stompClient.value?.unsubscribe(SYNC_CAMERA_STATUS_TOPIC);
},
onStompError: (frame) => {
console.log('Stomp错误', frame);
window.$message.error('Stomp错误');
},
onWebSocketError: (event: Event) => {
console.log('WebSocket错误', event);
window.$message.error('WebSocket错误');
},
});
if (!offlineDev.value) {
stompClient.value.activate();
}
});
onBeforeUnmount(() => {
stompClient.value?.deactivate();
stompClient.value = null;
});
watch(offlineDev, (offline) => {
if (offline) {
stompClient.value?.deactivate();
} else {
stompClient.value?.activate();
}
});
// 当有车站的未读报警变化,即新收到告警时,需要同步告警数据,
// 但告警可能非常频繁,所以需要防抖处理
const abortControllerMap = ref(new Map<Station['code'], AbortController>());
watchDebounced(
() => Object.entries(unreadLineAlarms.value).map(([stationCode, stationAlarms]) => ({ stationCode, count: stationAlarms['unclassified'].length })),
(newValue, oldValue) => {
// 启用离线模式时,跳过处理
if (offlineDev.value) return;
if (newValue.length === 0) return;
const codes: Station['code'][] = [];
newValue.forEach(({ stationCode, count }) => {
const prevState = oldValue.find((stat) => stat.stationCode === stationCode);
if (!prevState || count !== prevState.count) {
codes.push(stationCode);
}
});
// console.log('以下车站收到新告警:', codes);
for (const stationCode of codes) {
const station = stations.value.find((station) => station.code === stationCode);
if (!station) continue;
abortControllerMap.value.get(stationCode)?.abort();
abortControllerMap.value.set(stationCode, new AbortController());
refreshStationAlarms({ station, signal: abortControllerMap.value.get(stationCode)?.signal });
}
},
{
debounce: 2500,
maxWait: 5000,
},
);
return {
stompClient,
syncCameraResult,
afterCheckSyncCameraResult: () => {
syncCameraResult.value = {};
},
};
};