feat: 完善功能

This commit is contained in:
teo 2025-11-10 16:30:19 +08:00
parent a4aa133db5
commit 40803462ac
45 changed files with 2661 additions and 70 deletions

View File

@ -1,8 +1,8 @@
# 应用标题 # 应用标题
VITE_APP_TITLE=Vben Admin Tdesign VITE_APP_TITLE=星戎物联
# 应用命名空间用于缓存、store等功能的前缀确保隔离 # 应用命名空间用于缓存、store等功能的前缀确保隔离
VITE_APP_NAMESPACE=vben-web-tdesign VITE_APP_NAMESPACE=iot
# 对store进行加密的密钥在将store持久化到localStorage时会使用该密钥进行加密 # 对store进行加密的密钥在将store持久化到localStorage时会使用该密钥进行加密
VITE_APP_STORE_SECURE_KEY=please-replace-me-with-your-own-key VITE_APP_STORE_SECURE_KEY=xrit2doa1q3swe

View 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);
}

View File

@ -47,5 +47,5 @@ export async function logoutApi() {
* *
*/ */
export async function getAccessCodesApi() { export async function getAccessCodesApi() {
return requestClient.get<string[]>('/auth/codes'); return requestClient.post<string[]>('/auth/codes');
} }

View File

@ -0,0 +1,5 @@
import { requestClient } from '#/api/request';
export function getCardOptions() {
return requestClient.post('/card/options');
}

View 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);
}

View 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);
}

View File

@ -1,3 +1,6 @@
export * from './admin';
export * from './auth'; export * from './auth';
export * from './dept';
export * from './menu'; export * from './menu';
export * from './user'; export * from './point';
export * from './role';

View File

@ -1,10 +1,24 @@
import type { RouteRecordStringComponent } from '@vben/types';
import { requestClient } from '#/api/request'; import { requestClient } from '#/api/request';
/**
*
*/
export async function getAllMenusApi() { 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);
} }

View 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);
}

View File

@ -0,0 +1,5 @@
import { requestClient } from '#/api/request';
export function getProductOptions() {
return requestClient.post('/product/options');
}

View 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);
}

View File

@ -0,0 +1,10 @@
export const CommonStatusEnum = [
{
value: 1,
label: '启用',
},
{
value: 0,
label: '禁用',
},
];

View 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');

View 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');

View 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');

View 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');

View File

@ -0,0 +1,9 @@
import { toObject } from '#/enum/index';
export const SendHandlerEnum = [
{
label: '地灾平台',
value: 'DZPTSendHandler',
},
];
export const SendHandlerEnumMap = toObject(SendHandlerEnum, 'value', 'label');

View File

@ -0,0 +1,10 @@
import { toObject } from '#/enum/index';
export const SubjectEnum = [
{
value: 1,
label: '数学',
},
];
export const SubjectEnumMap = toObject(SubjectEnum, 'value', 'label');

View 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;
};

View 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;

View 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;

View File

@ -10,7 +10,7 @@ import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { notification } from '#/adapter/tdesign'; import { notification } from '#/adapter/tdesign';
import { getAccessCodesApi, getUserInfoApi, loginApi, logoutApi } from '#/api'; import { getAccessCodesApi, getAdminInfoApi, loginApi, logoutApi } from '#/api';
import { $t } from '#/locales'; import { $t } from '#/locales';
export const useAuthStore = defineStore('auth', () => { export const useAuthStore = defineStore('auth', () => {
@ -98,7 +98,7 @@ export const useAuthStore = defineStore('auth', () => {
async function fetchUserInfo() { async function fetchUserInfo() {
let userInfo: null | UserInfo = null; let userInfo: null | UserInfo = null;
userInfo = await getUserInfoApi(); userInfo = await getAdminInfoApi();
userStore.setUserInfo(userInfo); userStore.setUserInfo(userInfo);
return userInfo; return userInfo;
} }

View File

@ -1,10 +1,9 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { VbenFormSchema } from '@vben/common-ui'; 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 { $t } from '@vben/locales';
import { useAuthStore } from '#/store'; import { useAuthStore } from '#/store';
@ -13,58 +12,13 @@ defineOptions({ name: 'Login' });
const authStore = useAuthStore(); const authStore = useAuthStore();
const MOCK_USER_OPTIONS: BasicOption[] = [
{
label: 'Super',
value: 'vben',
},
{
label: 'Admin',
value: 'admin',
},
{
label: 'User',
value: 'jack',
},
];
const formSchema = computed((): VbenFormSchema[] => { const formSchema = computed((): VbenFormSchema[] => {
return [ 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', component: 'VbenInput',
componentProps: { componentProps: {
placeholder: $t('authentication.usernameTip'), 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', fieldName: 'username',
label: $t('authentication.username'), label: $t('authentication.username'),
rules: z.string().min(1, { message: $t('authentication.usernameTip') }), rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
@ -78,14 +32,6 @@ const formSchema = computed((): VbenFormSchema[] => {
label: $t('authentication.password'), label: $t('authentication.password'),
rules: z.string().min(1, { message: $t('authentication.passwordTip') }), 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> </script>
@ -94,6 +40,12 @@ const formSchema = computed((): VbenFormSchema[] => {
<AuthenticationLogin <AuthenticationLogin
:form-schema="formSchema" :form-schema="formSchema"
:loading="authStore.loginLoading" :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" @submit="authStore.authLogin"
/> />
</template> </template>

View 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' },
},
];
}

View 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>

View 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>

View 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' },
},
];
}

View 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>

View 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>

View 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' },
},
];
}

View 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>

View 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>

View 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' },
},
];
}

View 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>

View 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>

View 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' },
},
];
}

View 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>

View 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>

View 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' },
},
];
}

View 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>

View 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>

View 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' },
},
];
}

View 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>

View 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>

View File

@ -10,7 +10,7 @@ export default defineConfig(async () => {
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''), rewrite: (path) => path.replace(/^\/api/, ''),
// mock代理目标地址 // mock代理目标地址
target: 'http://localhost:5320/api', target: 'http://localhost:7901/api',
ws: true, ws: true,
}, },
}, },