feat: 完善功能
This commit is contained in:
parent
a4aa133db5
commit
40803462ac
@ -1,8 +1,8 @@
|
||||
# 应用标题
|
||||
VITE_APP_TITLE=Vben Admin Tdesign
|
||||
VITE_APP_TITLE=星戎物联
|
||||
|
||||
# 应用命名空间,用于缓存、store等功能的前缀,确保隔离
|
||||
VITE_APP_NAMESPACE=vben-web-tdesign
|
||||
VITE_APP_NAMESPACE=iot
|
||||
|
||||
# 对store进行加密的密钥,在将store持久化到localStorage时会使用该密钥进行加密
|
||||
VITE_APP_STORE_SECURE_KEY=please-replace-me-with-your-own-key
|
||||
VITE_APP_STORE_SECURE_KEY=xrit2doa1q3swe
|
||||
|
||||
24
apps/web-tdesign/src/api/core/admin.ts
Normal file
24
apps/web-tdesign/src/api/core/admin.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export async function getAdminInfoApi() {
|
||||
return requestClient.post('/admin/info');
|
||||
}
|
||||
|
||||
export async function getAdminListByPage(data: any) {
|
||||
return requestClient.post('/admin/queryByPage', data);
|
||||
}
|
||||
|
||||
export async function getAdminOptions() {
|
||||
return requestClient.post('/admin/options');
|
||||
}
|
||||
|
||||
export async function createAdmin(data: any) {
|
||||
return requestClient.post('/admin/create', data);
|
||||
}
|
||||
export async function updateAdmin(data: any) {
|
||||
return requestClient.post('/admin/update', data);
|
||||
}
|
||||
|
||||
export async function changeAdminStatus(data: any) {
|
||||
return requestClient.post('/admin/changeStatus', data);
|
||||
}
|
||||
@ -47,5 +47,5 @@ export async function logoutApi() {
|
||||
* 获取用户权限码
|
||||
*/
|
||||
export async function getAccessCodesApi() {
|
||||
return requestClient.get<string[]>('/auth/codes');
|
||||
return requestClient.post<string[]>('/auth/codes');
|
||||
}
|
||||
|
||||
5
apps/web-tdesign/src/api/core/card.ts
Normal file
5
apps/web-tdesign/src/api/core/card.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export function getCardOptions() {
|
||||
return requestClient.post('/card/options');
|
||||
}
|
||||
21
apps/web-tdesign/src/api/core/dept.ts
Normal file
21
apps/web-tdesign/src/api/core/dept.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export function getDeptList() {
|
||||
return requestClient.post('/dept/queryAll');
|
||||
}
|
||||
|
||||
export function getDeptOptions() {
|
||||
return requestClient.post('/dept/options');
|
||||
}
|
||||
|
||||
export function createDept(data: any) {
|
||||
return requestClient.post('/dept/create', data);
|
||||
}
|
||||
|
||||
export function updateDept(data: any) {
|
||||
return requestClient.post('/dept/update', data);
|
||||
}
|
||||
|
||||
export function changeDeptStatus(data: any) {
|
||||
return requestClient.post('/dept/changeStatus', data);
|
||||
}
|
||||
17
apps/web-tdesign/src/api/core/device.ts
Normal file
17
apps/web-tdesign/src/api/core/device.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export function getDeviceListByPage(data: any) {
|
||||
return requestClient.post('/device/queryByPage', data);
|
||||
}
|
||||
|
||||
export function getDeviceOptions() {
|
||||
return requestClient.post('/device/options');
|
||||
}
|
||||
|
||||
export function createDevice(data: any) {
|
||||
return requestClient.post('/device/create', data);
|
||||
}
|
||||
|
||||
export function updateDevice(data: any) {
|
||||
return requestClient.post('/device/update', data);
|
||||
}
|
||||
@ -1,3 +1,6 @@
|
||||
export * from './admin';
|
||||
export * from './auth';
|
||||
export * from './dept';
|
||||
export * from './menu';
|
||||
export * from './user';
|
||||
export * from './point';
|
||||
export * from './role';
|
||||
|
||||
@ -1,10 +1,24 @@
|
||||
import type { RouteRecordStringComponent } from '@vben/types';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
/**
|
||||
* 获取用户所有菜单
|
||||
*/
|
||||
export async function getAllMenusApi() {
|
||||
return requestClient.get<RouteRecordStringComponent[]>('/menu/all');
|
||||
return requestClient.get('/menu/all');
|
||||
}
|
||||
|
||||
export async function getMenuList() {
|
||||
return requestClient.post('/menu/queryAll');
|
||||
}
|
||||
|
||||
export async function getMenuTree() {
|
||||
return requestClient.post('/menu/queryByTree');
|
||||
}
|
||||
|
||||
export async function createMenu(data: any) {
|
||||
return requestClient.post('/menu/create', data);
|
||||
}
|
||||
export async function updateMenu(data: any) {
|
||||
return requestClient.post('/menu/update', data);
|
||||
}
|
||||
|
||||
export async function changeMenuStatus(data: any) {
|
||||
return requestClient.post('/menu/changeStatus', data);
|
||||
}
|
||||
|
||||
17
apps/web-tdesign/src/api/core/point.ts
Normal file
17
apps/web-tdesign/src/api/core/point.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export function getPointListByPage(data: any) {
|
||||
return requestClient.post('/point/queryByPage', data);
|
||||
}
|
||||
|
||||
export function getPointOptions() {
|
||||
return requestClient.post('/point/options');
|
||||
}
|
||||
|
||||
export function createPoint(data: any) {
|
||||
return requestClient.post('/point/create', data);
|
||||
}
|
||||
|
||||
export function updatePoint(data: any) {
|
||||
return requestClient.post('/point/update', data);
|
||||
}
|
||||
5
apps/web-tdesign/src/api/core/product.ts
Normal file
5
apps/web-tdesign/src/api/core/product.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export function getProductOptions() {
|
||||
return requestClient.post('/product/options');
|
||||
}
|
||||
21
apps/web-tdesign/src/api/core/role.ts
Normal file
21
apps/web-tdesign/src/api/core/role.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export function getRoleList() {
|
||||
return requestClient.post('/role/list');
|
||||
}
|
||||
|
||||
export function getRoleOptions() {
|
||||
return requestClient.post('/role/options');
|
||||
}
|
||||
|
||||
export function createRole(data: any) {
|
||||
return requestClient.post('/role/create', data);
|
||||
}
|
||||
|
||||
export function updateRole(data: any) {
|
||||
return requestClient.post('/role/update', data);
|
||||
}
|
||||
|
||||
export function changeRoleStatus(data: any) {
|
||||
return requestClient.post('/role/changeStatus', data);
|
||||
}
|
||||
10
apps/web-tdesign/src/enum/CommonStatus.ts
Normal file
10
apps/web-tdesign/src/enum/CommonStatus.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export const CommonStatusEnum = [
|
||||
{
|
||||
value: 1,
|
||||
label: '启用',
|
||||
},
|
||||
{
|
||||
value: 0,
|
||||
label: '禁用',
|
||||
},
|
||||
];
|
||||
14
apps/web-tdesign/src/enum/Gender.ts
Normal file
14
apps/web-tdesign/src/enum/Gender.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { toObject } from '#/enum/index';
|
||||
|
||||
export const GenderEnum = [
|
||||
{
|
||||
label: '男',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: '女',
|
||||
value: 2,
|
||||
},
|
||||
];
|
||||
|
||||
export const GenderEnumMap = toObject(GenderEnum, 'value', 'label');
|
||||
42
apps/web-tdesign/src/enum/Grade.ts
Normal file
42
apps/web-tdesign/src/enum/Grade.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { toObject } from '#/enum/index';
|
||||
|
||||
export const GradeEnum = [
|
||||
{
|
||||
value: 1,
|
||||
label: '一年级',
|
||||
},
|
||||
{
|
||||
value: 2,
|
||||
label: '二年级',
|
||||
},
|
||||
{
|
||||
value: 3,
|
||||
label: '三年级',
|
||||
},
|
||||
{
|
||||
value: 4,
|
||||
label: '四年级',
|
||||
},
|
||||
{
|
||||
value: 5,
|
||||
label: '五年级',
|
||||
},
|
||||
{
|
||||
value: 6,
|
||||
label: '六年级',
|
||||
},
|
||||
{
|
||||
value: 7,
|
||||
label: '七年级',
|
||||
},
|
||||
{
|
||||
value: 8,
|
||||
label: '八年级',
|
||||
},
|
||||
{
|
||||
value: 9,
|
||||
label: '九年级',
|
||||
},
|
||||
];
|
||||
|
||||
export const GradeEnumMap = toObject(GradeEnum, 'value', 'label');
|
||||
18
apps/web-tdesign/src/enum/MenuType.ts
Normal file
18
apps/web-tdesign/src/enum/MenuType.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { toObject } from '#/enum/index';
|
||||
|
||||
export const MenuTypeEnum = [
|
||||
{
|
||||
label: '目录',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: '菜单',
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
label: '按钮',
|
||||
value: 3,
|
||||
},
|
||||
];
|
||||
|
||||
export const MenuTypeEnumMap = toObject(MenuTypeEnum, 'value', 'label');
|
||||
15
apps/web-tdesign/src/enum/RoleCode.ts
Normal file
15
apps/web-tdesign/src/enum/RoleCode.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { toObject } from '#/enum/index';
|
||||
|
||||
export const RoleCodeEnum = [
|
||||
{
|
||||
value: 'root',
|
||||
label: '管理员',
|
||||
color: {
|
||||
color: '#FCE8E6', // 背景浅红
|
||||
textColor: '#D32F2F', // 文字深红
|
||||
borderColor: '#E57373', // 边框柔红
|
||||
},
|
||||
},
|
||||
];
|
||||
export const RoleCodeEnumMap = toObject(RoleCodeEnum, 'value', 'label');
|
||||
export const RoleCodeEnumColorMap = toObject(RoleCodeEnum, 'value', 'color');
|
||||
9
apps/web-tdesign/src/enum/SendHandler.ts
Normal file
9
apps/web-tdesign/src/enum/SendHandler.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { toObject } from '#/enum/index';
|
||||
|
||||
export const SendHandlerEnum = [
|
||||
{
|
||||
label: '地灾平台',
|
||||
value: 'DZPTSendHandler',
|
||||
},
|
||||
];
|
||||
export const SendHandlerEnumMap = toObject(SendHandlerEnum, 'value', 'label');
|
||||
10
apps/web-tdesign/src/enum/Subject.ts
Normal file
10
apps/web-tdesign/src/enum/Subject.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { toObject } from '#/enum/index';
|
||||
|
||||
export const SubjectEnum = [
|
||||
{
|
||||
value: 1,
|
||||
label: '数学',
|
||||
},
|
||||
];
|
||||
|
||||
export const SubjectEnumMap = toObject(SubjectEnum, 'value', 'label');
|
||||
13
apps/web-tdesign/src/enum/index.ts
Normal file
13
apps/web-tdesign/src/enum/index.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export const toObject = <T extends Record<string, any>>(
|
||||
arr: T[],
|
||||
keyField: keyof T,
|
||||
valueField: keyof T,
|
||||
): Record<string, any> => {
|
||||
const result: Record<string, any> = {};
|
||||
|
||||
for (const item of arr) {
|
||||
result[item[keyField] as any] = item[valueField];
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
53
apps/web-tdesign/src/router/routes/modules/system.ts
Normal file
53
apps/web-tdesign/src/router/routes/modules/system.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
meta: {
|
||||
icon: 'lucide:layout-dashboard',
|
||||
order: -1,
|
||||
title: '系统管理',
|
||||
},
|
||||
name: 'System',
|
||||
path: '/system',
|
||||
children: [
|
||||
{
|
||||
name: 'User',
|
||||
path: '/admin',
|
||||
component: () => import('#/views/system/admin/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:area-chart',
|
||||
title: '用户管理',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Role',
|
||||
path: '/role',
|
||||
component: () => import('#/views/system/role/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:area-chart',
|
||||
title: '角色管理',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Menu',
|
||||
path: '/menu',
|
||||
component: () => import('#/views/system/menu/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:area-chart',
|
||||
title: '权限管理',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Dept',
|
||||
path: '/dept',
|
||||
component: () => import('#/views/system/dept/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:area-chart',
|
||||
title: '部门管理',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
46
apps/web-tdesign/src/router/routes/modules/things.ts
Normal file
46
apps/web-tdesign/src/router/routes/modules/things.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
meta: {
|
||||
icon: 'lucide:layout-dashboard',
|
||||
order: -1,
|
||||
title: '运维管理',
|
||||
},
|
||||
name: 'Things',
|
||||
path: '/things',
|
||||
children: [
|
||||
{
|
||||
name: 'Point',
|
||||
path: '/point',
|
||||
component: () => import('#/views/things/point/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:area-chart',
|
||||
title: '监测点管理',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Device',
|
||||
path: '/device',
|
||||
component: () => import('#/views/things/device/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:area-chart',
|
||||
title: '设备管理',
|
||||
fullPathKey: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Flow',
|
||||
path: '/flow',
|
||||
component: () => import('#/views/things/flow/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:area-chart',
|
||||
title: '同步管理',
|
||||
fullPathKey: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
@ -10,7 +10,7 @@ import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
import { notification } from '#/adapter/tdesign';
|
||||
import { getAccessCodesApi, getUserInfoApi, loginApi, logoutApi } from '#/api';
|
||||
import { getAccessCodesApi, getAdminInfoApi, loginApi, logoutApi } from '#/api';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
@ -98,7 +98,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
|
||||
async function fetchUserInfo() {
|
||||
let userInfo: null | UserInfo = null;
|
||||
userInfo = await getUserInfoApi();
|
||||
userInfo = await getAdminInfoApi();
|
||||
userStore.setUserInfo(userInfo);
|
||||
return userInfo;
|
||||
}
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VbenFormSchema } from '@vben/common-ui';
|
||||
import type { BasicOption } from '@vben/types';
|
||||
|
||||
import { computed, markRaw } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { AuthenticationLogin, SliderCaptcha, z } from '@vben/common-ui';
|
||||
import { AuthenticationLogin, z } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { useAuthStore } from '#/store';
|
||||
@ -13,58 +12,13 @@ defineOptions({ name: 'Login' });
|
||||
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const MOCK_USER_OPTIONS: BasicOption[] = [
|
||||
{
|
||||
label: 'Super',
|
||||
value: 'vben',
|
||||
},
|
||||
{
|
||||
label: 'Admin',
|
||||
value: 'admin',
|
||||
},
|
||||
{
|
||||
label: 'User',
|
||||
value: 'jack',
|
||||
},
|
||||
];
|
||||
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
component: 'VbenSelect',
|
||||
componentProps: {
|
||||
options: MOCK_USER_OPTIONS,
|
||||
placeholder: $t('authentication.selectAccount'),
|
||||
},
|
||||
fieldName: 'selectAccount',
|
||||
label: $t('authentication.selectAccount'),
|
||||
rules: z
|
||||
.string()
|
||||
.min(1, { message: $t('authentication.selectAccount') })
|
||||
.optional()
|
||||
.default('vben'),
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.usernameTip'),
|
||||
},
|
||||
dependencies: {
|
||||
trigger(values, form) {
|
||||
if (values.selectAccount) {
|
||||
const findUser = MOCK_USER_OPTIONS.find(
|
||||
(item) => item.value === values.selectAccount,
|
||||
);
|
||||
if (findUser) {
|
||||
form.setValues({
|
||||
password: '123456',
|
||||
username: findUser.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
triggerFields: ['selectAccount'],
|
||||
},
|
||||
fieldName: 'username',
|
||||
label: $t('authentication.username'),
|
||||
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
|
||||
@ -78,14 +32,6 @@ const formSchema = computed((): VbenFormSchema[] => {
|
||||
label: $t('authentication.password'),
|
||||
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
|
||||
},
|
||||
|
||||
{
|
||||
component: markRaw(SliderCaptcha),
|
||||
fieldName: 'captcha',
|
||||
rules: z.boolean().refine((value) => value, {
|
||||
message: $t('authentication.verifyRequiredTip'),
|
||||
}),
|
||||
},
|
||||
];
|
||||
});
|
||||
</script>
|
||||
@ -94,6 +40,12 @@ const formSchema = computed((): VbenFormSchema[] => {
|
||||
<AuthenticationLogin
|
||||
:form-schema="formSchema"
|
||||
:loading="authStore.loginLoading"
|
||||
:show-forget-password="false"
|
||||
:show-code-login="false"
|
||||
:show-qrcode-login="false"
|
||||
:show-register="false"
|
||||
:show-remember-me="false"
|
||||
:show-third-party-login="false"
|
||||
@submit="authStore.authLogin"
|
||||
/>
|
||||
</template>
|
||||
|
||||
147
apps/web-tdesign/src/views/system/admin/data.ts
Normal file
147
apps/web-tdesign/src/views/system/admin/data.ts
Normal file
@ -0,0 +1,147 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
|
||||
import { getDeptOptions, getRoleOptions } from '#/api';
|
||||
|
||||
export function useSubmitFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'displayName',
|
||||
label: '姓名',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'account',
|
||||
label: '账户',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'ApiSelect',
|
||||
fieldName: 'roleIds',
|
||||
componentProps: {
|
||||
api: getRoleOptions,
|
||||
multiple: true,
|
||||
clearable: true,
|
||||
labelField: 'roleName',
|
||||
valueField: 'id',
|
||||
},
|
||||
label: '角色',
|
||||
},
|
||||
{
|
||||
component: 'ApiSelect',
|
||||
fieldName: 'deptId',
|
||||
componentProps: {
|
||||
api: getDeptOptions,
|
||||
clearable: true,
|
||||
labelField: 'deptName',
|
||||
valueField: 'id',
|
||||
},
|
||||
label: '部门',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'email',
|
||||
label: '邮箱',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'phone',
|
||||
label: '手机号',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
type: 'textarea',
|
||||
},
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function useSearchFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
defaultValue: '',
|
||||
componentProps: {
|
||||
placeholder: '请输入姓名',
|
||||
},
|
||||
fieldName: 'displayName',
|
||||
label: '姓名',
|
||||
},
|
||||
{
|
||||
component: 'ApiSelect',
|
||||
fieldName: 'roleIds',
|
||||
defaultValue: [],
|
||||
componentProps: {
|
||||
api: getRoleOptions,
|
||||
multiple: true,
|
||||
clearable: true,
|
||||
labelField: 'roleName',
|
||||
valueField: 'id',
|
||||
},
|
||||
label: '角色',
|
||||
},
|
||||
{
|
||||
component: 'ApiSelect',
|
||||
fieldName: 'deptId',
|
||||
componentProps: {
|
||||
api: getDeptOptions,
|
||||
clearable: true,
|
||||
labelField: 'deptName',
|
||||
valueField: 'id',
|
||||
},
|
||||
label: '部门',
|
||||
},
|
||||
];
|
||||
}
|
||||
export function useGridSchema() {
|
||||
return [
|
||||
{
|
||||
field: 'status',
|
||||
slots: { default: 'status' },
|
||||
title: '状态',
|
||||
width: 120,
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
field: 'displayName',
|
||||
title: '姓名',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
field: 'phone',
|
||||
title: '手机号',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
field: 'email',
|
||||
title: '邮箱',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
title: '备注',
|
||||
},
|
||||
{
|
||||
field: 'roleIds',
|
||||
slots: { default: 'roleIds' },
|
||||
width: 200,
|
||||
title: '角色',
|
||||
},
|
||||
{
|
||||
field: 'deptId',
|
||||
slots: { default: 'deptId' },
|
||||
title: '部门',
|
||||
},
|
||||
{
|
||||
field: 'actions',
|
||||
title: '操作',
|
||||
fixed: 'right',
|
||||
width: 120,
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
73
apps/web-tdesign/src/views/system/admin/form.vue
Normal file
73
apps/web-tdesign/src/views/system/admin/form.vue
Normal file
@ -0,0 +1,73 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, nextTick, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { createAdmin, updateAdmin } from '#/api';
|
||||
|
||||
import { useSubmitFormSchema } from './data';
|
||||
|
||||
const emits = defineEmits(['success']);
|
||||
|
||||
const formData = ref();
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
schema: useSubmitFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const id = ref();
|
||||
const [Model, modelApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) return;
|
||||
const values = await formApi.getValues();
|
||||
modelApi.lock();
|
||||
(id.value
|
||||
? updateAdmin({
|
||||
id: id.value,
|
||||
...values,
|
||||
})
|
||||
: createAdmin(values)
|
||||
)
|
||||
.then(() => {
|
||||
emits('success');
|
||||
modelApi.close();
|
||||
})
|
||||
.catch(() => {
|
||||
modelApi.unlock();
|
||||
});
|
||||
},
|
||||
|
||||
async onOpenChange(isOpen) {
|
||||
if (isOpen) {
|
||||
const data = modelApi.getData();
|
||||
await formApi.resetForm();
|
||||
|
||||
if (data) {
|
||||
formData.value = data;
|
||||
id.value = data.id;
|
||||
} else {
|
||||
id.value = undefined;
|
||||
}
|
||||
|
||||
// Wait for Vue to flush DOM updates (form fields mounted)
|
||||
await nextTick();
|
||||
if (data) {
|
||||
await formApi.setValues(data);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const getDrawerTitle = computed(() => {
|
||||
return formData.value?.id ? '编辑用户' : '新建用户';
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Model :title="getDrawerTitle">
|
||||
<Form />
|
||||
</Model>
|
||||
</template>
|
||||
<style scoped></style>
|
||||
133
apps/web-tdesign/src/views/system/admin/index.vue
Normal file
133
apps/web-tdesign/src/views/system/admin/index.vue
Normal file
@ -0,0 +1,133 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { confirm, Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { changeAdminStatus, getAdminListByPage } from '#/api';
|
||||
import { toObject } from '#/enum';
|
||||
import { CommonStatusEnum } from '#/enum/CommonStatus';
|
||||
|
||||
import { useGridSchema, useSearchFormSchema } from './data';
|
||||
import Form from './form.vue';
|
||||
|
||||
const [FormModel, formModelApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
import { message } from '#/adapter/tdesign';
|
||||
|
||||
import { Button as TButton, Space as TSpace, Switch as TSwitch, Link as TLink } from 'tdesign-vue-next';
|
||||
|
||||
function onStatusChange(value: any, row: any) {
|
||||
confirm(`确认${value === 1 ? '启用' : '禁用'}用户`, `切换状态`)
|
||||
.then(async () => {
|
||||
await changeAdminStatus({ id: row.id, status: value });
|
||||
row.status = value;
|
||||
message.success(`${value === 1 ? '启用' : '禁用'}成功`);
|
||||
})
|
||||
.catch(() => {
|
||||
message.warning('取消操作');
|
||||
});
|
||||
}
|
||||
|
||||
const [Grid, GridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useSearchFormSchema(),
|
||||
showCollapseButton: false,
|
||||
submitButtonOptions: {
|
||||
content: '查询',
|
||||
},
|
||||
submitOnChange: true,
|
||||
submitOnEnter: true,
|
||||
},
|
||||
gridOptions: {
|
||||
minHeight: 600,
|
||||
columns: useGridSchema(),
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getAdminListByPage({
|
||||
current: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// 完全移除分隔条
|
||||
separator: false,
|
||||
// 你也可以使用下面的代码来移除分隔条
|
||||
// separator: { show: false },
|
||||
// 或者使用下面的代码来改变分隔条的颜色
|
||||
// separator: { backgroundColor: 'rgba(100,100,0,0.5)' },
|
||||
});
|
||||
|
||||
const addEvent = () => {
|
||||
formModelApi.setData({}).open();
|
||||
};
|
||||
const editEvent = (row) => {
|
||||
formModelApi.setData(row).open();
|
||||
};
|
||||
const onRefresh = () => {
|
||||
GridApi.reload();
|
||||
};
|
||||
|
||||
const roleOptionsMap = computed(() => {
|
||||
return toObject(
|
||||
GridApi.formApi.getFieldComponentRef('roleIds')?.getOptions() ?? [],
|
||||
'value',
|
||||
'label',
|
||||
);
|
||||
});
|
||||
const deptOptionsMap = computed(() => {
|
||||
return toObject(
|
||||
GridApi.formApi.getFieldComponentRef('deptId')?.getOptions() ?? [],
|
||||
'value',
|
||||
'label',
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<Grid table-title="用户列表">
|
||||
<template #toolbar-tools>
|
||||
<TButton class="mr-2" type="primary" @click="addEvent">
|
||||
新增用户
|
||||
</TButton>
|
||||
</template>
|
||||
|
||||
<template #status="{ row }">
|
||||
<TSwitch
|
||||
:value="row.status"
|
||||
:checked-value="true"
|
||||
:unchecked-value="false"
|
||||
:on-update:value="(value) => onStatusChange(value, row)"
|
||||
>
|
||||
{{ toObject(CommonStatusEnum, 'value', 'label')[row.status] }}
|
||||
</TSwitch>
|
||||
</template>
|
||||
|
||||
<template #roleIds="{ row }">
|
||||
<TSpace>
|
||||
<TTag v-for="id in row.roleIds" :key="id">
|
||||
{{ roleOptionsMap[id] }}
|
||||
</TTag>
|
||||
</TSpace>
|
||||
</template>
|
||||
|
||||
<template #deptId="{ row }"> {{ deptOptionsMap[row.deptId] }} </template>
|
||||
|
||||
<template #actions="{ row }">
|
||||
<TSpace>
|
||||
<TLink theme="primary" @click="editEvent(row)">编辑</TLink>
|
||||
</TSpace>
|
||||
</template>
|
||||
</Grid>
|
||||
|
||||
<FormModel @success="onRefresh" />
|
||||
</Page>
|
||||
</template>
|
||||
80
apps/web-tdesign/src/views/system/dept/data.ts
Normal file
80
apps/web-tdesign/src/views/system/dept/data.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
|
||||
import { getAdminOptions } from '#/api';
|
||||
import { getPointOptions } from '#/api/core/point';
|
||||
|
||||
export function useSubmitFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'deptName',
|
||||
label: '部门名称',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'ApiSelect',
|
||||
fieldName: 'adminIds',
|
||||
label: '用户',
|
||||
defaultValue: [],
|
||||
componentProps: {
|
||||
api: getAdminOptions,
|
||||
resultField: '',
|
||||
multiple: true,
|
||||
clearable: true,
|
||||
labelField: 'displayName',
|
||||
valueField: 'id',
|
||||
optionsPropName: 'options',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'ApiSelect',
|
||||
fieldName: 'pointIds',
|
||||
label: '观测点',
|
||||
defaultValue: [],
|
||||
componentProps: {
|
||||
api: getPointOptions,
|
||||
resultField: '',
|
||||
multiple: true,
|
||||
clearable: true,
|
||||
labelField: 'displayName',
|
||||
valueField: 'id',
|
||||
optionsPropName: 'options',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
componentProps: {
|
||||
type: 'textarea',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function useGridSchema() {
|
||||
return [
|
||||
{
|
||||
field: 'deptName',
|
||||
title: '部门名称',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
field: 'adminIds',
|
||||
title: '用户',
|
||||
slots: { default: 'adminIds' },
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
title: '备注',
|
||||
},
|
||||
{
|
||||
field: 'actions',
|
||||
title: '操作',
|
||||
fixed: 'right',
|
||||
width: 150,
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
70
apps/web-tdesign/src/views/system/dept/form.vue
Normal file
70
apps/web-tdesign/src/views/system/dept/form.vue
Normal file
@ -0,0 +1,70 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, nextTick, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { createDept, updateDept } from '#/api/core';
|
||||
|
||||
import { useSubmitFormSchema } from './data';
|
||||
|
||||
const emits = defineEmits(['success']);
|
||||
const id = ref();
|
||||
const formData = ref();
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
schema: useSubmitFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Model, modelApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) return;
|
||||
const values = await formApi.getValues();
|
||||
modelApi.lock();
|
||||
(id.value
|
||||
? updateDept({
|
||||
id: id.value,
|
||||
...values,
|
||||
})
|
||||
: createDept({
|
||||
...values,
|
||||
})
|
||||
)
|
||||
.then(() => {
|
||||
emits('success');
|
||||
modelApi.close();
|
||||
})
|
||||
.finally(() => modelApi.unlock());
|
||||
},
|
||||
|
||||
async onOpenChange(isOpen) {
|
||||
if (isOpen) {
|
||||
const data = modelApi.getData();
|
||||
await formApi.resetForm();
|
||||
|
||||
if (data) {
|
||||
formData.value = data;
|
||||
id.value = data.id;
|
||||
} else {
|
||||
id.value = undefined;
|
||||
}
|
||||
|
||||
// Wait for Vue to flush DOM updates (form fields mounted)
|
||||
await nextTick();
|
||||
if (data) {
|
||||
await formApi.setValues(data);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const getTitle = computed(() => (id.value ? '编辑角色' : '新增角色'));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Model :title="getTitle">
|
||||
<Form />
|
||||
</Model>
|
||||
</template>
|
||||
88
apps/web-tdesign/src/views/system/dept/index.vue
Normal file
88
apps/web-tdesign/src/views/system/dept/index.vue
Normal file
@ -0,0 +1,88 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { NButton, NSpace, NTag } from 'naive-ui';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getAdminOptions } from '#/api';
|
||||
import { getDeptList } from '#/api/core/dept';
|
||||
import { toObject } from '#/enum';
|
||||
|
||||
import { useGridSchema } from './data';
|
||||
import Form from './form.vue';
|
||||
|
||||
// 弹窗
|
||||
const [FormModel, formModelApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
// 表格
|
||||
const [Grid, GridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
minHeight: 600,
|
||||
columns: useGridSchema(),
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async () => await getDeptList(),
|
||||
},
|
||||
},
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
separator: false,
|
||||
});
|
||||
|
||||
// 刷新
|
||||
const refresh = () => GridApi.reload();
|
||||
|
||||
// 新增
|
||||
const addEvent = () => formModelApi.setData({}).open();
|
||||
|
||||
// 编辑
|
||||
const editEvent = (row) => formModelApi.setData(row).open();
|
||||
|
||||
const adminOptions = ref([]);
|
||||
|
||||
const queryAdminOptions = async () => {
|
||||
adminOptions.value = await getAdminOptions();
|
||||
};
|
||||
const adminOptionsMap = computed(() =>
|
||||
toObject(adminOptions.value ?? [], 'id', 'displayName'),
|
||||
);
|
||||
onMounted(() => {
|
||||
queryAdminOptions();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<Grid table-title="部门列表">
|
||||
<template #toolbar-tools>
|
||||
<NSpace>
|
||||
<NButton type="primary" @click="addEvent">新增部门</NButton>
|
||||
<NButton @click="refresh">查询</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<template #adminIds="{ row }">
|
||||
<NSpace>
|
||||
<NTag v-for="id in row.adminIds" :key="id">
|
||||
{{ adminOptionsMap[id] }}
|
||||
</NTag>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<template #actions="{ row }">
|
||||
<NSpace>
|
||||
<NButton text type="primary" @click="editEvent(row)">编辑</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
</Grid>
|
||||
|
||||
<FormModel @success="refresh" />
|
||||
</Page>
|
||||
</template>
|
||||
113
apps/web-tdesign/src/views/system/menu/data.ts
Normal file
113
apps/web-tdesign/src/views/system/menu/data.ts
Normal file
@ -0,0 +1,113 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
|
||||
import { getMenuTree, getRoleOptions } from '#/api';
|
||||
import { MenuTypeEnum } from '#/enum/MenuType';
|
||||
|
||||
export function useSubmitFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
fieldName: 'menuType',
|
||||
defaultValue: 1,
|
||||
componentProps: {
|
||||
options: MenuTypeEnum,
|
||||
},
|
||||
label: '权限类型',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'ApiTreeSelect',
|
||||
fieldName: 'parentId',
|
||||
label: '上级权限',
|
||||
componentProps: {
|
||||
api: getMenuTree,
|
||||
resultField: 'items',
|
||||
childrenField: 'children',
|
||||
labelField: 'menuName',
|
||||
valueField: 'id',
|
||||
clearable: true,
|
||||
},
|
||||
defaultValue: undefined,
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'menuName',
|
||||
label: '权限名称',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'menuCode',
|
||||
label: '权限编码',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'ApiSelect',
|
||||
fieldName: 'roleIds',
|
||||
componentProps: {
|
||||
api: getRoleOptions,
|
||||
multiple: true,
|
||||
clearable: true,
|
||||
labelField: 'roleName',
|
||||
valueField: 'id',
|
||||
},
|
||||
label: '角色',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
type: 'textarea',
|
||||
},
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function useSearchFormSchema(): VbenFormSchema[] {
|
||||
return [];
|
||||
}
|
||||
export function useGridSchema() {
|
||||
return [
|
||||
{
|
||||
field: 'status',
|
||||
slots: { default: 'status' },
|
||||
title: '状态',
|
||||
width: 120,
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
field: 'menuName',
|
||||
title: '权限名称',
|
||||
width: 200,
|
||||
treeNode: true,
|
||||
},
|
||||
{
|
||||
field: 'menuCode',
|
||||
title: '权限编码',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
field: 'menuType',
|
||||
title: '权限类型',
|
||||
slots: { default: 'menuType' },
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
title: '备注',
|
||||
},
|
||||
{
|
||||
field: 'roleIds',
|
||||
slots: { default: 'roleIds' },
|
||||
title: '角色',
|
||||
},
|
||||
{
|
||||
field: 'actions',
|
||||
title: '操作',
|
||||
fixed: 'right',
|
||||
width: 200,
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
73
apps/web-tdesign/src/views/system/menu/form.vue
Normal file
73
apps/web-tdesign/src/views/system/menu/form.vue
Normal file
@ -0,0 +1,73 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, nextTick, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { createMenu, updateMenu } from '#/api';
|
||||
|
||||
import { useSubmitFormSchema } from './data';
|
||||
|
||||
const emits = defineEmits(['success']);
|
||||
|
||||
const formData = ref();
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
schema: useSubmitFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const id = ref();
|
||||
const [Model, modelApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) return;
|
||||
const values = await formApi.getValues();
|
||||
modelApi.lock();
|
||||
(id.value
|
||||
? updateMenu({
|
||||
id: id.value,
|
||||
...values,
|
||||
})
|
||||
: createMenu(values)
|
||||
)
|
||||
.then(() => {
|
||||
emits('success');
|
||||
modelApi.close();
|
||||
})
|
||||
.catch(() => {
|
||||
modelApi.unlock();
|
||||
});
|
||||
},
|
||||
|
||||
async onOpenChange(isOpen) {
|
||||
if (isOpen) {
|
||||
const data = modelApi.getData();
|
||||
await formApi.resetForm();
|
||||
|
||||
if (data) {
|
||||
formData.value = data;
|
||||
id.value = data.id;
|
||||
} else {
|
||||
id.value = undefined;
|
||||
}
|
||||
|
||||
// Wait for Vue to flush DOM updates (form fields mounted)
|
||||
await nextTick();
|
||||
if (data) {
|
||||
await formApi.setValues(data);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const getDrawerTitle = computed(() => {
|
||||
return formData.value?.id ? '编辑用户' : '新建用户';
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Model :title="getDrawerTitle">
|
||||
<Form />
|
||||
</Model>
|
||||
</template>
|
||||
<style scoped></style>
|
||||
157
apps/web-tdesign/src/views/system/menu/index.vue
Normal file
157
apps/web-tdesign/src/views/system/menu/index.vue
Normal file
@ -0,0 +1,157 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
import { confirm, Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { NButton, NSpace, NSwitch, NTag, useMessage } from 'naive-ui';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { changeMenuStatus, getMenuList, getRoleOptions } from '#/api';
|
||||
import { toObject } from '#/enum';
|
||||
import { CommonStatusEnum } from '#/enum/CommonStatus';
|
||||
import { MenuTypeEnumMap } from '#/enum/MenuType';
|
||||
|
||||
import { useGridSchema, useSearchFormSchema } from './data';
|
||||
import Form from './form.vue';
|
||||
|
||||
const [FormModel, formModelApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const message = useMessage();
|
||||
|
||||
function onStatusChange(value: any, row: any) {
|
||||
confirm(`确认${value === 1 ? '启用' : '禁用'}权限`, `切换状态`)
|
||||
.then(async () => {
|
||||
await changeMenuStatus({ id: row.id, status: value });
|
||||
row.status = value;
|
||||
message.success(`${value === 1 ? '启用' : '禁用'}成功`);
|
||||
})
|
||||
.catch(() => {
|
||||
message.warning('取消操作');
|
||||
});
|
||||
}
|
||||
|
||||
const [Grid, GridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useSearchFormSchema(),
|
||||
showCollapseButton: false,
|
||||
submitButtonOptions: {
|
||||
content: '查询',
|
||||
},
|
||||
submitOnChange: true,
|
||||
submitOnEnter: true,
|
||||
},
|
||||
gridOptions: {
|
||||
minHeight: 600,
|
||||
columns: useGridSchema(),
|
||||
treeConfig: {
|
||||
transform: true,
|
||||
parentField: 'parentId',
|
||||
rowField: 'id',
|
||||
showIcon: true,
|
||||
expandAll: true,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async () => {
|
||||
return await getMenuList();
|
||||
},
|
||||
},
|
||||
},
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
// 完全移除分隔条
|
||||
separator: false,
|
||||
// 你也可以使用下面的代码来移除分隔条
|
||||
// separator: { show: false },
|
||||
// 或者使用下面的代码来改变分隔条的颜色
|
||||
// separator: { backgroundColor: 'rgba(100,100,0,0.5)' },
|
||||
});
|
||||
|
||||
const addEvent = () => {
|
||||
formModelApi.setData({}).open();
|
||||
};
|
||||
const addChildMenu = (row) => {
|
||||
formModelApi.setData({ parentId: row.id }).open();
|
||||
};
|
||||
const editEvent = (row) => {
|
||||
formModelApi.setData(row).open();
|
||||
};
|
||||
const onRefresh = () => {
|
||||
GridApi.reload();
|
||||
};
|
||||
|
||||
const roleOptions = ref([]);
|
||||
|
||||
const queryRoleOptions = async () => {
|
||||
roleOptions.value = await getRoleOptions();
|
||||
GridApi.formApi.updateSchema([
|
||||
{
|
||||
fieldName: 'roleIds',
|
||||
componentProps: {
|
||||
options: roleOptions.value,
|
||||
},
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const roleOptionsMap = computed(() =>
|
||||
toObject(roleOptions.value ?? [], 'id', 'roleName'),
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
queryRoleOptions();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<Grid table-title="权限列表">
|
||||
<template #toolbar-tools>
|
||||
<NButton class="mr-2" type="primary" @click="addEvent">
|
||||
新增权限
|
||||
</NButton>
|
||||
</template>
|
||||
|
||||
<template #status="{ row }">
|
||||
<NSwitch
|
||||
:value="row.status"
|
||||
:checked-value="true"
|
||||
:unchecked-value="false"
|
||||
:on-update:value="(value) => onStatusChange(value, row)"
|
||||
>
|
||||
{{ toObject(CommonStatusEnum, 'value', 'label')[row.status] }}
|
||||
</NSwitch>
|
||||
</template>
|
||||
|
||||
<template #menuType="{ row }">
|
||||
<NTag>
|
||||
{{ MenuTypeEnumMap[row.menuType] }}
|
||||
</NTag>
|
||||
</template>
|
||||
|
||||
<template #roleIds="{ row }">
|
||||
<NSpace>
|
||||
<NTag v-for="id in row.roleIds" :key="id">
|
||||
{{ roleOptionsMap[id] }}
|
||||
</NTag>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<template #actions="{ row }">
|
||||
<NSpace>
|
||||
<NButton text type="primary" @click="addChildMenu(row)">
|
||||
增加下级菜单
|
||||
</NButton>
|
||||
<NButton text type="primary" @click="editEvent(row)">编辑</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
</Grid>
|
||||
|
||||
<FormModel @success="onRefresh" />
|
||||
</Page>
|
||||
</template>
|
||||
99
apps/web-tdesign/src/views/system/role/data.ts
Normal file
99
apps/web-tdesign/src/views/system/role/data.ts
Normal file
@ -0,0 +1,99 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
|
||||
import { getAdminOptions, getMenuTree } from '#/api';
|
||||
|
||||
export function useSubmitFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'roleName',
|
||||
label: '角色名称',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'roleCode',
|
||||
label: '角色编码',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'ApiSelect',
|
||||
fieldName: 'adminIds',
|
||||
label: '用户',
|
||||
componentProps: {
|
||||
api: getAdminOptions,
|
||||
resultField: '',
|
||||
multiple: true,
|
||||
clearable: true,
|
||||
labelField: 'displayName',
|
||||
valueField: 'id',
|
||||
optionsPropName: 'options',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'ApiTreeSelect',
|
||||
// 对应组件的参数
|
||||
componentProps: {
|
||||
// 菜单接口
|
||||
api: getMenuTree,
|
||||
resultField: 'items',
|
||||
childrenField: 'children',
|
||||
labelField: 'menuName',
|
||||
valueField: 'id',
|
||||
maxTagCount: 'responsive',
|
||||
clearable: true,
|
||||
multiple: true,
|
||||
cascade: true,
|
||||
checkable: true,
|
||||
},
|
||||
fieldName: 'menuIds',
|
||||
label: '权限',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
componentProps: {
|
||||
type: 'textarea',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function useGridSchema() {
|
||||
return [
|
||||
{
|
||||
field: 'status',
|
||||
title: '状态',
|
||||
slots: { default: 'status' },
|
||||
width: 120,
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
field: 'roleName',
|
||||
title: '角色名称',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
field: 'roleCode',
|
||||
title: '角色编码',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
field: 'adminIds',
|
||||
title: '用户',
|
||||
slots: { default: 'adminIds' },
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
title: '备注',
|
||||
},
|
||||
{
|
||||
field: 'actions',
|
||||
title: '操作',
|
||||
fixed: 'right',
|
||||
width: 150,
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
78
apps/web-tdesign/src/views/system/role/form.vue
Normal file
78
apps/web-tdesign/src/views/system/role/form.vue
Normal file
@ -0,0 +1,78 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, nextTick, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { createRole, updateRole } from '#/api';
|
||||
|
||||
import { useSubmitFormSchema } from './data';
|
||||
|
||||
const emits = defineEmits(['success']);
|
||||
const id = ref();
|
||||
const formData = ref();
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
schema: useSubmitFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Model, modelApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) return;
|
||||
const values = await formApi.getValues();
|
||||
|
||||
const menuIds = formApi
|
||||
.getFieldComponentRef('menuIds')
|
||||
.getComponentRef()
|
||||
.getIndeterminateData().keys;
|
||||
|
||||
modelApi.lock();
|
||||
(id.value
|
||||
? updateRole({
|
||||
id: id.value,
|
||||
...values,
|
||||
menuIds: [...values.menuIds, ...menuIds],
|
||||
})
|
||||
: createRole({
|
||||
...values,
|
||||
menuIds: [...values.menuIds, ...menuIds],
|
||||
})
|
||||
)
|
||||
.then(() => {
|
||||
emits('success');
|
||||
modelApi.close();
|
||||
})
|
||||
.finally(() => modelApi.unlock());
|
||||
},
|
||||
|
||||
async onOpenChange(isOpen) {
|
||||
if (isOpen) {
|
||||
const data = modelApi.getData();
|
||||
await formApi.resetForm();
|
||||
|
||||
if (data) {
|
||||
formData.value = data;
|
||||
id.value = data.id;
|
||||
} else {
|
||||
id.value = undefined;
|
||||
}
|
||||
|
||||
// Wait for Vue to flush DOM updates (form fields mounted)
|
||||
await nextTick();
|
||||
if (data) {
|
||||
await formApi.setValues(data);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const getTitle = computed(() => (id.value ? '编辑角色' : '新增角色'));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Model :title="getTitle">
|
||||
<Form />
|
||||
</Model>
|
||||
</template>
|
||||
110
apps/web-tdesign/src/views/system/role/index.vue
Normal file
110
apps/web-tdesign/src/views/system/role/index.vue
Normal file
@ -0,0 +1,110 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
import { confirm, Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { NButton, NSpace, NSwitch, NTag, useMessage } from 'naive-ui';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { changeRoleStatus, getAdminOptions, getRoleList } from '#/api';
|
||||
import { toObject } from '#/enum';
|
||||
|
||||
import { useGridSchema } from './data';
|
||||
import Form from './form.vue';
|
||||
|
||||
const message = useMessage();
|
||||
|
||||
// 弹窗
|
||||
const [FormModel, formModelApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
// 表格
|
||||
const [Grid, GridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
minHeight: 600,
|
||||
columns: useGridSchema(),
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
// 关键:不做分页,一次性加载
|
||||
query: async () => await getRoleList(),
|
||||
},
|
||||
},
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
separator: false,
|
||||
});
|
||||
|
||||
// 刷新
|
||||
const refresh = () => GridApi.reload();
|
||||
|
||||
// 新增
|
||||
const addEvent = () => formModelApi.setData({}).open();
|
||||
|
||||
// 编辑
|
||||
const editEvent = (row) => formModelApi.setData(row).open();
|
||||
|
||||
// 状态切换
|
||||
const onStatusChange = (value, row) => {
|
||||
confirm(`确认${value ? '启用' : '禁用'}角色?`, '提示')
|
||||
.then(async () => {
|
||||
await changeRoleStatus({ id: row.id, status: value });
|
||||
row.status = value;
|
||||
message.success('状态更新成功');
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
const adminOptions = ref([]);
|
||||
|
||||
const queryAdminOptions = async () => {
|
||||
adminOptions.value = await getAdminOptions();
|
||||
};
|
||||
const adminOptionsMap = computed(() =>
|
||||
toObject(adminOptions.value ?? [], 'id', 'displayName'),
|
||||
);
|
||||
onMounted(() => {
|
||||
queryAdminOptions();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<Grid table-title="角色列表">
|
||||
<template #toolbar-tools>
|
||||
<NSpace>
|
||||
<NButton type="primary" @click="addEvent">新增角色</NButton>
|
||||
<NButton @click="refresh">查询</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<template #status="{ row }">
|
||||
<NSwitch
|
||||
:value="row.status"
|
||||
:checked-value="true"
|
||||
:unchecked-value="false"
|
||||
:on-update:value="(value) => onStatusChange(value, row)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #adminIds="{ row }">
|
||||
<NSpace>
|
||||
<NTag v-for="id in row.adminIds" :key="id">
|
||||
{{ adminOptionsMap[id] }}
|
||||
</NTag>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<template #actions="{ row }">
|
||||
<NSpace>
|
||||
<NButton text type="primary" @click="editEvent(row)">编辑</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
</Grid>
|
||||
|
||||
<FormModel @success="refresh" />
|
||||
</Page>
|
||||
</template>
|
||||
193
apps/web-tdesign/src/views/things/device/data.ts
Normal file
193
apps/web-tdesign/src/views/things/device/data.ts
Normal file
@ -0,0 +1,193 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
|
||||
import { getPointOptions } from '#/api';
|
||||
import { getCardOptions } from '#/api/core/card';
|
||||
import { getProductOptions } from '#/api/core/product';
|
||||
|
||||
export function useSubmitFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'ApiSelect',
|
||||
fieldName: 'pointId',
|
||||
componentProps: {
|
||||
api: getPointOptions,
|
||||
clearable: true,
|
||||
labelField: 'displayName',
|
||||
valueField: 'id',
|
||||
},
|
||||
label: '监测点',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'parentId',
|
||||
dependencies: {
|
||||
show: false,
|
||||
triggerFields: ['productIdSwitch'],
|
||||
},
|
||||
label: '父级设备',
|
||||
},
|
||||
{
|
||||
component: 'ApiSelect',
|
||||
fieldName: 'productId',
|
||||
componentProps: {
|
||||
api: getProductOptions,
|
||||
clearable: true,
|
||||
},
|
||||
label: '设备型号',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'displayName',
|
||||
componentProps: {
|
||||
clearable: true,
|
||||
},
|
||||
label: '设备名称',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'msgType',
|
||||
componentProps: {
|
||||
clearable: true,
|
||||
placeholder: '请选择设备消息类型',
|
||||
options: [
|
||||
{
|
||||
label: 'rs485ValueRpt',
|
||||
value: 'rs485ValueRpt',
|
||||
},
|
||||
{
|
||||
label: 'diValueRpt',
|
||||
value: 'diValueRpt',
|
||||
},
|
||||
{
|
||||
label: 'aiValueRpt',
|
||||
value: 'aiValueRpt',
|
||||
},
|
||||
],
|
||||
},
|
||||
label: '设备消息类型',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'sn',
|
||||
componentProps: {
|
||||
clearable: true,
|
||||
},
|
||||
label: '设备编码',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'card',
|
||||
componentProps: {
|
||||
clearable: true,
|
||||
},
|
||||
label: '设备卡号',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'longitude',
|
||||
componentProps: {
|
||||
clearable: true,
|
||||
},
|
||||
label: '经度',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'latitude',
|
||||
componentProps: {
|
||||
clearable: true,
|
||||
},
|
||||
label: '纬度',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function useSearchFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'ApiSelect',
|
||||
fieldName: 'pointId',
|
||||
componentProps: {
|
||||
api: getPointOptions,
|
||||
clearable: true,
|
||||
labelField: 'displayName',
|
||||
valueField: 'id',
|
||||
},
|
||||
label: '监测点',
|
||||
},
|
||||
{
|
||||
component: 'ApiSelect',
|
||||
fieldName: 'productId',
|
||||
componentProps: {
|
||||
api: getProductOptions,
|
||||
clearable: true,
|
||||
multiple: true,
|
||||
},
|
||||
label: '监测点',
|
||||
},
|
||||
{
|
||||
component: 'ApiSelect',
|
||||
fieldName: 'cardId',
|
||||
componentProps: {
|
||||
api: getCardOptions,
|
||||
clearable: true,
|
||||
labelField: 'cardNumber',
|
||||
valueField: 'id',
|
||||
},
|
||||
label: '卡号',
|
||||
},
|
||||
];
|
||||
}
|
||||
export function useGridSchema() {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '序号',
|
||||
width: 120,
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
field: 'pointId',
|
||||
title: '点位名称',
|
||||
slots: { default: 'pointId' },
|
||||
width: 300,
|
||||
treeNode: true,
|
||||
},
|
||||
{
|
||||
field: 'displayName',
|
||||
title: '设备名称',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
field: 'productId',
|
||||
title: '设备型号',
|
||||
slots: { default: 'productId' },
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
field: 'cardId',
|
||||
title: '设备卡号',
|
||||
slots: { default: 'cardId' },
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
field: 'longitude',
|
||||
title: '经度',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
field: 'latitude',
|
||||
title: '纬度',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
field: 'actions',
|
||||
title: '操作',
|
||||
fixed: 'right',
|
||||
width: 200,
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
75
apps/web-tdesign/src/views/things/device/form.vue
Normal file
75
apps/web-tdesign/src/views/things/device/form.vue
Normal file
@ -0,0 +1,75 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, nextTick, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { createDevice, updateDevice } from '#/api/core/device';
|
||||
|
||||
import { useSubmitFormSchema } from './data';
|
||||
|
||||
const emits = defineEmits(['success']);
|
||||
|
||||
const formData = ref();
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
schema: useSubmitFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const id = ref();
|
||||
const [Model, modelApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) return;
|
||||
const values = await formApi.getValues();
|
||||
modelApi.lock();
|
||||
(id.value
|
||||
? updateDevice({
|
||||
id: id.value,
|
||||
...values,
|
||||
})
|
||||
: createDevice({
|
||||
...values,
|
||||
})
|
||||
)
|
||||
.then(() => {
|
||||
emits('success');
|
||||
modelApi.close();
|
||||
})
|
||||
.catch(() => {
|
||||
modelApi.unlock();
|
||||
});
|
||||
},
|
||||
|
||||
async onOpenChange(isOpen) {
|
||||
if (isOpen) {
|
||||
const data = modelApi.getData();
|
||||
await formApi.resetForm();
|
||||
|
||||
if (data) {
|
||||
formData.value = data;
|
||||
id.value = data.id;
|
||||
} else {
|
||||
id.value = undefined;
|
||||
}
|
||||
|
||||
// Wait for Vue to flush DOM updates (form fields mounted)
|
||||
await nextTick();
|
||||
if (data) {
|
||||
await formApi.setValues(data);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const getDrawerTitle = computed(() => {
|
||||
return formData.value?.id ? '编辑设备' : '新建设备';
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Model :title="getDrawerTitle">
|
||||
<Form />
|
||||
</Model>
|
||||
</template>
|
||||
<style scoped></style>
|
||||
149
apps/web-tdesign/src/views/things/device/index.vue
Normal file
149
apps/web-tdesign/src/views/things/device/index.vue
Normal file
@ -0,0 +1,149 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { NButton, NSpace } from 'naive-ui';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getDeviceListByPage } from '#/api/core/device';
|
||||
import { toObject } from '#/enum';
|
||||
|
||||
import { useGridSchema, useSearchFormSchema } from './data';
|
||||
import Form from './form.vue';
|
||||
|
||||
const [FormModel, formModelApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const [Grid, GridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useSearchFormSchema(),
|
||||
showCollapseButton: false,
|
||||
submitButtonOptions: {
|
||||
content: '查询',
|
||||
},
|
||||
submitOnChange: true,
|
||||
submitOnEnter: true,
|
||||
},
|
||||
gridOptions: {
|
||||
minHeight: 600,
|
||||
columns: useGridSchema(),
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getDeviceListByPage({
|
||||
current: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
treeConfig: {
|
||||
parentField: 'parentId',
|
||||
rowField: 'id',
|
||||
childrenField: 'children',
|
||||
showIcon: true,
|
||||
expandAll: true,
|
||||
},
|
||||
},
|
||||
// 完全移除分隔条
|
||||
separator: false,
|
||||
// 你也可以使用下面的代码来移除分隔条
|
||||
// separator: { show: false },
|
||||
// 或者使用下面的代码来改变分隔条的颜色
|
||||
// separator: { backgroundColor: 'rgba(100,100,0,0.5)' },
|
||||
});
|
||||
|
||||
const addEvent = () => {
|
||||
formModelApi
|
||||
.setData({
|
||||
pointId:
|
||||
GridApi.formApi.getFieldComponentRef('pointId').getValue() ?? undefined,
|
||||
})
|
||||
.open();
|
||||
};
|
||||
const addChildEvent = (row) => {
|
||||
formModelApi
|
||||
.setData({
|
||||
parentId: row.id,
|
||||
pointId: row.pointId,
|
||||
})
|
||||
.open();
|
||||
};
|
||||
const editEvent = (row) => {
|
||||
formModelApi
|
||||
.setData({
|
||||
...row,
|
||||
card: row.cardId ? cardOptionsMap.value[row.cardId] : undefined,
|
||||
})
|
||||
.open();
|
||||
};
|
||||
const onRefresh = () => {
|
||||
GridApi.formApi.getFieldComponentRef('cardId')?.fetchApi();
|
||||
GridApi.reload();
|
||||
};
|
||||
|
||||
const pointOptionsMap = computed(() => {
|
||||
return toObject(
|
||||
GridApi.formApi.getFieldComponentRef('pointId')?.getOptions() ?? [],
|
||||
'value',
|
||||
'label',
|
||||
);
|
||||
});
|
||||
const productOptionsMap = computed(() => {
|
||||
return toObject(
|
||||
GridApi.formApi.getFieldComponentRef('productId')?.getOptions() ?? [],
|
||||
'value',
|
||||
'label',
|
||||
);
|
||||
});
|
||||
const cardOptionsMap = computed(() => {
|
||||
return toObject(
|
||||
GridApi.formApi.getFieldComponentRef('cardId')?.getOptions() ?? [],
|
||||
'value',
|
||||
'label',
|
||||
);
|
||||
});
|
||||
onMounted(() => {
|
||||
const route = useRoute();
|
||||
GridApi.formApi.setFieldValue('pointId', route.query.pointId);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<Grid table-title="监测点列表">
|
||||
<template #toolbar-tools>
|
||||
<NButton class="mr-2" type="primary" @click="addEvent">
|
||||
新增设备
|
||||
</NButton>
|
||||
</template>
|
||||
<template #pointId="{ row }">
|
||||
{{ pointOptionsMap[row.pointId] }}
|
||||
</template>
|
||||
|
||||
<template #productId="{ row }">
|
||||
{{ productOptionsMap[row.productId] }}
|
||||
</template>
|
||||
|
||||
<template #cardId="{ row }">
|
||||
{{ cardOptionsMap[row.cardId] }}
|
||||
</template>
|
||||
|
||||
<template #actions="{ row }">
|
||||
<NSpace>
|
||||
<NButton text type="primary" @click="editEvent(row)">编辑</NButton>
|
||||
<NButton text type="primary" @click="addChildEvent(row)">
|
||||
增加子设备
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
</Grid>
|
||||
|
||||
<FormModel @success="onRefresh" />
|
||||
</Page>
|
||||
</template>
|
||||
103
apps/web-tdesign/src/views/things/flow/data.ts
Normal file
103
apps/web-tdesign/src/views/things/flow/data.ts
Normal file
@ -0,0 +1,103 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
|
||||
import { getDeptOptions } from '#/api';
|
||||
import { SendHandlerEnum } from '#/enum/SendHandler';
|
||||
|
||||
export function useSubmitFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'deviceId',
|
||||
label: '设备id',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入同步key',
|
||||
},
|
||||
fieldName: 'syncKey',
|
||||
label: '同步key',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入同步密钥',
|
||||
},
|
||||
fieldName: 'syncSecret',
|
||||
label: '同步密钥',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请选择同步平台',
|
||||
options: SendHandlerEnum,
|
||||
clearable: true,
|
||||
},
|
||||
fieldName: 'sendHandlerClass',
|
||||
label: '同步平台',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function useSearchFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入观测点名称',
|
||||
},
|
||||
fieldName: 'displayName',
|
||||
label: '观测点名称',
|
||||
},
|
||||
{
|
||||
component: 'ApiSelect',
|
||||
fieldName: 'deptId',
|
||||
componentProps: {
|
||||
api: getDeptOptions,
|
||||
clearable: true,
|
||||
labelField: 'deptName',
|
||||
valueField: 'id',
|
||||
},
|
||||
label: '部门',
|
||||
},
|
||||
];
|
||||
}
|
||||
export function useGridSchema() {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '序号',
|
||||
width: 120,
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
field: 'displayName',
|
||||
title: '点位名称',
|
||||
slots: { default: 'displayName' },
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
field: 'locationName',
|
||||
title: '行政区划',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
field: 'deptIds',
|
||||
title: '部门',
|
||||
slots: { default: 'deptIds' },
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
title: '备注',
|
||||
},
|
||||
{
|
||||
field: 'actions',
|
||||
title: '操作',
|
||||
fixed: 'right',
|
||||
width: 120,
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
101
apps/web-tdesign/src/views/things/flow/form.vue
Normal file
101
apps/web-tdesign/src/views/things/flow/form.vue
Normal file
@ -0,0 +1,101 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, nextTick, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { areaList } from '@vant/area-data';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { createPoint, updatePoint } from '#/api';
|
||||
|
||||
import { useSubmitFormSchema } from './data';
|
||||
|
||||
const emits = defineEmits(['success']);
|
||||
|
||||
const formData = ref();
|
||||
|
||||
const codeToText = (code: string): string => {
|
||||
const { province_list, city_list, county_list } = areaList;
|
||||
if (!code) return '';
|
||||
|
||||
// 自动根据位数推断 (代码通常为6位)
|
||||
const provinceCode = `${code.slice(0, 2)}0000`;
|
||||
const cityCode = `${code.slice(0, 4)}00`;
|
||||
const countyCode = code;
|
||||
|
||||
const province = province_list[provinceCode] || '';
|
||||
const city = city_list[cityCode] || '';
|
||||
const county = county_list[countyCode] || '';
|
||||
|
||||
// 过滤空值并用空格拼接
|
||||
return [province, city, county].filter(Boolean).join(' ');
|
||||
};
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
schema: useSubmitFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const id = ref();
|
||||
const [Model, modelApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) return;
|
||||
const values = await formApi.getValues();
|
||||
|
||||
modelApi.lock();
|
||||
(id.value
|
||||
? updatePoint({
|
||||
id: id.value,
|
||||
...values,
|
||||
...(!!values.locationCode && {
|
||||
locationName: codeToText(values.locationCode),
|
||||
}),
|
||||
})
|
||||
: createPoint({
|
||||
...values,
|
||||
...(!!values.locationCode && {
|
||||
locationName: codeToText(values.locationCode),
|
||||
}),
|
||||
})
|
||||
)
|
||||
.then(() => {
|
||||
emits('success');
|
||||
modelApi.close();
|
||||
})
|
||||
.catch(() => {
|
||||
modelApi.unlock();
|
||||
});
|
||||
},
|
||||
|
||||
async onOpenChange(isOpen) {
|
||||
if (isOpen) {
|
||||
const data = modelApi.getData();
|
||||
await formApi.resetForm();
|
||||
|
||||
if (data) {
|
||||
formData.value = data;
|
||||
id.value = data.id;
|
||||
} else {
|
||||
id.value = undefined;
|
||||
}
|
||||
|
||||
// Wait for Vue to flush DOM updates (form fields mounted)
|
||||
await nextTick();
|
||||
if (data) {
|
||||
await formApi.setValues(data);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const getDrawerTitle = computed(() => {
|
||||
return formData.value?.id ? '编辑用户' : '新建用户';
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Model :title="getDrawerTitle">
|
||||
<Form />
|
||||
</Model>
|
||||
</template>
|
||||
<style scoped></style>
|
||||
113
apps/web-tdesign/src/views/things/flow/index.vue
Normal file
113
apps/web-tdesign/src/views/things/flow/index.vue
Normal file
@ -0,0 +1,113 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { NButton, NSpace, NTag } from 'naive-ui';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getPointListByPage } from '#/api';
|
||||
import { toObject } from '#/enum';
|
||||
import { router } from '#/router';
|
||||
|
||||
import { useGridSchema, useSearchFormSchema } from './data';
|
||||
import Form from './form.vue';
|
||||
|
||||
const [FormModel, formModelApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const [Grid, GridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useSearchFormSchema(),
|
||||
showCollapseButton: false,
|
||||
submitButtonOptions: {
|
||||
content: '查询',
|
||||
},
|
||||
submitOnChange: true,
|
||||
submitOnEnter: true,
|
||||
},
|
||||
gridOptions: {
|
||||
minHeight: 600,
|
||||
columns: useGridSchema(),
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getPointListByPage({
|
||||
current: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// 完全移除分隔条
|
||||
separator: false,
|
||||
// 你也可以使用下面的代码来移除分隔条
|
||||
// separator: { show: false },
|
||||
// 或者使用下面的代码来改变分隔条的颜色
|
||||
// separator: { backgroundColor: 'rgba(100,100,0,0.5)' },
|
||||
});
|
||||
|
||||
const addEvent = () => {
|
||||
formModelApi.setData({}).open();
|
||||
};
|
||||
const editEvent = (row) => {
|
||||
formModelApi.setData(row).open();
|
||||
};
|
||||
const onRefresh = () => {
|
||||
GridApi.reload();
|
||||
};
|
||||
|
||||
const deptOptionsMap = computed(() => {
|
||||
return toObject(
|
||||
GridApi.formApi.getFieldComponentRef('deptId')?.getOptions() ?? [],
|
||||
'value',
|
||||
'label',
|
||||
);
|
||||
});
|
||||
|
||||
const jumpToDevice = (row: any) => {
|
||||
router.push({
|
||||
name: 'Device',
|
||||
query: {
|
||||
pointId: row.id,
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<Grid table-title="监测点列表">
|
||||
<template #toolbar-tools>
|
||||
<NButton class="mr-2" type="primary" @click="addEvent">
|
||||
新增监测点
|
||||
</NButton>
|
||||
</template>
|
||||
|
||||
<template #displayName="{ row }">
|
||||
<NButton type="primary" text @click="jumpToDevice(row)">
|
||||
{{ row.displayName }}
|
||||
</NButton>
|
||||
</template>
|
||||
<template #deptIds="{ row }">
|
||||
<NSpace>
|
||||
<NTag v-for="id in row.deptIds" :key="id">
|
||||
{{ deptOptionsMap[id] }}
|
||||
</NTag>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<template #actions="{ row }">
|
||||
<NSpace>
|
||||
<NButton text type="primary" @click="editEvent(row)">编辑</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
</Grid>
|
||||
|
||||
<FormModel @success="onRefresh" />
|
||||
</Page>
|
||||
</template>
|
||||
113
apps/web-tdesign/src/views/things/point/data.ts
Normal file
113
apps/web-tdesign/src/views/things/point/data.ts
Normal file
@ -0,0 +1,113 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
|
||||
import { useCascaderAreaData } from '@vant/area-data';
|
||||
|
||||
import { getDeptOptions } from '#/api';
|
||||
|
||||
export function useSubmitFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Cascader',
|
||||
componentProps: {
|
||||
options: useCascaderAreaData(),
|
||||
labelField: 'text',
|
||||
valueField: 'value',
|
||||
filterable: true,
|
||||
clearable: true,
|
||||
placeholder: '请选择行政区划',
|
||||
},
|
||||
defaultValue: [],
|
||||
fieldName: 'locationCode',
|
||||
label: '行政区划',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'displayName',
|
||||
label: '观测点名称',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'ApiSelect',
|
||||
fieldName: 'deptIds',
|
||||
defaultValue: [],
|
||||
componentProps: {
|
||||
api: getDeptOptions,
|
||||
clearable: true,
|
||||
multiple: true,
|
||||
labelField: 'deptName',
|
||||
valueField: 'id',
|
||||
},
|
||||
label: '部门',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
type: 'textarea',
|
||||
},
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function useSearchFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入观测点名称',
|
||||
},
|
||||
fieldName: 'displayName',
|
||||
label: '观测点名称',
|
||||
},
|
||||
{
|
||||
component: 'ApiSelect',
|
||||
fieldName: 'deptId',
|
||||
componentProps: {
|
||||
api: getDeptOptions,
|
||||
clearable: true,
|
||||
labelField: 'deptName',
|
||||
valueField: 'id',
|
||||
},
|
||||
label: '部门',
|
||||
},
|
||||
];
|
||||
}
|
||||
export function useGridSchema() {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '序号',
|
||||
width: 120,
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
field: 'displayName',
|
||||
title: '点位名称',
|
||||
slots: { default: 'displayName' },
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
field: 'locationName',
|
||||
title: '行政区划',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
field: 'deptIds',
|
||||
title: '部门',
|
||||
slots: { default: 'deptIds' },
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
title: '备注',
|
||||
},
|
||||
{
|
||||
field: 'actions',
|
||||
title: '操作',
|
||||
fixed: 'right',
|
||||
width: 120,
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
101
apps/web-tdesign/src/views/things/point/form.vue
Normal file
101
apps/web-tdesign/src/views/things/point/form.vue
Normal file
@ -0,0 +1,101 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, nextTick, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { areaList } from '@vant/area-data';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { createPoint, updatePoint } from '#/api';
|
||||
|
||||
import { useSubmitFormSchema } from './data';
|
||||
|
||||
const emits = defineEmits(['success']);
|
||||
|
||||
const formData = ref();
|
||||
|
||||
const codeToText = (code: string): string => {
|
||||
const { province_list, city_list, county_list } = areaList;
|
||||
if (!code) return '';
|
||||
|
||||
// 自动根据位数推断 (代码通常为6位)
|
||||
const provinceCode = `${code.slice(0, 2)}0000`;
|
||||
const cityCode = `${code.slice(0, 4)}00`;
|
||||
const countyCode = code;
|
||||
|
||||
const province = province_list[provinceCode] || '';
|
||||
const city = city_list[cityCode] || '';
|
||||
const county = county_list[countyCode] || '';
|
||||
|
||||
// 过滤空值并用空格拼接
|
||||
return [province, city, county].filter(Boolean).join(' ');
|
||||
};
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
schema: useSubmitFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const id = ref();
|
||||
const [Model, modelApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) return;
|
||||
const values = await formApi.getValues();
|
||||
|
||||
modelApi.lock();
|
||||
(id.value
|
||||
? updatePoint({
|
||||
id: id.value,
|
||||
...values,
|
||||
...(!!values.locationCode && {
|
||||
locationName: codeToText(values.locationCode),
|
||||
}),
|
||||
})
|
||||
: createPoint({
|
||||
...values,
|
||||
...(!!values.locationCode && {
|
||||
locationName: codeToText(values.locationCode),
|
||||
}),
|
||||
})
|
||||
)
|
||||
.then(() => {
|
||||
emits('success');
|
||||
modelApi.close();
|
||||
})
|
||||
.catch(() => {
|
||||
modelApi.unlock();
|
||||
});
|
||||
},
|
||||
|
||||
async onOpenChange(isOpen) {
|
||||
if (isOpen) {
|
||||
const data = modelApi.getData();
|
||||
await formApi.resetForm();
|
||||
|
||||
if (data) {
|
||||
formData.value = data;
|
||||
id.value = data.id;
|
||||
} else {
|
||||
id.value = undefined;
|
||||
}
|
||||
|
||||
// Wait for Vue to flush DOM updates (form fields mounted)
|
||||
await nextTick();
|
||||
if (data) {
|
||||
await formApi.setValues(data);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const getDrawerTitle = computed(() => {
|
||||
return formData.value?.id ? '编辑用户' : '新建用户';
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Model :title="getDrawerTitle">
|
||||
<Form />
|
||||
</Model>
|
||||
</template>
|
||||
<style scoped></style>
|
||||
113
apps/web-tdesign/src/views/things/point/index.vue
Normal file
113
apps/web-tdesign/src/views/things/point/index.vue
Normal file
@ -0,0 +1,113 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { NButton, NSpace, NTag } from 'naive-ui';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getPointListByPage } from '#/api';
|
||||
import { toObject } from '#/enum';
|
||||
import { router } from '#/router';
|
||||
|
||||
import { useGridSchema, useSearchFormSchema } from './data';
|
||||
import Form from './form.vue';
|
||||
|
||||
const [FormModel, formModelApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const [Grid, GridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useSearchFormSchema(),
|
||||
showCollapseButton: false,
|
||||
submitButtonOptions: {
|
||||
content: '查询',
|
||||
},
|
||||
submitOnChange: true,
|
||||
submitOnEnter: true,
|
||||
},
|
||||
gridOptions: {
|
||||
minHeight: 600,
|
||||
columns: useGridSchema(),
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getPointListByPage({
|
||||
current: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// 完全移除分隔条
|
||||
separator: false,
|
||||
// 你也可以使用下面的代码来移除分隔条
|
||||
// separator: { show: false },
|
||||
// 或者使用下面的代码来改变分隔条的颜色
|
||||
// separator: { backgroundColor: 'rgba(100,100,0,0.5)' },
|
||||
});
|
||||
|
||||
const addEvent = () => {
|
||||
formModelApi.setData({}).open();
|
||||
};
|
||||
const editEvent = (row) => {
|
||||
formModelApi.setData(row).open();
|
||||
};
|
||||
const onRefresh = () => {
|
||||
GridApi.reload();
|
||||
};
|
||||
|
||||
const deptOptionsMap = computed(() => {
|
||||
return toObject(
|
||||
GridApi.formApi.getFieldComponentRef('deptId')?.getOptions() ?? [],
|
||||
'value',
|
||||
'label',
|
||||
);
|
||||
});
|
||||
|
||||
const jumpToDevice = (row: any) => {
|
||||
router.push({
|
||||
name: 'Device',
|
||||
query: {
|
||||
pointId: row.id,
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<Grid table-title="监测点列表">
|
||||
<template #toolbar-tools>
|
||||
<NButton class="mr-2" type="primary" @click="addEvent">
|
||||
新增监测点
|
||||
</NButton>
|
||||
</template>
|
||||
|
||||
<template #displayName="{ row }">
|
||||
<NButton type="primary" text @click="jumpToDevice(row)">
|
||||
{{ row.displayName }}
|
||||
</NButton>
|
||||
</template>
|
||||
<template #deptIds="{ row }">
|
||||
<NSpace>
|
||||
<NTag v-for="id in row.deptIds" :key="id">
|
||||
{{ deptOptionsMap[id] }}
|
||||
</NTag>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<template #actions="{ row }">
|
||||
<NSpace>
|
||||
<NButton text type="primary" @click="editEvent(row)">编辑</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
</Grid>
|
||||
|
||||
<FormModel @success="onRefresh" />
|
||||
</Page>
|
||||
</template>
|
||||
@ -10,7 +10,7 @@ export default defineConfig(async () => {
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||
// mock代理目标地址
|
||||
target: 'http://localhost:5320/api',
|
||||
target: 'http://localhost:7901/api',
|
||||
ws: true,
|
||||
},
|
||||
},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user