refactor(device): 设备列表改用上游 Crud 体系,移除 3932 行本地分叉实现
列表、搜索、新增、编辑全部改为 CrudPage/CrudSearch/CrudForm 配置驱动,在线状态通过独立响应式字典接收 SSE 推送。
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,274 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- drawer -->
|
||||
<template v-if="modalConfig.component === 'drawer'">
|
||||
<el-drawer
|
||||
v-model="modalVisible"
|
||||
v-bind="{ destroyOnClose: true, ...modalConfig.drawer }"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form ref="formRef" v-bind="modalConfig.form" :model="formData" :rules="formRules">
|
||||
<el-row :gutter="20">
|
||||
<template v-for="item in formItems" :key="item.prop">
|
||||
<el-col v-show="!item.hidden" v-bind="item.col">
|
||||
<el-form-item :label="item.label" :prop="item.prop">
|
||||
<!-- Label -->
|
||||
<template #label>
|
||||
<span>
|
||||
{{ item?.label || "" }}
|
||||
<el-tooltip v-if="item?.tips" v-bind="getTooltipProps(item.tips)">
|
||||
<QuestionFilled class="w-4 h-4 mx-1" />
|
||||
</el-tooltip>
|
||||
<span v-if="modalConfig.colon" class="ml-0.5">:</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<!-- components -->
|
||||
<template v-if="item.type === 'custom'">
|
||||
<slot
|
||||
:name="item.slotName ?? item.prop"
|
||||
:prop="item.prop"
|
||||
:form-data="formData"
|
||||
:attrs="item.attrs"
|
||||
></slot>
|
||||
</template>
|
||||
<component
|
||||
:is="componentMap.get(item.type)"
|
||||
v-else
|
||||
v-model.trim="formData[item.prop]"
|
||||
v-bind="{ style: { width: '100%' }, ...item.attrs }"
|
||||
>
|
||||
<template v-if="['select', 'radio', 'checkbox'].includes(item.type)">
|
||||
<component
|
||||
:is="childrenMap.get(item.type)"
|
||||
v-for="opt in item.options"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
></component>
|
||||
</template>
|
||||
|
||||
<template v-if="item?.slotName && $slots[item.slotName]" #[item.slotName]>
|
||||
<slot :name="item.slotName" />
|
||||
</template>
|
||||
</component>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</template>
|
||||
</el-row>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button v-if="!formDisable" type="primary" @click="handleSubmit">确定</el-button>
|
||||
<el-button @click="handleClose">关闭</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
<!-- dialog -->
|
||||
<template v-else>
|
||||
<el-dialog
|
||||
v-model="modalVisible"
|
||||
v-bind="{ destroyOnClose: true, alignCenter: true, ...modalConfig.dialog }"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form ref="formRef" v-bind="modalConfig.form" :model="formData" :rules="formRules">
|
||||
<el-scrollbar max-height="70vh" :view-style="{ overflowX: 'hidden' }">
|
||||
<el-row :gutter="20">
|
||||
<template v-for="item in formItems" :key="item.prop">
|
||||
<el-col v-show="!item.hidden" v-bind="item.col">
|
||||
<el-form-item :label="item.label" :prop="item.prop">
|
||||
<template #label>
|
||||
<span>
|
||||
{{ item?.label || "" }}
|
||||
<el-tooltip v-if="item?.tips" v-bind="getTooltipProps(item.tips)">
|
||||
<QuestionFilled class="w-4 h-4 mx-1" />
|
||||
</el-tooltip>
|
||||
<span v-if="modalConfig.colon" class="ml-0.5">:</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template v-if="item.type === 'custom'">
|
||||
<slot
|
||||
:name="item.slotName ?? item.prop"
|
||||
:prop="item.prop"
|
||||
:form-data="formData"
|
||||
:attrs="item.attrs"
|
||||
></slot>
|
||||
</template>
|
||||
<component
|
||||
:is="componentMap.get(item.type)"
|
||||
v-else
|
||||
v-model.trim="formData[item.prop]"
|
||||
v-bind="{ style: { width: '100%' }, ...item.attrs }"
|
||||
>
|
||||
<template v-if="['select', 'radio', 'checkbox'].includes(item.type)">
|
||||
<component
|
||||
:is="childrenMap.get(item.type)"
|
||||
v-for="opt in item.options"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
></component>
|
||||
</template>
|
||||
|
||||
<template v-if="item?.slotName && $slots[item.slotName]" #[item.slotName]>
|
||||
<slot :name="item.slotName" />
|
||||
</template>
|
||||
</component>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</template>
|
||||
</el-row>
|
||||
<slot name="bottom" :form-data="formData"></slot>
|
||||
</el-scrollbar>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button v-if="!formDisable" type="primary" @click="handleSubmit">确定</el-button>
|
||||
<el-button @click="handleClose">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useThrottleFn } from "@vueuse/core";
|
||||
import cloneDeep from "lodash-es/cloneDeep";
|
||||
import type { FormInstance, FormRules } from "element-plus";
|
||||
import type { IComponentType, IModalConfig, IObject } from "./types";
|
||||
import InputTag from "@/components/InputTag/index.vue";
|
||||
import IconSelect from "@/components/IconSelect/index.vue";
|
||||
|
||||
defineSlots<{ [key: string]: (_args: any) => any }>();
|
||||
// 定义接收的属性
|
||||
const props = defineProps<{ modalConfig: IModalConfig }>();
|
||||
// 自定义事件
|
||||
const emit = defineEmits<{ submitClick: []; customSubmit: [queryParams: IObject] }>();
|
||||
// 组件映射
|
||||
|
||||
const componentMap = new Map<IComponentType, any>([
|
||||
// @ts-ignore
|
||||
["input", markRaw(ElInput)], // @ts-ignore
|
||||
["select", markRaw(ElSelect)], // @ts-ignore
|
||||
["switch", markRaw(ElSwitch)], // @ts-ignore
|
||||
["cascader", markRaw(ElCascader)], // @ts-ignore
|
||||
["input-number", markRaw(ElInputNumber)], // @ts-ignore
|
||||
["input-tag", markRaw(InputTag)], // @ts-ignore
|
||||
["time-picker", markRaw(ElTimePicker)], // @ts-ignore
|
||||
["time-select", markRaw(ElTimeSelect)], // @ts-ignore
|
||||
["date-picker", markRaw(ElDatePicker)], // @ts-ignore
|
||||
["tree-select", markRaw(ElTreeSelect)], // @ts-ignore"
|
||||
["custom-tag", markRaw(InputTag)], // @ts-ignore
|
||||
["text", markRaw(ElText)], // @ts-ignore
|
||||
["radio", markRaw(ElRadioGroup)], // @ts-ignore"
|
||||
["checkbox", markRaw(ElCheckboxGroup)], // @ts-ignore"
|
||||
["icon-select", markRaw(IconSelect)], // @ts-ignore"
|
||||
["custom", ""],
|
||||
]);
|
||||
const childrenMap = new Map<IComponentType, any>([
|
||||
// @ts-ignore
|
||||
["select", markRaw(ElOption)], // @ts-ignore
|
||||
["radio", markRaw(ElRadio)], // @ts-ignore"
|
||||
["checkbox", markRaw(ElCheckbox)],
|
||||
]);
|
||||
|
||||
const pk = props.modalConfig.pk ?? "id"; // 主键名,用于表单数据处理
|
||||
const modalVisible = ref(false); // 弹窗显示状态"
|
||||
const formRef = ref<FormInstance>(); // 表单实例
|
||||
const formItems = reactive(props.modalConfig.formItems ?? []); // 表单配置项"
|
||||
const formData = reactive<IObject>({}); // 表单数据
|
||||
const formRules: FormRules = {}; // 表单验证规则
|
||||
const formDisable = ref(false); // 表单禁用状态"
|
||||
|
||||
// 获取 tooltip 提示框属性
|
||||
const getTooltipProps = (tips: string | IObject) => {
|
||||
return typeof tips === "string" ? { content: tips } : tips;
|
||||
};
|
||||
// 隐藏弹窗
|
||||
const handleClose = () => {
|
||||
modalVisible.value = false;
|
||||
formRef.value?.resetFields();
|
||||
};
|
||||
// 设置表单项
|
||||
const setFormData = (data: IObject) => {
|
||||
for (const key in formData) {
|
||||
if (Object.prototype.hasOwnProperty.call(formData, key) && key in data) {
|
||||
formData[key] = cloneDeep(data[key]);
|
||||
}
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(data, pk)) {
|
||||
formData[pk] = cloneDeep(data[pk]);
|
||||
}
|
||||
};
|
||||
// 表单提交
|
||||
const handleSubmit = useThrottleFn(() => {
|
||||
formRef.value?.validate((valid: boolean) => {
|
||||
if (!valid) return;
|
||||
if (typeof props.modalConfig.beforeSubmit === "function") {
|
||||
props.modalConfig.beforeSubmit(formData);
|
||||
}
|
||||
if (!props.modalConfig?.formAction) {
|
||||
emit("customSubmit", formData);
|
||||
handleClose();
|
||||
return;
|
||||
}
|
||||
props.modalConfig.formAction(formData).then(() => {
|
||||
if (props.modalConfig.component === "drawer") {
|
||||
ElMessage.success(`${props.modalConfig.drawer?.title}成功`);
|
||||
} else {
|
||||
ElMessage.success(`${props.modalConfig.dialog?.title}成功`);
|
||||
}
|
||||
emit("submitClick");
|
||||
handleClose();
|
||||
});
|
||||
});
|
||||
}, 3000);
|
||||
|
||||
onMounted(() => {
|
||||
formItems.forEach((item) => {
|
||||
if (item.initFn) {
|
||||
item.initFn(item);
|
||||
}
|
||||
formRules[item.prop] = item?.rules ?? [];
|
||||
props.modalConfig.form = { labelWidth: "auto", ...props.modalConfig?.form };
|
||||
|
||||
if (["input-tag", "custom-tag", "cascader"].includes(item.type)) {
|
||||
formData[item.prop] = Array.isArray(item.initialValue) ? item.initialValue : [];
|
||||
} else if (item.type === "input-number") {
|
||||
formData[item.prop] = item.initialValue ?? null;
|
||||
} else {
|
||||
formData[item.prop] = item.initialValue ?? "";
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 暴露的属性和方法
|
||||
defineExpose({
|
||||
setFormData,
|
||||
// 展示/隐藏 modal
|
||||
setModalVisible: (visible: boolean = true) => (modalVisible.value = visible),
|
||||
// 获取表单数据
|
||||
getFormData: (key: string) => formData[key] ?? formData,
|
||||
// 设置表单项
|
||||
setFormItemData: (key: string, value: any) => (formData[key] = value),
|
||||
// 禁用表单
|
||||
handleDisabled: (disable: boolean) => {
|
||||
formDisable.value = disable;
|
||||
props.modalConfig.form = {
|
||||
...props.modalConfig.form,
|
||||
disabled: disable,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-input-number .el-input__inner) {
|
||||
text-align: left;
|
||||
}
|
||||
:deep(.el-input-number.is-without-controls .el-input__wrapper) {
|
||||
padding-right: 11px !important ;
|
||||
padding-left: 11px !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,169 +0,0 @@
|
||||
<template>
|
||||
<div v-show="visible">
|
||||
<el-card v-bind="cardAttrs">
|
||||
<el-form ref="queryFormRef" :model="queryParams" v-bind="formAttrs" :class="isGrid">
|
||||
<template v-for="(item, index) in formItems" :key="item.prop">
|
||||
<el-form-item
|
||||
v-show="isExpand ? true : index < showNumber"
|
||||
:label="item?.label"
|
||||
:prop="item.prop"
|
||||
>
|
||||
<!-- Label -->
|
||||
<template #label>
|
||||
<span class="flex-y-center">
|
||||
{{ item?.label || "" }}
|
||||
<el-tooltip v-if="item?.tips" v-bind="getTooltipProps(item.tips)">
|
||||
<QuestionFilled class="w-4 h-4 mx-1" />
|
||||
</el-tooltip>
|
||||
<span v-if="searchConfig.colon" class="ml-0.5">:</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<!-- 自定义插槽 -->
|
||||
<slot
|
||||
v-if="item.type === 'custom'"
|
||||
:name="item.slotName"
|
||||
:form-data="queryParams"
|
||||
:prop="item.prop"
|
||||
:attrs="{ style: { width: '100%' }, ...item.attrs }"
|
||||
/>
|
||||
<el-cascader
|
||||
v-else-if="item.type === 'cascader'"
|
||||
v-model.trim="queryParams[item.prop]"
|
||||
v-bind="{ style: { width: '100%' }, ...item.attrs }"
|
||||
v-on="item.events || {}"
|
||||
/>
|
||||
<component
|
||||
:is="componentMap.get(item.type)"
|
||||
v-else
|
||||
v-model.trim="queryParams[item.prop]"
|
||||
v-bind="{ style: { width: '100%' }, ...item.attrs }"
|
||||
v-on="item.events || {}"
|
||||
>
|
||||
<template v-if="item.type === 'select'">
|
||||
<template v-for="opt in item.options" :key="opt.value">
|
||||
<el-option :label="opt.label" :value="opt.value" />
|
||||
</template>
|
||||
</template>
|
||||
</component>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<el-form-item :class="{ 'col-[auto/-1] justify-self-end': searchConfig?.grid === 'right' }">
|
||||
<el-button icon="search" type="primary" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="refresh" @click="handleReset">重置</el-button>
|
||||
<!-- 展开/收起 -->
|
||||
<template v-if="isExpandable && formItems.length > showNumber">
|
||||
<el-link class="ml-3" type="primary" underline="never" @click="isExpand = !isExpand">
|
||||
{{ isExpand ? "收起" : "展开" }}
|
||||
<component :is="isExpand ? ArrowUp : ArrowDown" class="w-4 h-4 ml-2" />
|
||||
</el-link>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { IObject, IForm, ISearchConfig, ISearchComponent } from "./types";
|
||||
import { ArrowUp, ArrowDown } from "@element-plus/icons-vue";
|
||||
import type { FormInstance } from "element-plus";
|
||||
import InputTag from "@/components/InputTag/index.vue";
|
||||
|
||||
// 定义接收的属性
|
||||
const props = defineProps<{ searchConfig: ISearchConfig }>();
|
||||
// 自定义事件
|
||||
const emit = defineEmits<{
|
||||
queryClick: [queryParams: IObject];
|
||||
resetClick: [queryParams: IObject];
|
||||
}>();
|
||||
// 组件映射表
|
||||
const componentMap = new Map<ISearchComponent, any>([
|
||||
// @ts-ignore
|
||||
["input", markRaw(ElInput)], // @ts-ignore
|
||||
["select", markRaw(ElSelect)], // @ts-ignore
|
||||
["cascader", markRaw(ElCascader)], // @ts-ignore
|
||||
["input-number", markRaw(ElInputNumber)], // @ts-ignore
|
||||
["date-picker", markRaw(ElDatePicker)], // @ts-ignore
|
||||
["time-picker", markRaw(ElTimePicker)], // @ts-ignore
|
||||
["time-select", markRaw(ElTimeSelect)], // @ts-ignore
|
||||
["tree-select", markRaw(ElTreeSelect)], // @ts-ignore
|
||||
["input-tag", markRaw(ElInputTag)], // @ts-ignore
|
||||
["custom-tag", markRaw(InputTag)],
|
||||
]);
|
||||
|
||||
// 存储表单实例
|
||||
const queryFormRef = ref<FormInstance>();
|
||||
// 存储查询参数
|
||||
const queryParams = reactive<IObject>({});
|
||||
// 是否显示
|
||||
const visible = ref(true);
|
||||
// 响应式的formItems
|
||||
const formItems = reactive(props.searchConfig?.formItems ?? []);
|
||||
// 是否可展开/收缩
|
||||
const isExpandable = ref(props.searchConfig?.isExpandable ?? true);
|
||||
// 是否已展开
|
||||
const isExpand = ref(false);
|
||||
// 表单项展示数量,若可展开,超出展示数量的表单项隐藏
|
||||
const showNumber = computed(() =>
|
||||
isExpandable.value ? (props.searchConfig?.showNumber ?? 3) : formItems.length,
|
||||
);
|
||||
// 卡片组件自定义属性(阴影、自定义边距样式等)
|
||||
const cardAttrs = computed<IObject>(() => {
|
||||
return { shadow: "never", style: { "margin-bottom": "12px" }, ...props.searchConfig?.cardAttrs };
|
||||
});
|
||||
// 表单组件自定义属性(label位置、宽度、对齐方式等)
|
||||
const formAttrs = computed<IForm>(() => {
|
||||
return { inline: true, ...props.searchConfig?.form };
|
||||
});
|
||||
// 是否使用自适应网格布局
|
||||
const isGrid = computed(() =>
|
||||
props.searchConfig?.grid
|
||||
? "grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 3xl:grid-cols-5 4xl:grid-cols-6 gap-5"
|
||||
: "flex flex-wrap gap-x-8 gap-y-4",
|
||||
);
|
||||
|
||||
// 获取tooltip提示框属性
|
||||
const getTooltipProps = (tips: string | IObject) => {
|
||||
return typeof tips === "string" ? { content: tips } : tips;
|
||||
};
|
||||
// 查询/重置操作
|
||||
const handleQuery = () => emit("queryClick", queryParams);
|
||||
const handleReset = () => {
|
||||
queryFormRef.value?.resetFields();
|
||||
emit("resetClick", queryParams);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
formItems.forEach((item) => {
|
||||
if (item?.initFn) {
|
||||
item.initFn(item);
|
||||
}
|
||||
if (["input-tag", "custom-tag", "cascader"].includes(item?.type ?? "")) {
|
||||
queryParams[item.prop] = Array.isArray(item.initialValue) ? item.initialValue : [];
|
||||
} else if (item.type === "input-number") {
|
||||
queryParams[item.prop] = item.initialValue ?? null;
|
||||
} else {
|
||||
queryParams[item.prop] = item.initialValue ?? "";
|
||||
}
|
||||
});
|
||||
});
|
||||
// 暴露的属性和方法
|
||||
defineExpose({
|
||||
// 获取分页数据
|
||||
getQueryParams: () => queryParams,
|
||||
// 显示/隐藏 SearchForm
|
||||
toggleVisible: () => (visible.value = !visible.value),
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-input-number .el-input__inner) {
|
||||
text-align: left;
|
||||
}
|
||||
.el-form-item {
|
||||
margin-right: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,222 +0,0 @@
|
||||
import type { DialogProps, DrawerProps, FormItemRule, PaginationProps } from "element-plus";
|
||||
import type { FormProps, TableProps, ColProps, ButtonProps, CardProps } from "element-plus";
|
||||
import type PageContent from "./DevicePageContent.vue";
|
||||
import type PageModal from "./DevicePageModal.vue";
|
||||
import type PageSearch from "./DevicePageSearch.vue";
|
||||
import type SnDetail from "./DeviceSnDetail.vue";
|
||||
import type { CSSProperties } from "vue";
|
||||
import type { PageResult } from "@/api/common";
|
||||
|
||||
export type PageSearchInstance = InstanceType<typeof PageSearch>;
|
||||
export type PageContentInstance = InstanceType<typeof PageContent>;
|
||||
export type PageModalInstance = InstanceType<typeof PageModal>;
|
||||
export type SnDetailInstance = InstanceType<typeof SnDetail>;
|
||||
|
||||
export type IObject = Record<string, any>;
|
||||
|
||||
type DateComponent = "date-picker" | "time-picker" | "time-select" | "custom-tag" | "input-tag";
|
||||
type InputComponent = "input" | "select" | "input-number" | "cascader" | "tree-select";
|
||||
type OtherComponent = "text" | "radio" | "checkbox" | "switch" | "icon-select" | "custom";
|
||||
export type ISearchComponent = DateComponent | InputComponent | "custom";
|
||||
export type IComponentType = DateComponent | InputComponent | OtherComponent;
|
||||
|
||||
type ToolbarLeft = "add" | "delete" | "import" | "export";
|
||||
type ToolbarRight = "refresh" | "filter" | "imports" | "exports" | "search";
|
||||
type ToolbarTable = "edit" | "view" | "delete";
|
||||
export type IToolsButton = {
|
||||
name: string; // 按钮名称
|
||||
text?: string; // 按钮文本
|
||||
perm?: Array<string> | string; // 权限标识(可以是完整权限字符串如'sys:user:create'或操作权限如'create')
|
||||
attrs?: Partial<ButtonProps> & { style?: CSSProperties }; // 按钮属性
|
||||
render?: (row: IObject) => boolean; // 条件渲染
|
||||
children?: IToolsButton[];
|
||||
};
|
||||
export type IToolsDefault = ToolbarLeft | ToolbarRight | ToolbarTable | IToolsButton;
|
||||
|
||||
export interface IOperateData {
|
||||
name: string;
|
||||
row: IObject;
|
||||
column: IObject;
|
||||
$index: number;
|
||||
}
|
||||
|
||||
export interface ISearchConfig {
|
||||
// 权限前缀(如sys:user,用于组成权限标识),不提供则不进行权限校验
|
||||
permPrefix?: string;
|
||||
// 标签冒号(默认:false)
|
||||
colon?: boolean;
|
||||
// 表单项(默认:[])
|
||||
formItems?: IFormItems<ISearchComponent>;
|
||||
// 是否开启展开和收缩(默认:true)
|
||||
isExpandable?: boolean;
|
||||
// 默认展示的表单项数量(默认:3)
|
||||
showNumber?: number;
|
||||
// 卡片属性
|
||||
cardAttrs?: Partial<CardProps> & { style?: CSSProperties };
|
||||
// form组件属性
|
||||
form?: IForm;
|
||||
// 自适应网格布局(使用时表单不要添加 style: { width: "200px" })
|
||||
grid?: boolean | "left" | "right";
|
||||
}
|
||||
|
||||
export interface IContentConfig<TQuery = any, TItem = any> {
|
||||
// 权限前缀(如sys:user,用于组成权限标识),不提供则不进行权限校验
|
||||
permPrefix?: string;
|
||||
// table组件属性
|
||||
table?: Partial<Omit<TableProps<any>, "data">>;
|
||||
// 分页组件位置(默认:left)
|
||||
pagePosition?: "left" | "right";
|
||||
// pagination组件属性
|
||||
pagination?:
|
||||
| boolean
|
||||
| Partial<
|
||||
Omit<
|
||||
PaginationProps,
|
||||
"v-model:page-size" | "v-model:current-page" | "total" | "currentPage"
|
||||
>
|
||||
>;
|
||||
// 列表的网络请求函数(需返回promise)
|
||||
indexAction: (queryParams: TQuery) => Promise<PageResult<TItem> | TItem[]>;
|
||||
// 默认的分页相关的请求参数
|
||||
request?: {
|
||||
pageName: string;
|
||||
limitName: string;
|
||||
};
|
||||
// 分页接口统一返回 PageResult { list, total }
|
||||
// 修改属性的网络请求函数(需返回promise)
|
||||
modifyAction?: (data: {
|
||||
[key: string]: any;
|
||||
field: string;
|
||||
value: boolean | string | number;
|
||||
}) => Promise<any>;
|
||||
// 删除的网络请求函数(需返回promise)
|
||||
deleteAction?: (ids: string) => Promise<any>;
|
||||
// 后端导出的网络请求函数(需返回promise)
|
||||
exportAction?: (queryParams: TQuery) => Promise<any>;
|
||||
// 前端全量导出的网络请求函数(需返回promise)
|
||||
exportsAction?: (queryParams: TQuery) => Promise<TItem[]>;
|
||||
// 导入模板
|
||||
importTemplate?: string | (() => Promise<any>);
|
||||
// 后端导入的网络请求函数(需返回promise)
|
||||
importAction?: (file: File) => Promise<any>;
|
||||
// 前端导入的网络请求函数(需返回promise)
|
||||
importsAction?: (data: IObject[]) => Promise<any>;
|
||||
// 主键名(默认为id)
|
||||
pk?: string;
|
||||
// 表格工具栏(默认:add,delete,export,也可自定义)
|
||||
toolbar?: Array<ToolbarLeft | IToolsButton>;
|
||||
// 表格工具栏右侧图标(默认:refresh,filter,imports,exports,search)
|
||||
defaultToolbar?: Array<ToolbarRight | IToolsButton>;
|
||||
// table组件列属性(额外的属性templet,operat,slotName)
|
||||
cols: Array<{
|
||||
type?: "default" | "selection" | "index" | "expand";
|
||||
label?: string;
|
||||
prop?: string;
|
||||
width?: string | number;
|
||||
align?: "left" | "center" | "right";
|
||||
columnKey?: string;
|
||||
reserveSelection?: boolean;
|
||||
// 列是否显示
|
||||
show?: boolean;
|
||||
// 模板
|
||||
templet?:
|
||||
| "image"
|
||||
| "list"
|
||||
| "url"
|
||||
| "switch"
|
||||
| "input"
|
||||
| "price"
|
||||
| "percent"
|
||||
| "icon"
|
||||
| "date"
|
||||
| "tool"
|
||||
| "custom";
|
||||
// image模板相关参数
|
||||
imageWidth?: number;
|
||||
imageHeight?: number;
|
||||
// list模板相关参数
|
||||
selectList?: IObject;
|
||||
// switch模板相关参数
|
||||
activeValue?: boolean | string | number;
|
||||
inactiveValue?: boolean | string | number;
|
||||
activeText?: string;
|
||||
inactiveText?: string;
|
||||
// input模板相关参数
|
||||
inputType?: string;
|
||||
// price模板相关参数
|
||||
priceFormat?: string;
|
||||
// date模板相关参数
|
||||
dateFormat?: string;
|
||||
// tool模板相关参数
|
||||
operat?: Array<ToolbarTable | IToolsButton>;
|
||||
// filter值拼接符
|
||||
filterJoin?: string;
|
||||
[key: string]: any;
|
||||
// 初始化数据函数
|
||||
initFn?: (item: IObject) => void;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface IModalConfig<T = any> {
|
||||
// 权限前缀(如sys:user,用于组成权限标识),不提供则不进行权限校验
|
||||
permPrefix?: string;
|
||||
// 标签冒号(默认:false)
|
||||
colon?: boolean;
|
||||
// 主键名(主要用于编辑数据,默认为id)
|
||||
pk?: string;
|
||||
// 组件类型(默认:dialog)
|
||||
component?: "dialog" | "drawer";
|
||||
// dialog组件属性
|
||||
dialog?: Partial<Omit<DialogProps, "modelValue">>;
|
||||
// drawer组件属性
|
||||
drawer?: Partial<Omit<DrawerProps, "modelValue">>;
|
||||
// form组件属性
|
||||
form?: IForm;
|
||||
// 表单项
|
||||
formItems: IFormItems<IComponentType>;
|
||||
// 提交之前处理
|
||||
beforeSubmit?: (data: T) => void;
|
||||
// 提交的网络请求函数(需返回promise)
|
||||
formAction?: (data: T) => Promise<any>;
|
||||
}
|
||||
|
||||
export type IForm = Partial<Omit<FormProps, "model" | "rules">>;
|
||||
|
||||
// 表单项
|
||||
export type IFormItems<T = IComponentType> = Array<{
|
||||
// 组件类型(如input,select,radio,custom等)
|
||||
type: T;
|
||||
// 标签提示
|
||||
tips?: string | IObject;
|
||||
// 标签文本
|
||||
label: string;
|
||||
// 键名
|
||||
prop: string;
|
||||
// 组件属性
|
||||
attrs?: IObject;
|
||||
// 组件可选项(只适用于select,radio,checkbox组件)
|
||||
options?: Array<{ label: string; value: any; [key: string]: any }> | Ref<any[]>;
|
||||
// 验证规则
|
||||
rules?: FormItemRule[];
|
||||
// 初始值
|
||||
initialValue?: any;
|
||||
// 插槽名(适用于自定义组件,设置类型为custom)
|
||||
slotName?: string;
|
||||
// 是否隐藏
|
||||
hidden?: boolean;
|
||||
// layout组件Col属性
|
||||
col?: Partial<ColProps>;
|
||||
// 组件事件
|
||||
events?: Record<string, (...args: any) => void>;
|
||||
// 初始化数据函数扩展
|
||||
initFn?: (item: IObject) => void;
|
||||
}>;
|
||||
|
||||
export interface IPageForm {
|
||||
// 主键名(主要用于编辑数据,默认为id)
|
||||
pk?: string;
|
||||
// form组件属性
|
||||
form?: IForm;
|
||||
// 表单项
|
||||
formItems: IFormItems<IComponentType>;
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
import { ref } from "vue";
|
||||
import type { IObject, PageContentInstance, PageModalInstance, PageSearchInstance } from "./types";
|
||||
|
||||
function usePage() {
|
||||
const searchRef = ref<PageSearchInstance>();
|
||||
const contentRef = ref<PageContentInstance>();
|
||||
const addModalRef = ref<PageModalInstance>();
|
||||
const editModalRef = ref<PageModalInstance>();
|
||||
|
||||
// 搜索
|
||||
function handleQueryClick(queryParams: IObject) {
|
||||
const filterParams = contentRef.value?.getFilterParams();
|
||||
contentRef.value?.fetchPageData({ ...queryParams, ...filterParams }, true);
|
||||
}
|
||||
// 重置
|
||||
function handleResetClick(queryParams: IObject) {
|
||||
const filterParams = contentRef.value?.getFilterParams();
|
||||
contentRef.value?.fetchPageData({ ...queryParams, ...filterParams }, true);
|
||||
}
|
||||
// 新增
|
||||
function handleAddClick(RefImpl?: Ref<PageModalInstance>) {
|
||||
if (RefImpl) {
|
||||
RefImpl?.value.setModalVisible();
|
||||
RefImpl?.value.handleDisabled(false);
|
||||
} else {
|
||||
addModalRef.value?.setModalVisible();
|
||||
addModalRef.value?.handleDisabled(false);
|
||||
}
|
||||
}
|
||||
// 编辑
|
||||
async function handleEditClick(
|
||||
row: IObject,
|
||||
callback?: (result?: IObject) => IObject | Promise<IObject>,
|
||||
RefImpl?: Ref<PageModalInstance>,
|
||||
) {
|
||||
if (RefImpl) {
|
||||
RefImpl.value?.setModalVisible();
|
||||
RefImpl.value?.handleDisabled(false);
|
||||
const from = await (callback?.(row) ?? Promise.resolve(row));
|
||||
RefImpl.value?.setFormData(from ? from : row);
|
||||
} else {
|
||||
editModalRef.value?.setModalVisible();
|
||||
editModalRef.value?.handleDisabled(false);
|
||||
const from = await (callback?.(row) ?? Promise.resolve(row));
|
||||
editModalRef.value?.setFormData(from ? from : row);
|
||||
}
|
||||
}
|
||||
// 查看
|
||||
async function handleViewClick(
|
||||
row: IObject,
|
||||
callback?: (result?: IObject) => IObject | Promise<IObject>,
|
||||
RefImpl?: Ref<PageModalInstance>,
|
||||
) {
|
||||
if (RefImpl) {
|
||||
RefImpl.value?.setModalVisible();
|
||||
RefImpl.value?.handleDisabled(true);
|
||||
const from = await (callback?.(row) ?? Promise.resolve(row));
|
||||
RefImpl.value?.setFormData(from ? from : row);
|
||||
} else {
|
||||
editModalRef.value?.setModalVisible();
|
||||
editModalRef.value?.handleDisabled(true);
|
||||
const from = await (callback?.(row) ?? Promise.resolve(row));
|
||||
editModalRef.value?.setFormData(from ? from : row);
|
||||
}
|
||||
}
|
||||
// 表单提交
|
||||
function handleSubmitClick() {
|
||||
//根据检索条件刷新列表数据
|
||||
const queryParams = searchRef.value?.getQueryParams();
|
||||
contentRef.value?.fetchPageData(queryParams, true);
|
||||
}
|
||||
// 导出
|
||||
function handleExportClick() {
|
||||
// 根据检索条件导出数据
|
||||
const queryParams = searchRef.value?.getQueryParams();
|
||||
contentRef.value?.exportPageData(queryParams);
|
||||
}
|
||||
// 搜索显隐
|
||||
function handleSearchClick() {
|
||||
searchRef.value?.toggleVisible();
|
||||
}
|
||||
// 筛选数据
|
||||
function handleFilterChange(filterParams: IObject) {
|
||||
const queryParams = searchRef.value?.getQueryParams();
|
||||
contentRef.value?.fetchPageData({ ...queryParams, ...filterParams }, true);
|
||||
}
|
||||
|
||||
return {
|
||||
searchRef,
|
||||
contentRef,
|
||||
addModalRef,
|
||||
editModalRef,
|
||||
handleQueryClick,
|
||||
handleResetClick,
|
||||
handleAddClick,
|
||||
handleEditClick,
|
||||
handleViewClick,
|
||||
handleSubmitClick,
|
||||
handleExportClick,
|
||||
handleSearchClick,
|
||||
handleFilterChange,
|
||||
};
|
||||
}
|
||||
|
||||
export default usePage;
|
||||
@@ -1,9 +1,9 @@
|
||||
import DeviceAPI from "@/api/device";
|
||||
import type { DeviceForm } from "@/api/device/types";
|
||||
import type { IModalConfig } from "../components/types";
|
||||
import type { CrudFormConfig } from "@/components/Crud/types";
|
||||
|
||||
const modalConfig: IModalConfig<DeviceForm> = {
|
||||
permPrefix: "sys:device",
|
||||
const addFormConfig: CrudFormConfig<DeviceForm> = {
|
||||
component: "dialog",
|
||||
dialog: {
|
||||
title: "新增设备",
|
||||
width: 800,
|
||||
@@ -12,13 +12,8 @@ const modalConfig: IModalConfig<DeviceForm> = {
|
||||
form: {
|
||||
labelWidth: 100,
|
||||
},
|
||||
formAction(data) {
|
||||
return DeviceAPI.create(data);
|
||||
},
|
||||
beforeSubmit(data) {
|
||||
return data;
|
||||
},
|
||||
formItems: [
|
||||
submitForm: (data) => DeviceAPI.create(data),
|
||||
fields: [
|
||||
{
|
||||
label: "设备SN",
|
||||
prop: "serialno",
|
||||
@@ -125,5 +120,4 @@ const modalConfig: IModalConfig<DeviceForm> = {
|
||||
],
|
||||
};
|
||||
|
||||
// 如果有异步数据会修改配置的,推荐用reactive包裹,而纯静态配置的可以直接导出
|
||||
export default reactive(modalConfig) as IModalConfig<DeviceForm>;
|
||||
export default addFormConfig;
|
||||
|
||||
@@ -1,9 +1,27 @@
|
||||
import DeviceAPI from "@/api/device";
|
||||
import type { DeviceQueryParams, DeviceItem } from "@/api/device/types";
|
||||
import type { IContentConfig } from "../components/types";
|
||||
import type { DeviceItem, DeviceQueryParams } from "@/api/device/types";
|
||||
import type { CrudTableConfig } from "@/components/Crud/types";
|
||||
import { onlineStatus, syncOnlineStatus } from "./online";
|
||||
|
||||
const contentConfig: IContentConfig<DeviceQueryParams, DeviceItem> = {
|
||||
permPrefix: "sys:device", // 不写不进行按钮权限校验
|
||||
/** 列表接口不保证带在线状态,加载后批量补齐 */
|
||||
async function fetchDevices(params: DeviceQueryParams) {
|
||||
const page = await DeviceAPI.getPage(params);
|
||||
syncOnlineStatus(page.list);
|
||||
const sns = page.list.map((item) => item.serialno).filter((sn): sn is string => !!sn);
|
||||
if (sns.length) {
|
||||
await DeviceAPI.getOnlineStatusBatch(sns)
|
||||
.then((onlineMap) => {
|
||||
Object.values(onlineMap ?? {}).forEach((info) => {
|
||||
if (info?.serialno) onlineStatus[info.serialno] = info.online ?? 0;
|
||||
});
|
||||
})
|
||||
.catch(() => undefined);
|
||||
}
|
||||
return page;
|
||||
}
|
||||
|
||||
const contentConfig: CrudTableConfig<DeviceQueryParams, DeviceItem> = {
|
||||
permissionPrefix: "sys:device",
|
||||
table: {
|
||||
border: true,
|
||||
highlightCurrentRow: true,
|
||||
@@ -14,182 +32,90 @@ const contentConfig: IContentConfig<DeviceQueryParams, DeviceItem> = {
|
||||
pageSize: 20,
|
||||
pageSizes: [10, 20, 30, 50],
|
||||
},
|
||||
indexAction(params) {
|
||||
return DeviceAPI.getPage(params);
|
||||
},
|
||||
deleteAction: DeviceAPI.deleteByIds,
|
||||
importAction(file) {
|
||||
return DeviceAPI.import(file);
|
||||
},
|
||||
exportAction: DeviceAPI.export,
|
||||
fetchData: fetchDevices,
|
||||
deleteByIds: DeviceAPI.deleteByIds,
|
||||
importFile: DeviceAPI.import,
|
||||
importTemplate: DeviceAPI.downloadTemplate,
|
||||
importsAction(data) {
|
||||
// 模拟导入数据
|
||||
console.warn("importsAction", data);
|
||||
return Promise.resolve();
|
||||
},
|
||||
async exportsAction(params) {
|
||||
// 模拟获取到的是全量数据
|
||||
const data = await DeviceAPI.getPage(params);
|
||||
console.warn("exportsAction", data.list);
|
||||
return data.list;
|
||||
},
|
||||
pk: "id",
|
||||
toolbar: [
|
||||
"add",
|
||||
"delete",
|
||||
"import",
|
||||
"export",
|
||||
{
|
||||
name: "custom1",
|
||||
text: "自定义1",
|
||||
perm: "add",
|
||||
attrs: { icon: "plus", color: "#626AEF" },
|
||||
},
|
||||
],
|
||||
defaultToolbar: ["refresh", "filter", "imports", "exports", "search"],
|
||||
cols: [
|
||||
exportFile: (params) => DeviceAPI.export(params as DeviceQueryParams),
|
||||
rowKey: "id",
|
||||
toolbarActions: ["create", "delete", "import", "export"],
|
||||
tableTools: ["refresh", "columns", "search"],
|
||||
columns: [
|
||||
{ type: "selection", width: 50, align: "center" },
|
||||
{ label: "编号", align: "center", prop: "id", width: 100, show: false },
|
||||
{
|
||||
label: "SN",
|
||||
align: "center",
|
||||
prop: "serialno",
|
||||
templet: "custom",
|
||||
slotName: "serialno",
|
||||
width: 220,
|
||||
},
|
||||
{
|
||||
label: "在线状态",
|
||||
align: "center",
|
||||
prop: "online",
|
||||
templet: "custom",
|
||||
slotName: "onlineStatus",
|
||||
width: 100,
|
||||
},
|
||||
{ label: "SN", align: "center", prop: "serialno", width: 220 },
|
||||
{ label: "在线状态", align: "center", prop: "online", width: 100 },
|
||||
{ label: "设备昵称", align: "center", prop: "snName", width: 200 },
|
||||
{ label: "设备型号", align: "center", prop: "snModel", width: 200 },
|
||||
{ label: "推送ID", align: "center", prop: "pushId", width: 200 },
|
||||
// { label: "设备 IMEI", align: "center", prop: "snImei", width: 200 },
|
||||
// { label: "设备版本号", align: "center", prop: "snDispalyId", width: 400 },
|
||||
// { label: "头像", align: "center", prop: "avatar", templet: "image" },
|
||||
// {
|
||||
// label: "性别",
|
||||
// align: "center",
|
||||
// prop: "gender",
|
||||
// width: 100,
|
||||
// templet: "custom",
|
||||
// slotName: "gender",
|
||||
// },
|
||||
// { label: "部门", align: "center", prop: "deptName", width: 120 },
|
||||
// {
|
||||
// label: "角色",
|
||||
// align: "center",
|
||||
// prop: "roleNames",
|
||||
// width: 120,
|
||||
// columnKey: "roleIds",
|
||||
// filters: [],
|
||||
// filterMultiple: true,
|
||||
// filterJoin: ",",
|
||||
// async initFn(colItem) {
|
||||
// const roleOptions = await RoleAPI.getOptions();
|
||||
// colItem.filters = roleOptions.map((item) => {
|
||||
// return { text: item.label, value: item.value };
|
||||
// });
|
||||
// },
|
||||
// },
|
||||
{
|
||||
label: "手机号码",
|
||||
align: "center",
|
||||
prop: "snMobile",
|
||||
templet: "custom",
|
||||
slotName: "mobile",
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
label: "状态",
|
||||
align: "center",
|
||||
prop: "status",
|
||||
templet: "custom",
|
||||
slotName: "status",
|
||||
width: 120,
|
||||
},
|
||||
{ label: "手机号码", align: "center", prop: "snMobile", width: 150 },
|
||||
{ label: "状态", align: "center", prop: "status", width: 120 },
|
||||
{ label: "创建时间", align: "center", prop: "createTime", width: 180 },
|
||||
{ label: "激活时间", align: "center", prop: "activateTime", width: 180 },
|
||||
{
|
||||
label: "操作",
|
||||
align: "center",
|
||||
prop: "actions",
|
||||
fixed: "right",
|
||||
width: 280,
|
||||
templet: "tool",
|
||||
operat: [
|
||||
cellType: "actions",
|
||||
maxVisibleActions: 3,
|
||||
actions: [
|
||||
{
|
||||
name: "detail",
|
||||
text: "详情",
|
||||
attrs: { icon: "Document", type: "primary" },
|
||||
key: "detail",
|
||||
label: "详情",
|
||||
permission: "*:*:*",
|
||||
buttonProps: { type: "primary", icon: "Document" },
|
||||
},
|
||||
// {
|
||||
// name: "reset_pwd",
|
||||
// text: "重置密码",
|
||||
// // perm: "password-reset",
|
||||
// attrs: {
|
||||
// icon: "refresh-left",
|
||||
// // color: "#626AEF", // 使用 text 属性,颜色不生效
|
||||
// style: {
|
||||
// "--el-button-text-color": "#626AEF",
|
||||
// "--el-button-hover-link-text-color": "#9197f4",
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
"edit",
|
||||
"delete",
|
||||
{
|
||||
name: "more", // 主按钮名称
|
||||
text: "更多", // 显示文本
|
||||
attrs: { icon: "more", type: "primary" }, // 图标
|
||||
// children 数组定义下拉菜单项
|
||||
children: [
|
||||
{
|
||||
name: "refresh",
|
||||
text: "刷新",
|
||||
attrs: { icon: "Refresh" },
|
||||
},
|
||||
{
|
||||
name: "screenshot",
|
||||
text: "截图",
|
||||
attrs: { icon: "FullScreen" },
|
||||
},
|
||||
{
|
||||
name: "mirror",
|
||||
text: "屏幕镜像",
|
||||
attrs: { icon: "VideoCamera" },
|
||||
},
|
||||
{
|
||||
name: "locate",
|
||||
text: "定位",
|
||||
attrs: { icon: "Position" },
|
||||
},
|
||||
{
|
||||
name: "shutdown",
|
||||
text: "关机",
|
||||
attrs: { icon: "SwitchButton" },
|
||||
},
|
||||
{
|
||||
name: "reboot",
|
||||
text: "重启",
|
||||
attrs: { icon: "SwitchButton" },
|
||||
},
|
||||
{
|
||||
name: "refactory",
|
||||
text: "重置",
|
||||
attrs: { icon: "RefreshLeft" },
|
||||
},
|
||||
{
|
||||
name: "unbind",
|
||||
text: "解除绑定",
|
||||
attrs: { icon: "Unlock" },
|
||||
},
|
||||
],
|
||||
key: "refresh",
|
||||
label: "刷新",
|
||||
permission: "*:*:*",
|
||||
buttonProps: { icon: "Refresh" },
|
||||
},
|
||||
{
|
||||
key: "screenshot",
|
||||
label: "截图",
|
||||
permission: "*:*:*",
|
||||
buttonProps: { icon: "FullScreen" },
|
||||
},
|
||||
{
|
||||
key: "mirror",
|
||||
label: "屏幕镜像",
|
||||
permission: "*:*:*",
|
||||
buttonProps: { icon: "VideoCamera" },
|
||||
},
|
||||
{
|
||||
key: "locate",
|
||||
label: "定位",
|
||||
permission: "*:*:*",
|
||||
buttonProps: { icon: "Position" },
|
||||
},
|
||||
{
|
||||
key: "shutdown",
|
||||
label: "关机",
|
||||
permission: "*:*:*",
|
||||
buttonProps: { icon: "SwitchButton" },
|
||||
},
|
||||
{
|
||||
key: "reboot",
|
||||
label: "重启",
|
||||
permission: "*:*:*",
|
||||
buttonProps: { icon: "SwitchButton" },
|
||||
},
|
||||
{
|
||||
key: "refactory",
|
||||
label: "重置",
|
||||
permission: "*:*:*",
|
||||
buttonProps: { icon: "RefreshLeft" },
|
||||
},
|
||||
{
|
||||
key: "unbind",
|
||||
label: "解除绑定",
|
||||
permission: "*:*:*",
|
||||
buttonProps: { icon: "Unlock" },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
import { reactive } from "vue";
|
||||
import DeviceAPI from "@/api/device";
|
||||
import type { DeviceForm } from "@/api/device/types";
|
||||
import type { IModalConfig } from "../components/types";
|
||||
import type { CrudFormConfig } from "@/components/Crud/types";
|
||||
import { DeviceEnum } from "@/enums/settings";
|
||||
import { useAppStore } from "@/stores";
|
||||
|
||||
const modalConfig: IModalConfig<DeviceForm> = {
|
||||
permPrefix: "sys:device",
|
||||
const editFormConfig: CrudFormConfig<DeviceForm> = {
|
||||
component: "drawer",
|
||||
rowKey: "id",
|
||||
drawer: {
|
||||
title: "修改设备",
|
||||
size: useAppStore().device === DeviceEnum.MOBILE ? "80%" : 500,
|
||||
},
|
||||
pk: "id",
|
||||
// 编辑只回传可修改字段(仅设备昵称)
|
||||
beforeSubmit(data) {
|
||||
return {
|
||||
id: data.id,
|
||||
snName: data.snName,
|
||||
status: data.status,
|
||||
} as DeviceForm;
|
||||
form: {
|
||||
labelWidth: 100,
|
||||
},
|
||||
formAction(data) {
|
||||
return DeviceAPI.update(data.id as string, data);
|
||||
},
|
||||
formItems: [
|
||||
// 编辑只回传可修改字段(仅设备昵称与状态)
|
||||
submitForm: (data) =>
|
||||
DeviceAPI.update(
|
||||
data.id as string,
|
||||
{
|
||||
id: data.id,
|
||||
snName: data.snName,
|
||||
status: data.status,
|
||||
} as DeviceForm,
|
||||
),
|
||||
fields: [
|
||||
{
|
||||
label: "设备SN",
|
||||
prop: "serialno",
|
||||
@@ -60,4 +60,4 @@ const modalConfig: IModalConfig<DeviceForm> = {
|
||||
],
|
||||
};
|
||||
|
||||
export default reactive(modalConfig) as IModalConfig<DeviceForm>;
|
||||
export default editFormConfig;
|
||||
|
||||
14
src/views/devices/sn/config/online.ts
Normal file
14
src/views/devices/sn/config/online.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { DeviceItem, DeviceOnlineVO } from "@/api/device/types";
|
||||
|
||||
/** 设备在线状态:分页接口提供当前页初值,SSE 推送实时覆盖 */
|
||||
export const onlineStatus = reactive<Record<string, number>>({});
|
||||
|
||||
export function syncOnlineStatus(list: DeviceItem[]) {
|
||||
list.forEach((item) => {
|
||||
if (item.serialno) onlineStatus[item.serialno] = item.online ?? 0;
|
||||
});
|
||||
}
|
||||
|
||||
export function updateOnlineStatus(data: DeviceOnlineVO) {
|
||||
if (data?.serialno) onlineStatus[data.serialno] = data.online ?? 0;
|
||||
}
|
||||
@@ -1,27 +1,14 @@
|
||||
/** 公共下载数据,减少重复请求次数 */
|
||||
import DeptAPI from "@/api/system/dept";
|
||||
import RoleAPI from "@/api/system/role";
|
||||
import type { OptionItem } from "@/api/common";
|
||||
|
||||
interface OptionItem {
|
||||
label: string;
|
||||
value: any;
|
||||
[key: string]: any; // 允许其他属性
|
||||
}
|
||||
|
||||
// 明确指定类型为 OptionItem[]
|
||||
/** 公共下载数据,减少重复请求次数 */
|
||||
export const deptArr = ref<OptionItem[]>([]);
|
||||
export const roleArr = ref<OptionItem[]>([]);
|
||||
export const stateArr = ref<OptionItem[]>([
|
||||
|
||||
export const stateArr = [
|
||||
{ label: "启用", value: 1 },
|
||||
{ label: "禁用", value: 0 },
|
||||
]);
|
||||
];
|
||||
|
||||
// 初始化逻辑,在 onMounted 钩子中调用
|
||||
export const initOptions = async () => {
|
||||
// 使用Promise.all并行请求
|
||||
const [dept, roles] = await Promise.all([DeptAPI.getOptions(), RoleAPI.getOptions()]);
|
||||
// 获取部门选项并赋值
|
||||
deptArr.value = dept;
|
||||
// 获取角色选项并赋值
|
||||
roleArr.value = roles;
|
||||
};
|
||||
export async function initOptions() {
|
||||
deptArr.value = await DeptAPI.getOptions();
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import type { ISearchConfig } from "../components/types";
|
||||
import type { CrudSearchConfig } from "@/components/Crud/types";
|
||||
import { deptArr, stateArr } from "./options";
|
||||
|
||||
const searchConfig: ISearchConfig = {
|
||||
permPrefix: "sys:device",
|
||||
formItems: [
|
||||
const searchConfig: CrudSearchConfig = {
|
||||
fields: [
|
||||
{
|
||||
tips: "支持模糊搜索",
|
||||
type: "input",
|
||||
label: "关键字",
|
||||
prop: "keywords",
|
||||
tips: "支持模糊搜索",
|
||||
attrs: {
|
||||
placeholder: "用户名/昵称/手机号",
|
||||
clearable: true,
|
||||
@@ -23,37 +22,35 @@ const searchConfig: ISearchConfig = {
|
||||
placeholder: "请选择",
|
||||
data: deptArr,
|
||||
filterable: true,
|
||||
"check-strictly": true,
|
||||
"render-after-expand": false,
|
||||
checkStrictly: true,
|
||||
renderAfterExpand: false,
|
||||
clearable: true,
|
||||
style: { width: "200px" },
|
||||
},
|
||||
// async initFn(formItem) {
|
||||
// // 注意:如果initFn函数不是箭头函数,this会指向此配置项对象,那么也就可以用this来替代形参formItem
|
||||
// formItem.attrs.data = await DeptAPI.getOptions();
|
||||
// },
|
||||
},
|
||||
{
|
||||
type: "select",
|
||||
label: "状态",
|
||||
prop: "status",
|
||||
initialValue: null,
|
||||
options: stateArr,
|
||||
attrs: {
|
||||
placeholder: "全部",
|
||||
clearable: true,
|
||||
style: { width: "200px" },
|
||||
},
|
||||
options: stateArr,
|
||||
},
|
||||
{
|
||||
type: "date-picker",
|
||||
label: "创建时间",
|
||||
prop: "createTime",
|
||||
initialValue: [],
|
||||
attrs: {
|
||||
type: "daterange",
|
||||
"range-separator": "~",
|
||||
"start-placeholder": "开始时间",
|
||||
"end-placeholder": "截止时间",
|
||||
"value-format": "YYYY-MM-DD",
|
||||
rangeSeparator: "~",
|
||||
startPlaceholder: "开始时间",
|
||||
endPlaceholder: "截止时间",
|
||||
valueFormat: "YYYY-MM-DD",
|
||||
style: { width: "200px" },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,96 +1,56 @@
|
||||
<template>
|
||||
<div class="app-container h-full flex flex-1 flex-col">
|
||||
<!-- 列表 -->
|
||||
<div v-show="!dialogVisible" class="flex flex-col h-full">
|
||||
<!-- 搜索 -->
|
||||
<DevicePageSearch
|
||||
ref="searchRef"
|
||||
<div v-show="!detailVisible" class="flex flex-col h-full">
|
||||
<CrudPage
|
||||
ref="pageRef"
|
||||
:search-config="searchConfig"
|
||||
@query-click="handleQueryClick"
|
||||
@reset-click="handleResetClick"
|
||||
/>
|
||||
|
||||
<!-- 列表 -->
|
||||
<DevicePageContent
|
||||
ref="contentRef"
|
||||
:content-config="contentConfig"
|
||||
class="flex-1 overflow-hidden"
|
||||
@add-click="handleAddClick"
|
||||
@export-click="handleExportClick"
|
||||
@search-click="handleSearchClick"
|
||||
@toolbar-click="handleToolbarClick"
|
||||
@operate-click="handleOperateClick"
|
||||
@filter-change="handleFilterChange"
|
||||
@data-loaded="fetchDeviceOnlineStatus"
|
||||
:table-config="contentConfig"
|
||||
@create="handleAddClick"
|
||||
@row-action="handleRowAction"
|
||||
>
|
||||
<template #status="scope">
|
||||
<el-tag :type="scope.row[scope.prop as string] == 1 ? 'success' : 'info'">
|
||||
{{ scope.row[scope.prop as string] == 1 ? "启用" : "禁用" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
<template #gender="scope">
|
||||
<DictTag v-model="scope.row[scope.prop as string]" code="gender" />
|
||||
</template>
|
||||
<template #mobile="scope">
|
||||
<el-text>{{ scope.row[scope.prop as string] }}</el-text>
|
||||
<copy-button
|
||||
v-if="scope.row[scope.prop as string]"
|
||||
:text="scope.row[scope.prop as string]"
|
||||
:style="{ marginLeft: '2px' }"
|
||||
/>
|
||||
</template>
|
||||
<template #serialno="scope">
|
||||
<el-link type="primary" @click="handleSerialnoClick(scope.row)">
|
||||
{{ scope.row[scope.prop as string] }}
|
||||
<template #table-cell-serialno="{ row }">
|
||||
<el-link type="primary" @click="handleSerialnoClick(row)">
|
||||
{{ row.serialno }}
|
||||
</el-link>
|
||||
<copy-button
|
||||
v-if="scope.row[scope.prop as string]"
|
||||
:text="scope.row[scope.prop as string]"
|
||||
<CopyButton
|
||||
v-if="row.serialno"
|
||||
:text="String(row.serialno)"
|
||||
:style="{ marginLeft: '2px' }"
|
||||
/>
|
||||
</template>
|
||||
<template #onlineStatus="scope">
|
||||
<template #table-cell-online="{ row }">
|
||||
<span
|
||||
class="status-dot"
|
||||
:style="{ backgroundColor: scope.row.online == 1 ? '#67c23a' : '#f56c6c' }"
|
||||
:style="{ backgroundColor: isOnline(row) ? '#67c23a' : '#f56c6c' }"
|
||||
/>
|
||||
<el-tag :type="scope.row.online == 1 ? 'success' : 'info'" size="small">
|
||||
{{ scope.row.online == 1 ? "在线" : "离线" }}
|
||||
<el-tag :type="isOnline(row) ? 'success' : 'info'" size="small">
|
||||
{{ isOnline(row) ? "在线" : "离线" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</DevicePageContent>
|
||||
|
||||
<!-- 新增 -->
|
||||
<DevicePageModal
|
||||
ref="addModalRef"
|
||||
:modal-config="addModalConfig"
|
||||
@submit-click="handleSubmitClick"
|
||||
>
|
||||
<template #gender="scope">
|
||||
<DictSelect v-model="scope.formData[scope.prop]" code="gender" v-bind="scope.attrs" />
|
||||
<template #table-cell-snMobile="{ row }">
|
||||
<el-text>{{ row.snMobile }}</el-text>
|
||||
<CopyButton
|
||||
v-if="row.snMobile"
|
||||
:text="String(row.snMobile)"
|
||||
:style="{ marginLeft: '2px' }"
|
||||
/>
|
||||
</template>
|
||||
<template #openModal>
|
||||
<el-button type="primary" @click="openSecondModal">打开二级弹窗</el-button>
|
||||
<template #table-cell-status="{ row }">
|
||||
<el-tag :type="row.status == 1 ? 'success' : 'info'">
|
||||
{{ row.status == 1 ? "启用" : "禁用" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</DevicePageModal>
|
||||
<!-- 编辑 -->
|
||||
<DevicePageModal
|
||||
ref="editModalRef"
|
||||
:modal-config="editModalConfig"
|
||||
@submit-click="handleSubmitClick"
|
||||
>
|
||||
<template #gender="scope">
|
||||
<DictSelect v-model="scope.formData[scope.prop]" code="gender" v-bind="scope.attrs" />
|
||||
</template>
|
||||
</DevicePageModal>
|
||||
</CrudPage>
|
||||
</div>
|
||||
<div v-show="dialogVisible">
|
||||
|
||||
<div v-show="detailVisible">
|
||||
<DeviceSnDetail
|
||||
v-model="dialogVisible"
|
||||
v-model="detailVisible"
|
||||
:serialno="selectedRow?.serialno"
|
||||
@return-click="handleReturnClick"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 操作列详情 - 概览抽屉,样式与编辑抽屉一致 -->
|
||||
<el-drawer v-model="overviewVisible" title="查看" size="500px" destroy-on-close>
|
||||
<DeviceSnOverView :serialno="selectedRow?.serialno" />
|
||||
@@ -98,138 +58,121 @@
|
||||
<el-button @click="overviewVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
|
||||
<CrudForm ref="addFormRef" :form-config="addFormConfig" @saved="handleSaved" />
|
||||
<CrudForm ref="editFormRef" :form-config="editFormConfig" @saved="handleSaved" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, ref } from "vue";
|
||||
import { onBeforeUnmount, onMounted, ref } from "vue";
|
||||
import DeviceAPI from "@/api/device";
|
||||
import type { DeviceOnlineVO } from "@/api/device/types";
|
||||
import type { IObject, PageModalInstance } from "./components/types";
|
||||
import type { CrudRecord } from "@/components/Crud/types";
|
||||
import { useSse } from "@/utils/sse";
|
||||
import DevicePageContent from "./components/DevicePageContent.vue";
|
||||
import DevicePageModal from "./components/DevicePageModal.vue";
|
||||
import DevicePageSearch from "./components/DevicePageSearch.vue";
|
||||
import DeviceSnDetail from "./components/DeviceSnDetail.vue";
|
||||
import DeviceSnOverView from "./components/DeviceSnOverView.vue";
|
||||
import usePage from "./components/usePage";
|
||||
import addModalConfig from "./config/add";
|
||||
import addFormConfig from "./config/add";
|
||||
import contentConfig from "./config/content";
|
||||
import editModalConfig from "./config/edit";
|
||||
import searchConfig from "./config/search";
|
||||
import editFormConfig from "./config/edit";
|
||||
import { onlineStatus, updateOnlineStatus } from "./config/online";
|
||||
import { initOptions } from "./config/options";
|
||||
import searchConfig from "./config/search";
|
||||
|
||||
const {
|
||||
searchRef,
|
||||
contentRef,
|
||||
addModalRef,
|
||||
editModalRef,
|
||||
handleQueryClick,
|
||||
handleResetClick,
|
||||
handleAddClick,
|
||||
handleEditClick,
|
||||
handleSubmitClick,
|
||||
handleExportClick,
|
||||
handleSearchClick,
|
||||
handleFilterChange,
|
||||
} = usePage();
|
||||
const pageRef = ref();
|
||||
const addFormRef = ref();
|
||||
const editFormRef = ref();
|
||||
|
||||
// 其他工具
|
||||
function handleToolbarClick(name: string) {
|
||||
console.warn(name);
|
||||
if (name === "custom1") {
|
||||
ElMessage.success("点击了自定义1按钮");
|
||||
}
|
||||
const detailVisible = ref(false);
|
||||
const overviewVisible = ref(false);
|
||||
const selectedRow = ref<CrudRecord>({});
|
||||
|
||||
/** SSE 推送的在线状态优先于列表接口返回值 */
|
||||
function isOnline(row: CrudRecord): boolean {
|
||||
return Number(onlineStatus[String(row.serialno)] ?? row.online) === 1;
|
||||
}
|
||||
|
||||
function handleAddClick() {
|
||||
pageRef.value?.openCreate(addFormRef);
|
||||
}
|
||||
|
||||
function handleSaved() {
|
||||
pageRef.value?.refresh();
|
||||
}
|
||||
|
||||
function handleReturnClick() {
|
||||
dialogVisible.value = false;
|
||||
detailVisible.value = false;
|
||||
overviewVisible.value = false;
|
||||
}
|
||||
|
||||
// SN点击事件 - 打开详情弹窗
|
||||
function handleSerialnoClick(row: CrudRecord) {
|
||||
selectedRow.value = row;
|
||||
detailVisible.value = true;
|
||||
}
|
||||
|
||||
// 表格工具
|
||||
const handleOperateClick = (data: IObject) => {
|
||||
if (data.name === "detail") {
|
||||
selectedRow.value = data.row;
|
||||
async function handleRowAction({ key, row }: { key: string; row: CrudRecord }) {
|
||||
if (key === "detail") {
|
||||
selectedRow.value = row;
|
||||
overviewVisible.value = true;
|
||||
} else if (data.name === "edit") {
|
||||
editModalConfig.drawer = { ...editModalConfig.drawer, title: "修改" };
|
||||
handleEditClick(data.row, async () => {
|
||||
return await DeviceAPI.getFormData(data.row.serialno as string); // 根据SN获取详情
|
||||
});
|
||||
} else if (data.name === "reset_pwd") {
|
||||
ElMessageBox.prompt("请输入用户名" + data.row.username + "」的新密码", "重置密码", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
}).then(
|
||||
(result) => {
|
||||
const value = result.value;
|
||||
if (!value || value.length < 6) {
|
||||
ElMessage.warning("密码至少需6位字符,请重新输入");
|
||||
return false;
|
||||
}
|
||||
DeviceAPI.resetPassword(data.row.id, value).then(() => {
|
||||
ElMessage.success("密码重置成功,新密码是:" + value);
|
||||
});
|
||||
},
|
||||
() => {
|
||||
// 用户取消
|
||||
},
|
||||
} else if (key === "edit") {
|
||||
await pageRef.value?.openEdit(
|
||||
row,
|
||||
(record: CrudRecord) => DeviceAPI.getFormData(String(record.serialno)),
|
||||
editFormRef,
|
||||
);
|
||||
} else if (data.name === "more") {
|
||||
// 处理新增的自定义操作
|
||||
ElMessage.info(`执行了自定义操作,设备ID: ${data.row.id}`);
|
||||
} else if (data.name === "refresh") {
|
||||
DeviceAPI.deviceRefresh(data.row.serialno as string).then(() => {
|
||||
ElMessage.success(`刷新设备,设备SN: ${data.row.serialno as string}`);
|
||||
} else if (key === "refresh") {
|
||||
DeviceAPI.deviceRefresh(String(row.serialno)).then(() => {
|
||||
ElMessage.success(`刷新设备,设备SN: ${row.serialno}`);
|
||||
});
|
||||
} else if (data.name === "screenshot") {
|
||||
DeviceAPI.deviceScreenshot(data.row.serialno as string).then(() => {
|
||||
ElMessage.success(`截图设备,设备SN: ${data.row.serialno as string}`);
|
||||
} else if (key === "screenshot") {
|
||||
DeviceAPI.deviceScreenshot(String(row.serialno)).then(() => {
|
||||
ElMessage.success(`截图设备,设备SN: ${row.serialno}`);
|
||||
});
|
||||
} else if (data.name === "mirror") {
|
||||
} else if (key === "mirror") {
|
||||
// 屏幕镜像:通知设备端启动屏幕采集,由家属端 client 进行 WebRTC 实时远程观看/控制
|
||||
DeviceAPI.deviceMirror(data.row.serialno as string)
|
||||
DeviceAPI.deviceMirror(String(row.serialno))
|
||||
.then(() => {
|
||||
ElMessage.success(`已发送屏幕镜像指令,设备SN: ${data.row.serialno as string}`);
|
||||
ElMessage.success(`已发送屏幕镜像指令,设备SN: ${row.serialno}`);
|
||||
})
|
||||
.catch(() => {});
|
||||
} else if (data.name === "reboot") {
|
||||
} else if (key === "reboot") {
|
||||
ElMessageBox.confirm("确定要重启该设备吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
})
|
||||
.then(() => {
|
||||
DeviceAPI.deviceReboot(data.row.serialno as string).then(() => {
|
||||
ElMessage.success(`重启设备,设备SN: ${data.row.serialno as string}`);
|
||||
DeviceAPI.deviceReboot(String(row.serialno)).then(() => {
|
||||
ElMessage.success(`重启设备,设备SN: ${row.serialno}`);
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
} else if (data.name === "shutdown") {
|
||||
DeviceAPI.deviceShutdown(data.row.serialno as string).then(() => {
|
||||
ElMessage.success(`关机设备,设备SN: ${data.row.serialno as string}`);
|
||||
} else if (key === "shutdown") {
|
||||
DeviceAPI.deviceShutdown(String(row.serialno)).then(() => {
|
||||
ElMessage.success(`关机设备,设备SN: ${row.serialno}`);
|
||||
});
|
||||
} else if (data.name === "locate") {
|
||||
DeviceAPI.deviceLocate(data.row.serialno as string).then(() => {
|
||||
ElMessage.success(`定位设备,设备SN: ${data.row.serialno as string}`);
|
||||
} else if (key === "locate") {
|
||||
DeviceAPI.deviceLocate(String(row.serialno)).then(() => {
|
||||
ElMessage.success(`定位设备,设备SN: ${row.serialno}`);
|
||||
});
|
||||
} else if (data.name === "refactory") {
|
||||
} else if (key === "refactory") {
|
||||
ElMessageBox.confirm("确定要重置该设备吗?此操作将清除设备所有数据,是否继续?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "error",
|
||||
})
|
||||
.then(() => {
|
||||
DeviceAPI.deviceRestore(data.row.serialno as string).then(() => {
|
||||
ElMessage.success(`重置设备,设备SN: ${data.row.serialno as string}`);
|
||||
DeviceAPI.deviceRestore(String(row.serialno)).then(() => {
|
||||
ElMessage.success(`重置设备,设备SN: ${row.serialno}`);
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
} else if (data.name === "unbind") {
|
||||
} else if (key === "unbind") {
|
||||
// 解除设备手机号绑定(管理员端):确认后清除 snMobile 并推送通知设备端标记解绑
|
||||
const sn = data.row.serialno as string;
|
||||
const mobile = (data.row.snMobile as string) || "";
|
||||
const sn = String(row.serialno);
|
||||
const mobile = (row.snMobile as string) || "";
|
||||
ElMessageBox.confirm(
|
||||
mobile
|
||||
? `设备「${sn}」当前绑定手机号:${mobile}\n确定要解除该绑定吗?解除后需由家属端重新绑定。`
|
||||
@@ -242,62 +185,12 @@ const handleOperateClick = (data: IObject) => {
|
||||
},
|
||||
)
|
||||
.then(() => {
|
||||
DeviceAPI.unbindDeviceMobile(data.row.serialno as string).then(() => {
|
||||
ElMessage.success(`已解除设备绑定,设备SN: ${data.row.serialno as string}`);
|
||||
DeviceAPI.unbindDeviceMobile(sn).then(() => {
|
||||
ElMessage.success(`已解除设备绑定,设备SN: ${sn}`);
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
};
|
||||
|
||||
// 打开二级弹窗
|
||||
const addModalRef2 = ref();
|
||||
const openSecondModal = () => {
|
||||
handleAddClick(addModalRef2 as Ref<PageModalInstance>);
|
||||
};
|
||||
|
||||
// SN点击事件 - 打开详情弹窗
|
||||
const handleSerialnoClick = async (row: IObject) => {
|
||||
// 将当前行的数据传递给详情组件
|
||||
selectedRow.value = row;
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
const overviewVisible = ref(false);
|
||||
const selectedRow = ref<IObject>({});
|
||||
|
||||
/**
|
||||
* 批量查询设备在线状态并更新表格数据
|
||||
*/
|
||||
async function fetchDeviceOnlineStatus() {
|
||||
try {
|
||||
// 从 contentRef 获取当前表格 pageData
|
||||
const pageData = contentRef.value?.pageData as IObject[] | undefined;
|
||||
if (!pageData || pageData.length === 0) {
|
||||
return;
|
||||
}
|
||||
// 收集所有设备序列号
|
||||
const sns = pageData.map((item) => item.serialno as string).filter((sn) => !!sn);
|
||||
if (sns.length === 0) {
|
||||
return;
|
||||
}
|
||||
// 批量查询在线状态
|
||||
const onlineMap = await DeviceAPI.getOnlineStatusBatch(sns);
|
||||
if (onlineMap) {
|
||||
// 更新表格数据中的在线状态
|
||||
pageData.forEach((item) => {
|
||||
const sn = item.serialno as string;
|
||||
const onlineInfo = onlineMap[sn];
|
||||
if (onlineInfo) {
|
||||
item.online = onlineInfo.online;
|
||||
item.lastHeartbeatTime = onlineInfo.lastHeartbeatTime;
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("批量查询设备在线状态失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -305,31 +198,13 @@ async function fetchDeviceOnlineStatus() {
|
||||
*/
|
||||
let unsubscribeDeviceOnlineStatus: (() => void) | null = null;
|
||||
|
||||
/**
|
||||
* 根据 SSE 推送的设备在线状态更新单条表格数据
|
||||
*/
|
||||
function handleDeviceOnlineStatus(data: DeviceOnlineVO) {
|
||||
if (!data || !data.serialno) {
|
||||
return;
|
||||
}
|
||||
const pageData = contentRef.value?.pageData as IObject[] | undefined;
|
||||
if (!pageData) {
|
||||
return;
|
||||
}
|
||||
const row = pageData.find((item) => item.serialno === data.serialno);
|
||||
if (row) {
|
||||
row.online = data.online;
|
||||
row.lastHeartbeatTime = data.lastHeartbeatTime;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initOptions();
|
||||
// 监听 SSE 推送的设备在线状态,实时更新表格对应行
|
||||
const sse = useSse();
|
||||
unsubscribeDeviceOnlineStatus = sse.on<DeviceOnlineVO>(
|
||||
"device-online-status",
|
||||
handleDeviceOnlineStatus,
|
||||
updateOnlineStatus,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user