From d78d3be5e01146436d11118b0e6f79c96ad86d19 Mon Sep 17 00:00:00 2001 From: BBIT-Kai <2911862937@qq.com> Date: Mon, 29 Dec 2025 16:29:15 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E7=89=A7=E5=AE=89=E4=BA=91?= =?UTF-8?q?=E5=93=A8-=E5=89=8D=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vue2/apps/web-antd/src/api/iot/update.ts | 65 ++++ vue2/apps/web-antd/src/api/sentinel/record.ts | 81 +++++ .../web-antd/src/views/iot/device/data.ts | 304 ++++++++++++++++++ .../web-antd/src/views/iot/device/form.vue | 123 +++++++ .../web-antd/src/views/iot/device/index.vue | 249 ++++++++++++++ .../web-antd/src/views/iot/update/data.ts | 179 +++++++++++ .../web-antd/src/views/iot/update/form.vue | 115 +++++++ .../web-antd/src/views/iot/update/index.vue | 104 ++++++ .../sentinel/record/VehicleAlertOverlay.vue | 84 +++++ .../src/views/sentinel/record/data.ts | 237 ++++++++++++++ .../src/views/sentinel/record/form.vue | 126 ++++++++ .../src/views/sentinel/record/index.vue | 238 ++++++++++++++ 12 files changed, 1905 insertions(+) create mode 100644 vue2/apps/web-antd/src/api/iot/update.ts create mode 100644 vue2/apps/web-antd/src/api/sentinel/record.ts create mode 100644 vue2/apps/web-antd/src/views/iot/device/data.ts create mode 100644 vue2/apps/web-antd/src/views/iot/device/form.vue create mode 100644 vue2/apps/web-antd/src/views/iot/device/index.vue create mode 100644 vue2/apps/web-antd/src/views/iot/update/data.ts create mode 100644 vue2/apps/web-antd/src/views/iot/update/form.vue create mode 100644 vue2/apps/web-antd/src/views/iot/update/index.vue create mode 100644 vue2/apps/web-antd/src/views/sentinel/record/VehicleAlertOverlay.vue create mode 100644 vue2/apps/web-antd/src/views/sentinel/record/data.ts create mode 100644 vue2/apps/web-antd/src/views/sentinel/record/form.vue create mode 100644 vue2/apps/web-antd/src/views/sentinel/record/index.vue diff --git a/vue2/apps/web-antd/src/api/iot/update.ts b/vue2/apps/web-antd/src/api/iot/update.ts new file mode 100644 index 0000000..1d6bf41 --- /dev/null +++ b/vue2/apps/web-antd/src/api/iot/update.ts @@ -0,0 +1,65 @@ +import type { Recordable } from '@vben/types'; + +import { pyRequestClient } from '#/api/request'; + +export namespace SystemUpdateApi { + export interface SystemUpdate { + [key: string]: any; + id: string; + code: number; + dept_id?: string; + dept_name?: string; + remark?: string; + oss_url?: string; + size?: number; + created_at?: string; + } +} +/** + * 获取升级包列表 + */ +async function getUpdateList(params: Recordable) { + return pyRequestClient.get>( + '/iot/common/update/list', + { params }, + ); +} +/** + * 创建升级包 + * @param data 升级包数据 + */ +async function createUpdate( + data: Omit, +) { + delete data.oss_url + return pyRequestClient.post('/iot/common/update', data); +} +/** + * 删除升级包 + * @param id 升级包 ID + */ +async function deleteUpdate(id: string) { + return pyRequestClient.delete(`/iot/common/update/${id}`); +} + +/** + * 上传 + */ +export async function getUpdateUploadUrl() { + return pyRequestClient.get('/iot/common/update/getUploadUrl'); +} + +/** + * 上传 + */ +export async function getMaxCodeByDeptId(dept_id: string) { + return pyRequestClient.get('/iot/common/update/getMaxCodeByDeptId',{ + params: {dept_id} + }); +} + +export { + createUpdate, + deleteUpdate, + getUpdateList, +}; diff --git a/vue2/apps/web-antd/src/api/sentinel/record.ts b/vue2/apps/web-antd/src/api/sentinel/record.ts new file mode 100644 index 0000000..4469d6f --- /dev/null +++ b/vue2/apps/web-antd/src/api/sentinel/record.ts @@ -0,0 +1,81 @@ +import type { Recordable } from '@vben/types'; + +import { pyRequestClient } from '#/api/request'; + +export namespace SentinelApi { + export interface Record { + id: string; + name: string; + is_inspected: 0 | 1; + license_plate?: string; + vehicle_type?: string; + license_plate_image?: string; + vehicle_image?: string; + livestock_type?: string; + livestock_source?: string; + created_at?: string; + updated_at?: string; + remark?: string; + dept_id?: string; + dept_name?: string; + } +} + +/** + * 获取记录列表数据 + */ +async function getSentinelRecordList(params: Recordable) { + return pyRequestClient.get>( + '/iot/sentinel/record/list', + { params }, + ); +} + +/** + * 创建记录 + * @param data 记录数据 + */ +async function createSentinelRecord(data: Omit) { + return pyRequestClient.post('/iot/sentinel/record', data); +} + +/** + * 更新记录 + * + * @param id 记录 ID + * @param data 记录数据 + */ +async function updateSentinelRecord( + id: string, + data: Omit, +) { + return pyRequestClient.put(`/iot/sentinel/record/${id}`, data); +} +/** + * 更新记录(部分更新) + * + * @param id 记录 ID + * @param data 需要更新的字段(部分字段即可) + */ +async function updateSentinelRecordPatch( + id: string, + data: Partial>, +) { + return pyRequestClient.patch(`/iot/sentinel/record/${id}`, data); +} + +/** + * 删除记录 + * @param id 记录 ID + */ +async function deleteSentinelRecord(id: string) { + return pyRequestClient.delete(`/iot/sentinel/record/${id}`); +} + +export { + createSentinelRecord, + deleteSentinelRecord, + getSentinelRecordList, + updateSentinelRecord, + updateSentinelRecordPatch, +}; diff --git a/vue2/apps/web-antd/src/views/iot/device/data.ts b/vue2/apps/web-antd/src/views/iot/device/data.ts new file mode 100644 index 0000000..d30b810 --- /dev/null +++ b/vue2/apps/web-antd/src/views/iot/device/data.ts @@ -0,0 +1,304 @@ +import { type VbenFormSchema, z } from "#/adapter/form"; +import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table'; +import type { SystemUserApi } from '#/api'; + +import * as api from '#/api'; +import { $t } from "@vben/locales"; + +export function useFormSchema(): VbenFormSchema[] { + return [ + { + component: 'Input', + fieldName: 'name', + label: '设备账号', + rules: z + .string({ + required_error: $t('ui.formRules.required', ['设备账号']), + invalid_type_error: $t('ui.formRules.required', ['设备账号']), + }) + .min(4, $t('ui.formRules.minLength', ['设备账号', 4])) + .max(50, $t('ui.formRules.maxLength', ['设备账号', 50])), + componentProps: { + allowClear: true, + }, + }, + { + component: 'InputPassword', + fieldName: 'password', + label: '密码', + description: '123456', + }, + { + component: 'ApiTreeSelect', + componentProps: { + allowClear: true, + api: api.getDeptList, + class: 'w-full', + labelField: 'name', + valueField: 'id', + childrenField: 'children', + }, + fieldName: 'dept_id', + label: '所属组织', + rules: 'required', + }, + { + component: 'Textarea', + fieldName: 'remark', + label: '备注', + componentProps: { + allowClear: true, + }, + }, + { + component: 'RadioGroup', + componentProps: { + buttonStyle: 'solid', + options: [ + { label: '可用', value: 1 }, + { label: '不可用', value: 0 }, + ], + optionType: 'button', + }, + defaultValue: 1, + fieldName: 'status', + label: '可用性', + }, + { + component: 'RadioGroup', + componentProps: { + buttonStyle: 'solid', + options: [ + { label: '是', value: 1 }, + { label: '否', value: 0 }, + ], + optionType: 'button', + }, + defaultValue: 0, + fieldName: 'is_superuser', + label: '管理员', + }, + ]; +} + +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + component: 'Input', + fieldName: 'id', + label: '设备编号', + componentProps: { + allowClear: true, + }, + }, + { + component: 'Input', + fieldName: 'name', + label: '设备名称', + componentProps: { + allowClear: true, + }, + // componentProps: { + // readonly: true, // 如果组件支持 readonly + // }, + }, + { + component: 'Select', + componentProps: { + allowClear: true, + options: [ + { label: '启用中', value: 1 }, + { label: '已禁用', value: 0 }, + ], + }, + fieldName: 'status', + label: '可用性', + }, + { + component: 'Select', + componentProps: { + allowClear: true, + options: [ + { label: '是', value: 1 }, + { label: '否', value: 0 }, + ], + }, + fieldName: 'is_superuser', + label: '管理员', + }, + { + component: 'ApiTreeSelect', + componentProps: { + allowClear: true, + api: api.getDeptList, + class: 'w-full', + labelField: 'name', + valueField: 'id', + childrenField: 'children', + }, + fieldName: 'dept_id', + label: '所属组织', + }, + { + component: 'RangePicker', + fieldName: 'createTime', + label: '创建时间', + }, + ]; +} + +export function useColumns( + onActionClick: OnActionClickFn, + onStatusChange?: (newStatus: any, row: T) => PromiseLike, + onIsSuperUserChange?: ( + newStatus: any, + row: T, + ) => PromiseLike, +): VxeTableGridOptions['columns'] { + return [ + { + field: 'id', + title: '设备编号', + width: 100, + }, + { + field: 'name', + title: '设备账号', + width: 150, + }, + { + field: 'online', + slots: { default: 'status' }, + title: '当前状态', + width: 100, + }, + { + field: 'project', + slots: { default: 'project' }, + title: '所属IoT项目', + width: 100, + }, + { + field: 'device_type', + slots: { default: 'deviceType' }, + title: '设备类型', + width: 100, + }, + { + field: 'dept_name', + minWidth: 120, + title: '所属组织', + }, + { + cellRender: { + attrs: { beforeChange: onStatusChange }, + name: onStatusChange ? 'CellSwitch' : 'CellTag', + }, + field: 'status', + title: '可用性', + width: 100, + }, + { + cellRender: { + attrs: { beforeChange: onIsSuperUserChange }, + name: onIsSuperUserChange ? 'CellSwitch' : 'CellTag', + }, + field: 'is_superuser', + title: '管理员', + width: 100, + }, + { + field: 'created_at', + title: '创建时间', + width: 150, + }, + { + field: 'remark', + title: '备注', + width: 300, + }, + { + field: 'version', + title: '软件版本号', + width: 100, + }, + { + field: 'ip', + title: 'IP地址', + width: 100, + }, + { + field: 'hostname', + title: '主机名', + width: 100, + }, + { + field: 'mac', + title: '网卡地址', + width: 100, + }, + { + field: 'os', + title: '操作系统', + width: 100, + }, + { + field: 'cpu', + title: 'CPU型号', + width: 100, + }, + { + field: 'memory_total', + title: '内存大小', + width: 100, + }, + { + field: 'disk_total', + title: '外存大小', + width: 100, + }, + { + field: 'last_seen', + title: '信息更新时间', + width: 100, + }, + { + field: 'operation', + title: '操作', + fixed: 'right', + width: 300, + align: 'center', + cellRender: { + name: 'CellOperationEx', + attrs: { + nameField: 'name', + nameTitle: '设备名称', + onClick: onActionClick, + }, + options: [ + { + code: 'shutdown', + text: '关闭程序', + confirm: true, + confirmTitle: '确认关闭程序?', + confirmMessage: (row) => `设备「${row.name}」将被关闭`, + }, + { + code: 'restart', + text: '重启程序', + confirm: true, + confirmTitle: '确认重启程序?', + confirmMessage: (row) => `设备「${row.name}」将被重启`, + }, + { + code: 'check_update', + text: '检查更新', + confirm: false, + }, + 'edit', + 'delete', + ], + }, + }, + ]; +} diff --git a/vue2/apps/web-antd/src/views/iot/device/form.vue b/vue2/apps/web-antd/src/views/iot/device/form.vue new file mode 100644 index 0000000..6e7976b --- /dev/null +++ b/vue2/apps/web-antd/src/views/iot/device/form.vue @@ -0,0 +1,123 @@ + + + diff --git a/vue2/apps/web-antd/src/views/iot/device/index.vue b/vue2/apps/web-antd/src/views/iot/device/index.vue new file mode 100644 index 0000000..c8e55ba --- /dev/null +++ b/vue2/apps/web-antd/src/views/iot/device/index.vue @@ -0,0 +1,249 @@ + + diff --git a/vue2/apps/web-antd/src/views/iot/update/data.ts b/vue2/apps/web-antd/src/views/iot/update/data.ts new file mode 100644 index 0000000..97e7ed1 --- /dev/null +++ b/vue2/apps/web-antd/src/views/iot/update/data.ts @@ -0,0 +1,179 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { VxeTableGridOptions } from '#/adapter/vxe-table'; + +import { reactive } from 'vue'; + +import { message } from 'ant-design-vue'; +import axios from 'axios'; + +import * as api from '#/api'; + +export function useFormSchema(uploadState: { + sizeKb?: number; + uploaded: boolean; + uploadId?: string; + uploading: boolean; +}): VbenFormSchema[] { + const formModel = reactive<{ code?: string; dept_id?: string }>({}); + + return [ + { + component: 'ApiTreeSelect', + fieldName: 'dept_id', + label: '所属组织', + componentProps: { + allowClear: true, + api: api.getDeptList, + class: 'w-full', + labelField: 'name', + valueField: 'id', + childrenField: 'children', + onChange: async (value: string) => { + if (!value) { + message.warning('请选择组织'); + return; + } + try { + const res = await api.getMaxCodeByDeptId(value); + const codeStr = String(res ?? '0'); + + // 弹窗显示最新版本号 + message.success(`该组织最新版本号:${codeStr}`); + } catch (error) { + message.error('获取最新版本号失败'); + console.error('获取最新版本号失败', error); + } + }, + }, + rules: 'required', + }, + { + component: 'Input', + fieldName: 'code', + label: '版本号', + componentProps: { + allowClear: true, + type: 'number', + }, + }, + { + component: 'Upload', + fieldName: 'oss_url', // 占位字段 + label: '更新包', + componentProps: { + maxCount: 1, + accept: '.exe', + customRequest: async ({ file, onSuccess, onError }) => { + try { + uploadState.uploading = true; + uploadState.uploaded = false; + + // 记录文件大小(KiB,保留 2 位小数) + uploadState.sizeKb = Number((file.size / 1024).toFixed(2)); + + // 获取上传地址 + const { uploadUrl, id } = await api.getUpdateUploadUrl(); + + // PUT 上传 + await axios.put(uploadUrl, file, { + headers: { + 'Content-Type': file.type, + }, + }); + + // 记录状态 + uploadState.uploadId = id; + uploadState.uploaded = true; + + onSuccess?.({}, file); + } catch (error) { + uploadState.uploading = false; + uploadState.uploaded = false; + onError?.(error as Error); + } finally { + uploadState.uploading = false; + } + }, + }, + }, + { + component: 'Textarea', + fieldName: 'remark', + label: '描述', + componentProps: { + allowClear: true, + }, + }, + ]; + + return { schemas, formModel }; +} + +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + component: 'Input', + fieldName: 'id', + label: '升级包编号', + componentProps: { + allowClear: true, + }, + }, + { + component: 'Input', + fieldName: 'code', + label: '版本号', + componentProps: { + allowClear: true, + }, + }, + { + component: 'ApiTreeSelect', + componentProps: { + allowClear: true, + api: api.getDeptList, + class: 'w-full', + labelField: 'name', + valueField: 'id', + childrenField: 'children', + }, + fieldName: 'dept_id', + label: '所属组织', + }, + { + component: 'RangePicker', + fieldName: 'createTime', + label: '创建时间', + }, + ]; +} + +export function useColumns(): VxeTableGridOptions['columns'] { + return [ + { + field: 'id', + title: '升级包编号', + width: 100, + }, + { + field: 'code', + title: '版本号', + width: 150, + }, + { + field: 'remark', + title: '升级描述', + minWidth: 200, + }, + { + field: 'dept_name', + minWidth: 120, + title: '所属组织', + }, + { + field: 'created_at', + title: '创建时间', + width: 200, + }, + ]; +} diff --git a/vue2/apps/web-antd/src/views/iot/update/form.vue b/vue2/apps/web-antd/src/views/iot/update/form.vue new file mode 100644 index 0000000..45f694c --- /dev/null +++ b/vue2/apps/web-antd/src/views/iot/update/form.vue @@ -0,0 +1,115 @@ + + + diff --git a/vue2/apps/web-antd/src/views/iot/update/index.vue b/vue2/apps/web-antd/src/views/iot/update/index.vue new file mode 100644 index 0000000..7f45beb --- /dev/null +++ b/vue2/apps/web-antd/src/views/iot/update/index.vue @@ -0,0 +1,104 @@ + + diff --git a/vue2/apps/web-antd/src/views/sentinel/record/VehicleAlertOverlay.vue b/vue2/apps/web-antd/src/views/sentinel/record/VehicleAlertOverlay.vue new file mode 100644 index 0000000..a3551c7 --- /dev/null +++ b/vue2/apps/web-antd/src/views/sentinel/record/VehicleAlertOverlay.vue @@ -0,0 +1,84 @@ + + + + + diff --git a/vue2/apps/web-antd/src/views/sentinel/record/data.ts b/vue2/apps/web-antd/src/views/sentinel/record/data.ts new file mode 100644 index 0000000..66fb1b5 --- /dev/null +++ b/vue2/apps/web-antd/src/views/sentinel/record/data.ts @@ -0,0 +1,237 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table'; +import type { SystemUserApi } from '#/api'; + +import * as api from '#/api'; + +/** + * 新增/修改 + */ +export function useFormSchema(): VbenFormSchema[] { + return [ + { + component: 'Input', + fieldName: 'license_plate', + label: '车牌', + componentProps: { + allowClear: true, + }, + }, + { + component: 'Input', + fieldName: 'vehicle_type', + label: '车型', + componentProps: { + allowClear: true, + }, + }, + { + component: 'Input', + fieldName: 'livestock_type', + label: '牲畜种类', + componentProps: { + allowClear: true, + }, + }, + { + component: 'Input', + fieldName: 'livestock_source', + label: '牲畜来源', + componentProps: { + allowClear: true, + }, + }, + { + component: 'RadioGroup', + componentProps: { + buttonStyle: 'solid', + options: [ + { label: '已检疫', value: 1 }, + { label: '未检疫', value: 0 }, + ], + optionType: 'button', + }, + defaultValue: 0, + fieldName: 'is_inspected', + label: '检疫状态', + }, + { + component: 'Textarea', + fieldName: 'remark', + label: '备注', + componentProps: { + allowClear: true, + }, + }, + ]; +} + +/** + * 筛选 + */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + component: 'Input', + fieldName: 'id', + label: '编号', + componentProps: { + allowClear: true, + }, + }, + { + component: 'Input', + fieldName: 'license_plate', + label: '车牌', + componentProps: { + allowClear: true, + }, + }, + { + component: 'Input', + fieldName: 'vehicle_type', + label: '车型', + componentProps: { + allowClear: true, + }, + }, + { + component: 'ApiCombobox', + fieldName: 'livestock_type', + label: '牲畜种类', + componentProps: { + api: () => api.getDictDetailList('livestock_type'), + labelField: 'value', + valueField: 'value', + showSearch: true, + mode: 'combobox', // 可选 combobox / tags + allowClear: true, + }, + }, + { + component: 'Input', + fieldName: 'livestock_source', + label: '牲畜来源', + componentProps: { + allowClear: true, + }, + }, + { + component: 'Select', + componentProps: { + allowClear: true, + options: [ + { label: '已检疫', value: 1 }, + { label: '未检疫', value: 0 }, + ], + }, + fieldName: 'is_inspected', + label: '检疫状态', + }, + { + component: 'RangePicker', + fieldName: 'createTime', + label: '录入时间', + }, + ]; +} + +/** + * 列表展示 + * @param onActionClick + * @param onStatusChange + */ +export function useColumns( + onActionClick: OnActionClickFn, + onStatusChange?: (newStatus: any, row: T) => PromiseLike, +): VxeTableGridOptions['columns'] { + return [ + { + field: 'id', + title: '编号', + width: 100, + }, + { + field: 'license_plate', + title: '车牌', + width: 150, + }, + { + cellRender: { name: 'CellImage' }, + field: 'license_plate_image', + title: '车牌照', + width: 150, + }, + { + field: 'vehicle_type', + title: '车型', + width: 150, + }, + { + cellRender: { name: 'CellImage' }, + field: 'vehicle_image', + title: '车身照', + width: 150, + }, + { + field: 'livestock_type', + title: '牲畜种类', + width: 100, + }, + { + field: 'livestock_source', + title: '牲畜来源', + width: 200, + }, + { + field: 'is_inspected', + title: '检疫状态', + width: 100, + cellRender: { + name: 'CellSwitch', + props: { + checkedChildren: '已检疫', + unCheckedChildren: '未检疫', + }, + attrs: { + beforeChange: onStatusChange, + }, + }, + }, + { + field: 'created_at', + title: '录入时间', + width: 150, + }, + { + field: 'updated_at', + title: '最后更新时间', + width: 150, + }, + { + field: 'dept_name', + title: '所属组织', + width: 300, + }, + { + field: 'remark', + title: '备注', + width: 300, + }, + { + align: 'center', + cellRender: { + attrs: { + nameField: 'name', + nameTitle: '设备名称', + onClick: onActionClick, + }, + name: 'CellOperation', + }, + field: 'operation', + fixed: 'right', + title: '操作', + width: 100, + }, + ]; +} diff --git a/vue2/apps/web-antd/src/views/sentinel/record/form.vue b/vue2/apps/web-antd/src/views/sentinel/record/form.vue new file mode 100644 index 0000000..8c2a853 --- /dev/null +++ b/vue2/apps/web-antd/src/views/sentinel/record/form.vue @@ -0,0 +1,126 @@ + + + diff --git a/vue2/apps/web-antd/src/views/sentinel/record/index.vue b/vue2/apps/web-antd/src/views/sentinel/record/index.vue new file mode 100644 index 0000000..98abdaa --- /dev/null +++ b/vue2/apps/web-antd/src/views/sentinel/record/index.vue @@ -0,0 +1,238 @@ + +