diff --git a/vue2/apps/web-antd/src/adapter/component/index.ts b/vue2/apps/web-antd/src/adapter/component/index.ts index 8455ef3..a62114e 100644 --- a/vue2/apps/web-antd/src/adapter/component/index.ts +++ b/vue2/apps/web-antd/src/adapter/component/index.ts @@ -372,6 +372,20 @@ async function initComponentAdapter() { modelPropName: 'value', }, ), + ApiCombobox: withDefaultPlaceholder( + { + ...ApiComponent, + name: 'ApiCombobox', + }, + 'select', + { + component: Select, + loadingSlot: 'suffixIcon', + visibleEvent: 'onDropdownVisibleChange', + modelPropName: 'value', + props: ['mode', 'allowClear', 'filterOption'], + }, +), ApiTreeSelect: withDefaultPlaceholder( { ...ApiComponent, diff --git a/vue2/apps/web-antd/src/api/index.ts b/vue2/apps/web-antd/src/api/index.ts index 472b4ae..0d69859 100644 --- a/vue2/apps/web-antd/src/api/index.ts +++ b/vue2/apps/web-antd/src/api/index.ts @@ -1,4 +1,6 @@ export * from './core'; export * from './cv'; +export * from './iot'; export * from './llm'; export * from './manager'; +export * from './sentinel'; diff --git a/vue2/apps/web-antd/src/api/iot/device.ts b/vue2/apps/web-antd/src/api/iot/device.ts new file mode 100644 index 0000000..ef55d79 --- /dev/null +++ b/vue2/apps/web-antd/src/api/iot/device.ts @@ -0,0 +1,80 @@ +import type { Recordable } from '@vben/types'; + +import { sha256 } from 'js-sha256'; + +import { pyRequestClient } from '#/api/request'; + +export namespace SystemDeviceApi { + export interface SystemDevice { + [key: string]: any; + id: string; + remark?: string; + name: string; + roles: string[]; + status: 0 | 1; + is_superuser: 0 | 1; + dept_id?: string; + dept_name?: string; + } +} + +/** + * 获取设备列表数据 + */ +async function getDeviceList(params: Recordable) { + return pyRequestClient.get>( + '/iot/common/device/list', + { params }, + ); +} + +/** + * 创建设备 + * @param data 设备数据 + */ +async function createDevice(data: Omit) { + data.password_hash = sha256(data.password?.toString() || ''); + delete data.password; + return pyRequestClient.post('/iot/common/device', data); +} + +/** + * 更新设备 + * + * @param id 设备 ID + * @param data 设备数据 + */ +async function updateDevice( + id: string, + data: Omit, +) { + return pyRequestClient.put(`/iot/common/device/${id}`, data); +} +/** + * 更新设备(部分更新) + * + * @param id 设备 ID + * @param data 需要更新的字段(部分字段即可) + */ +async function updateDevicePatch( + id: string, + data: Partial>, +) { + return pyRequestClient.patch(`/iot/common/device/${id}`, data); +} + +/** + * 删除设备 + * @param id 设备 ID + */ +async function deleteDevice(id: string) { + return pyRequestClient.delete(`/iot/common/device/${id}`); +} + +export { + createDevice, + deleteDevice, + getDeviceList, + updateDevice, + updateDevicePatch, +}; diff --git a/vue2/apps/web-antd/src/api/iot/index.ts b/vue2/apps/web-antd/src/api/iot/index.ts new file mode 100644 index 0000000..0ce9c17 --- /dev/null +++ b/vue2/apps/web-antd/src/api/iot/index.ts @@ -0,0 +1 @@ +export * from './device'; diff --git a/vue2/apps/web-antd/src/api/manager/dept.ts b/vue2/apps/web-antd/src/api/manager/dept.ts index e6fc077..1f76f30 100644 --- a/vue2/apps/web-antd/src/api/manager/dept.ts +++ b/vue2/apps/web-antd/src/api/manager/dept.ts @@ -10,6 +10,16 @@ export namespace SystemDeptApi { } } +export namespace SystemDeptDetailApi { + export interface SystemDept { + [key: string]: any; + children?: SystemDept[]; + id: string; + name: string; + comment?: string; + } +} + /** * 获取部门列表数据 */ diff --git a/vue2/apps/web-antd/src/api/manager/dict.ts b/vue2/apps/web-antd/src/api/manager/dict.ts new file mode 100644 index 0000000..1e5220f --- /dev/null +++ b/vue2/apps/web-antd/src/api/manager/dict.ts @@ -0,0 +1,105 @@ +import { pyRequestClient } from '#/api/request'; + +export namespace SystemDictApi { + export interface SystemDict { + [key: string]: any; + id: string; + name: string; + key: string; + remark?: string; + } +} + +/** + * 获取字典列表数据 + */ +async function getDictList(name = '', page = 1, pageSize = 9) { + return pyRequestClient.get>( + '/system/dict/list', + { params: { name, page, page_size: pageSize } }, + ); +} +/** + * 创建字典 + * @param data 字典数据 + */ +async function createDict(data: Omit) { + return pyRequestClient.post('/system/dict', data); +} + +/** + * 更新字典 + * + * @param id 字典 ID + * @param data 字典数据 + */ +async function updateDict( + id: string, + data: Omit, +) { + return pyRequestClient.put(`/system/dict/${id}`, data); +} + +/** + * 删除字典 + * @param id 字典 ID + */ +async function deleteDict(id: string) { + return pyRequestClient.delete(`/system/dict/${id}`); +} +export namespace SystemDictDetailApi { + export interface DictDetail { + id: string; + value: string; + sort?: number; + pid?: null | string; + dict_id: string; + remark?: string; + created_at?: string; + updated_at?: string; + } +} + +/** + * 获取字典详情列表(不分页) + */ +export async function getDictDetail(dictId: string) { + return pyRequestClient.get>( + '/system/dict/detail/list', + { params: { dictId } }, + ); +} + +/** + * 新增字典详情 + */ +export async function createDictDetail( + data: Omit< + SystemDictDetailApi.DictDetail, + 'created_at' | 'id' | 'updated_at' + >, +) { + return pyRequestClient.post('/system/dict/detail', data); +} + +/** + * 更新字典详情 + */ +export async function updateDictDetail( + id: string, + data: Partial< + Omit + >, +) { + return pyRequestClient.put(`/system/dict/detail/${id}`, data); +} + +/** + * 删除字典详情 + */ +export async function deleteDictDetail(id: string) { + return pyRequestClient.delete(`/system/dict/detail/${id}`); +} + + +export { createDict, deleteDict, getDictList, updateDict }; diff --git a/vue2/apps/web-antd/src/api/manager/index.ts b/vue2/apps/web-antd/src/api/manager/index.ts index 1b28fb6..00f5113 100644 --- a/vue2/apps/web-antd/src/api/manager/index.ts +++ b/vue2/apps/web-antd/src/api/manager/index.ts @@ -1,4 +1,6 @@ export * from './dept'; +export * from './dict'; export * from './menu'; export * from './role'; export * from './user'; +export * from './sys'; diff --git a/vue2/apps/web-antd/src/api/manager/sys.ts b/vue2/apps/web-antd/src/api/manager/sys.ts new file mode 100644 index 0000000..e6419c7 --- /dev/null +++ b/vue2/apps/web-antd/src/api/manager/sys.ts @@ -0,0 +1,25 @@ +import { pyRequestClient } from '#/api/request'; +// system/dict.ts 或 system/dictDetail.ts + +export namespace SystemDictApi { + export interface DictDetail { + id: string; + value: string; + label: string; + sort: number; + pid?: string | null; + remark?: string; + created_at?: string; + } +} +/** + * 根据字典 key 获取字典详情列表 + */ +export async function getDictDetailList(key: string) { + return pyRequestClient.get>( + '/system/dict/getValue', + { + params: { key }, + }, + ); +} diff --git a/vue2/apps/web-antd/src/api/manager/user.ts b/vue2/apps/web-antd/src/api/manager/user.ts index c25a535..7787740 100644 --- a/vue2/apps/web-antd/src/api/manager/user.ts +++ b/vue2/apps/web-antd/src/api/manager/user.ts @@ -1,11 +1,14 @@ import type { Recordable } from '@vben/types'; +import { sha256 } from 'js-sha256'; + +import { SystemMenuApi } from '#/api'; import { pyRequestClient } from '#/api/request'; -import { sha256 } from "js-sha256"; export namespace SystemUserApi { export interface SystemUser { [key: string]: any; + id: string; email?: string; name: string; @@ -37,18 +40,15 @@ async function createUser(data: Omit) { return pyRequestClient.post('/system/user', data); } -/** - * 更新用户 - * - * @param id 用户 ID - * @param data 用户数据 - */ -async function updateUser( - id: string, - data: Omit, +async function isUserNameExists( + username: string, + id?: SystemMenuApi.SystemMenu['id'], ) { - return pyRequestClient.put(`/system/user/${id}`, data); + return pyRequestClient.get('/system/user/name-exists', { + params: { id, username }, + }); } + /** * 更新用户(部分更新) * @@ -59,6 +59,14 @@ async function updateUserPatch( id: string, data: Partial>, ) { + if ( + data.password !== null && + data.password !== undefined && + data.password.trim() !== '' + ) { + data.password_hash = sha256(data.password?.toString() || ''); + } + delete data.password; return pyRequestClient.patch(`/system/user/${id}`, data); } @@ -70,4 +78,10 @@ async function deleteUser(id: string) { return pyRequestClient.delete(`/system/user/${id}`); } -export { createUser, deleteUser, getUserList, updateUser,updateUserPatch }; +export { + createUser, + deleteUser, + getUserList, + isUserNameExists, + updateUserPatch, +}; diff --git a/vue2/apps/web-antd/src/api/sentinel/device.ts b/vue2/apps/web-antd/src/api/sentinel/device.ts new file mode 100644 index 0000000..4469d6f --- /dev/null +++ b/vue2/apps/web-antd/src/api/sentinel/device.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/api/sentinel/index.ts b/vue2/apps/web-antd/src/api/sentinel/index.ts new file mode 100644 index 0000000..0ce9c17 --- /dev/null +++ b/vue2/apps/web-antd/src/api/sentinel/index.ts @@ -0,0 +1 @@ +export * from './device'; diff --git a/vue2/apps/web-antd/src/router/routes/modules/manager.ts b/vue2/apps/web-antd/src/router/routes/modules/manager.ts index 1b863dc..4d65a18 100644 --- a/vue2/apps/web-antd/src/router/routes/modules/manager.ts +++ b/vue2/apps/web-antd/src/router/routes/modules/manager.ts @@ -20,22 +20,11 @@ const routes: RouteRecordRaw[] = [ meta: { authority: ['manager'], icon: 'mdi:office-building-cog-outline', - title: '部门管理', + title: '组织管理', keepAlive: true, }, component: () => import('#/views/manager/department/index.vue'), }, - { - name: 'M-user', - path: '/manager/user', - meta: { - authority: ['manager'], - icon: 'mdi:account-cog-outline', - title: '用户管理', - keepAlive: true, - }, - component: () => import('#/views/manager/user/index.vue'), - }, { name: 'M-role', path: '/manager/role', @@ -47,13 +36,35 @@ const routes: RouteRecordRaw[] = [ }, component: () => import('#/views/manager/role/index.vue'), }, + { + name: 'M-user', + path: '/manager/user', + meta: { + authority: ['manager'], + icon: 'mdi:account-cog-outline', + title: '用户管理', + keepAlive: true, + }, + component: () => import('#/views/manager/user/index.vue'), + }, + { + name: 'M-dict', + path: '/manager/dict', + meta: { + authority: ['manager'], + icon: 'mdi:book-open-outline', + title: '字典管理', + keepAlive: true, + }, + component: () => import('#/views/manager/dict/index.vue'), + }, { name: 'M-perm', path: '/manager/perm', meta: { authority: ['manager'], icon: 'mdi:menu', - title: '菜单-AI实验室', + title: '菜单-实验室', keepAlive: true, }, component: () => import('#/views/manager/menu/index.vue'), @@ -64,7 +75,7 @@ const routes: RouteRecordRaw[] = [ meta: { authority: ['manager'], icon: 'mdi:menu', - title: '菜单-大数据可视化平台', + title: '菜单-大数据', keepAlive: true, }, component: () => import('#/views/manager/menu/index2.vue'), diff --git a/vue2/apps/web-antd/src/views/iot/data.ts b/vue2/apps/web-antd/src/views/iot/data.ts new file mode 100644 index 0000000..540538d --- /dev/null +++ b/vue2/apps/web-antd/src/views/iot/data.ts @@ -0,0 +1,224 @@ +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(20, $t('ui.formRules.maxLength', ['设备账号', 20])), + 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: 'online', + slots: { default: 'status' }, + title: '当前状态', + width: 100, + }, + { + field: 'name', + title: '设备账号', + width: 150, + }, + { + 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: 'dept_name', + minWidth: 120, + title: '所属组织', + }, + { + field: 'created_at', + title: '创建时间', + width: 200, + }, + { + field: 'remark', + title: '备注', + width: 200, + }, + { + align: 'center', + cellRender: { + attrs: { + nameField: 'name', + nameTitle: '设备名称', + onClick: onActionClick, + }, + name: 'CellOperation', + }, + field: 'operation', + fixed: 'right', + title: '操作', + width: 130, + }, + ]; +} diff --git a/vue2/apps/web-antd/src/views/iot/form.vue b/vue2/apps/web-antd/src/views/iot/form.vue new file mode 100644 index 0000000..5699ea7 --- /dev/null +++ b/vue2/apps/web-antd/src/views/iot/form.vue @@ -0,0 +1,126 @@ + + + diff --git a/vue2/apps/web-antd/src/views/iot/index.vue b/vue2/apps/web-antd/src/views/iot/index.vue new file mode 100644 index 0000000..bcc82ee --- /dev/null +++ b/vue2/apps/web-antd/src/views/iot/index.vue @@ -0,0 +1,213 @@ + + diff --git a/vue2/apps/web-antd/src/views/manager/department/data.ts b/vue2/apps/web-antd/src/views/manager/department/data.ts index e11d5e1..c920ab7 100644 --- a/vue2/apps/web-antd/src/views/manager/department/data.ts +++ b/vue2/apps/web-antd/src/views/manager/department/data.ts @@ -17,11 +17,11 @@ export function useSchema(): VbenFormSchema[] { { component: 'Input', fieldName: 'name', - label: '部门名', + label: '组织名', rules: z .string() - .min(2, $t('ui.formRules.minLength', ['部门名', 2])) - .max(20, $t('ui.formRules.maxLength', ['部门名', 20])), + .min(2, $t('ui.formRules.minLength', ['组织名', 2])) + .max(20, $t('ui.formRules.maxLength', ['组织名', 20])), }, { component: 'ApiTreeSelect', @@ -34,7 +34,7 @@ export function useSchema(): VbenFormSchema[] { childrenField: 'children', }, fieldName: 'pid', - label: '父部门', + label: '父组织', }, { component: 'Textarea', @@ -66,9 +66,9 @@ export function useColumns( align: 'left', field: 'name', fixed: 'left', - title: '部门名', + title: '组织名', treeNode: true, - width: 150, + width: 300, }, { field: 'createTime', @@ -90,7 +90,7 @@ export function useColumns( cellRender: { attrs: { nameField: 'name', - nameTitle: '部门名', + nameTitle: '组织名', onClick: onActionClick, }, name: 'CellOperation', diff --git a/vue2/apps/web-antd/src/views/manager/department/form.vue b/vue2/apps/web-antd/src/views/manager/department/form.vue index 4e0ddab..179db06 100644 --- a/vue2/apps/web-antd/src/views/manager/department/form.vue +++ b/vue2/apps/web-antd/src/views/manager/department/form.vue @@ -17,8 +17,8 @@ const emit = defineEmits(['success']); const formData = ref(); const getTitle = computed(() => { return formData.value?.id - ? $t('ui.actionTitle.edit', ['部门']) - : $t('ui.actionTitle.create', ['部门']); + ? $t('ui.actionTitle.edit', ['组织']) + : $t('ui.actionTitle.create', ['组织']); }); const [Form, formApi] = useVbenForm({ diff --git a/vue2/apps/web-antd/src/views/manager/department/index.vue b/vue2/apps/web-antd/src/views/manager/department/index.vue index e23b29c..7166b14 100644 --- a/vue2/apps/web-antd/src/views/manager/department/index.vue +++ b/vue2/apps/web-antd/src/views/manager/department/index.vue @@ -31,7 +31,7 @@ function onEdit(row: SystemDeptApi.SystemDept) { } /** - * 添加下级部门 + * 添加下级组织 * @param row */ function onAppend(row: SystemDeptApi.SystemDept) { @@ -39,14 +39,14 @@ function onAppend(row: SystemDeptApi.SystemDept) { } /** - * 创建新部门 + * 创建新组织 */ function onCreate() { formModalApi.setData(null).open(); } /** - * 删除部门 + * 删除组织 * @param row */ function onDelete(row: SystemDeptApi.SystemDept) { @@ -132,11 +132,11 @@ function refreshGrid() {