清理不必要文件

This commit is contained in:
BBIT-Kai
2025-12-29 16:27:22 +08:00
parent d178f5a057
commit e6a9a09492
1489 changed files with 0 additions and 134359 deletions
-224
View File
@@ -1,224 +0,0 @@
import { type VbenFormSchema, z } from "#/adapter/form";
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemUserApi } from '#/api';
import * as api from '#/api';
import { $t } from "@vben/locales";
export function useFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'name',
label: '设备账号',
rules: z
.string({
required_error: $t('ui.formRules.required', ['设备账号']),
invalid_type_error: $t('ui.formRules.required', ['设备账号']),
})
.min(4, $t('ui.formRules.minLength', ['设备账号', 4]))
.max(20, $t('ui.formRules.maxLength', ['设备账号', 20])),
componentProps: {
allowClear: true,
},
},
{
component: 'InputPassword',
fieldName: 'password',
label: '密码',
description: '123456',
},
{
component: 'ApiTreeSelect',
componentProps: {
allowClear: true,
api: api.getDeptList,
class: 'w-full',
labelField: 'name',
valueField: 'id',
childrenField: 'children',
},
fieldName: 'dept_id',
label: '所属组织',
rules: 'required',
},
{
component: 'Textarea',
fieldName: 'remark',
label: '备注',
componentProps: {
allowClear: true,
},
},
{
component: 'RadioGroup',
componentProps: {
buttonStyle: 'solid',
options: [
{ label: '可用', value: 1 },
{ label: '不可用', value: 0 },
],
optionType: 'button',
},
defaultValue: 1,
fieldName: 'status',
label: '可用性',
},
{
component: 'RadioGroup',
componentProps: {
buttonStyle: 'solid',
options: [
{ label: '是', value: 1 },
{ label: '否', value: 0 },
],
optionType: 'button',
},
defaultValue: 0,
fieldName: 'is_superuser',
label: '管理员',
},
];
}
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'id',
label: '设备编号',
componentProps: {
allowClear: true,
},
},
{
component: 'Input',
fieldName: 'name',
label: '设备名称',
componentProps: {
allowClear: true,
},
// componentProps: {
// readonly: true, // 如果组件支持 readonly
// },
},
{
component: 'Select',
componentProps: {
allowClear: true,
options: [
{ label: '启用中', value: 1 },
{ label: '已禁用', value: 0 },
],
},
fieldName: 'status',
label: '可用性',
},
{
component: 'Select',
componentProps: {
allowClear: true,
options: [
{ label: '是', value: 1 },
{ label: '否', value: 0 },
],
},
fieldName: 'is_superuser',
label: '管理员',
},
{
component: 'ApiTreeSelect',
componentProps: {
allowClear: true,
api: api.getDeptList,
class: 'w-full',
labelField: 'name',
valueField: 'id',
childrenField: 'children',
},
fieldName: 'dept_id',
label: '所属组织',
},
{
component: 'RangePicker',
fieldName: 'createTime',
label: '创建时间',
},
];
}
export function useColumns<T = SystemUserApi.SystemUser>(
onActionClick: OnActionClickFn<T>,
onStatusChange?: (newStatus: any, row: T) => PromiseLike<boolean | undefined>,
onIsSuperUserChange?: (
newStatus: any,
row: T,
) => PromiseLike<boolean | undefined>,
): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
title: '设备编号',
width: 100,
},
{
field: 'online',
slots: { default: 'status' },
title: '当前状态',
width: 100,
},
{
field: 'name',
title: '设备账号',
width: 150,
},
{
cellRender: {
attrs: { beforeChange: onStatusChange },
name: onStatusChange ? 'CellSwitch' : 'CellTag',
},
field: 'status',
title: '可用性',
width: 100,
},
{
cellRender: {
attrs: { beforeChange: onIsSuperUserChange },
name: onIsSuperUserChange ? 'CellSwitch' : 'CellTag',
},
field: 'is_superuser',
title: '管理员',
width: 100,
},
{
field: 'dept_name',
minWidth: 120,
title: '所属组织',
},
{
field: 'created_at',
title: '创建时间',
width: 200,
},
{
field: 'remark',
title: '备注',
width: 200,
},
{
align: 'center',
cellRender: {
attrs: {
nameField: 'name',
nameTitle: '设备名称',
onClick: onActionClick,
},
name: 'CellOperation',
},
field: 'operation',
fixed: 'right',
title: '操作',
width: 130,
},
];
}
-126
View File
@@ -1,126 +0,0 @@
<script lang="ts" setup>
import type { DataNode } from 'ant-design-vue/es/tree';
import type { Recordable } from '@vben/types';
import type { SystemUserApi } from '#/api';
import { computed, nextTick, ref } from 'vue';
import { Tree, useVbenDrawer } from '@vben/common-ui';
import { Spin } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createDevice, updateDevice } from '#/api';
import { $t } from '#/locales';
import { useFormSchema } from './data';
const emits = defineEmits(['success']);
const formData = ref<SystemUserApi.SystemUser>();
const [Form, formApi] = useVbenForm({
schema: useFormSchema(),
showDefaultActions: false,
});
const roles = ref<DataNode[]>([]);
const loadingRoles = ref(false);
const id = ref();
const [Drawer, drawerApi] = useVbenDrawer({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) return;
const values = await formApi.getValues();
drawerApi.lock();
(id.value ? updateDevice(id.value, values) : createDevice(values))
.then(() => {
emits('success');
drawerApi.close();
})
.catch(() => {
drawerApi.unlock();
});
},
async onOpenChange(isOpen) {
if (isOpen) {
const data = drawerApi.getData<SystemUserApi.SystemUser>();
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) {
formApi.setValues(data);
}
}
},
});
const getDrawerTitle = computed(() => {
return formData.value?.id
? $t('common.edit', $t('system.role.name'))
: $t('common.create', $t('system.role.name'));
});
function getNodeClass(node: Recordable<any>) {
const classes: string[] = [];
if (node.value?.type === 'button') {
classes.push('inline-flex');
}
return classes.join(' ');
}
</script>
<template>
<Drawer :title="getDrawerTitle">
<Form>
<template #roles="slotProps">
<Spin :spinning="loadingRoles" wrapper-class-name="w-full">
<Tree
:tree-data="roles"
multiple
bordered
:default-expanded-level="2"
:get-node-class="getNodeClass"
v-bind="slotProps"
value-field="id"
label-field="title"
>
<template #node="{ value }">
{{ $t(value.title) }}
</template>
</Tree>
</Spin>
</template>
</Form>
</Drawer>
</template>
<style lang="css" scoped>
:deep(.ant-tree-title) {
.tree-actions {
display: none;
margin-left: 20px;
}
}
:deep(.ant-tree-title:hover) {
.tree-actions {
display: flex;
flex: auto;
justify-content: flex-end;
margin-left: 20px;
}
}
</style>
-213
View File
@@ -1,213 +0,0 @@
<script lang="ts" setup>
import type { Recordable } from '@vben/types';
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { SystemDeviceApi } from '#/api';
import { onBeforeUnmount, onMounted, reactive } from 'vue';
import { Page, useVbenDrawer } from '@vben/common-ui';
import { Plus } from '@vben/icons';
import { Button, message, Modal, Tag } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteDevice, getDeviceList, updateDevicePatch } from '#/api';
import { useColumns, useGridFormSchema } from './data';
import Form from './form.vue';
const [FormDrawer, formDrawerApi] = useVbenDrawer({
connectedComponent: Form,
destroyOnClose: true,
});
/**
* 状态开关即将改变
* @param newStatus 期望改变的状态值
* @param row 行数据
* @returns 返回false则中止改变,返回其他值(undefined、true)则允许改变
*/
async function onStatusChange(
newStatus: number,
row: SystemDeviceApi.SystemDevice,
) {
const status: Recordable<string> = {
0: '禁用',
1: '启用',
};
try {
await confirm(
`你要将${row.name}的状态切换为 【${status[newStatus.toString()]}】 吗?`,
`切换状态`,
);
await updateDevicePatch(row.id, { status: newStatus });
return true;
} catch {
return false;
}
}
async function onIsSuperUserChange(
newStatus: number,
row: SystemDeviceApi.SystemDevice,
) {
const status: Recordable<string> = {
0: '普通用户',
1: '超级用户',
};
try {
await confirm(
`你要将${row.name}切换为 【${status[newStatus.toString()]}】 吗?`,
`切换状态`,
);
await updateDevicePatch(row.id, { is_superuser: newStatus });
return true;
} catch {
return false;
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
fieldMappingTime: [['createTime', ['startTime', 'endTime']]],
schema: useGridFormSchema(),
submitOnChange: true,
},
gridOptions: {
columns: useColumns(onActionClick, onStatusChange, onIsSuperUserChange),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
const res = await getDeviceList({
page: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
return {
items: res.list,
total: res.total,
};
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
custom: true,
export: false,
refresh: true,
search: true,
zoom: true,
},
} as VxeTableGridOptions<SystemDeviceApi.SystemDevice>,
});
function onActionClick(e: OnActionClickParams<SystemDeviceApi.SystemDevice>) {
switch (e.code) {
case 'delete': {
onDelete(e.row);
break;
}
case 'edit': {
onEdit(e.row);
break;
}
}
}
/**
* 将Antd的Modal.confirm封装为promise,方便在异步函数中调用。
* @param content 提示内容
* @param title 提示标题
*/
function confirm(content: string, title: string) {
return new Promise((reslove, reject) => {
Modal.confirm({
content,
onCancel() {
reject(new Error('已取消'));
},
onOk() {
reslove(true);
},
title,
});
});
}
function onEdit(row: SystemDeviceApi.SystemDevice) {
formDrawerApi.setData(row).open();
}
function onDelete(row: SystemDeviceApi.SystemDevice) {
const hideLoading = message.loading({
content: `正在删除设备:${[row.name]}`,
duration: 0,
key: 'action_process_msg',
});
deleteDevice(row.id)
.then(() => {
message.success({
content: `已删除设备:${[row.name]}`,
key: 'action_process_msg',
});
onRefresh();
})
.catch(() => {
hideLoading();
});
}
function onRefresh() {
gridApi.query();
}
function onCreate() {
formDrawerApi.setData({}).open();
}
const wsState = reactive({
ws: null as null | WebSocket,
});
onMounted(() => {
const ws = new WebSocket('wss://ai.ronsunny.cn:8090/ai/iot/ws/device-status');
wsState.ws = ws;
ws.onmessage = async (e) => {
const msg = JSON.parse(e.data);
console.log('WS 事件', msg);
// 简单策略:任何设备上下线都刷新列表 后续改进针对单行
await gridApi.query();
};
});
onBeforeUnmount(() => {
if (wsState.ws) wsState.ws.close();
});
</script>
<template>
<Page auto-content-height>
<FormDrawer @success="onRefresh" />
<Grid :table-title="设备列表">
<template #toolbar-tools>
<Button type="primary" @click="onCreate">
<Plus class="size-5" />
新增设备
</Button>
</template>
<template #status="{ row }">
<Tag v-if="row.online" style="color: green">在线</Tag>
<Tag v-else style="color: gray">离线</Tag>
</template>
</Grid>
</Page>
</template>
@@ -1,477 +0,0 @@
<script lang="ts" setup>
import type { ChangeEvent } from 'ant-design-vue/es/_util/EventInterface';
import type { Recordable } from '@vben/types';
import type { VbenFormSchema } from '#/adapter/form';
import { computed, h, ref } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import { $t, $te } from '@vben/locales';
import { getPopupContainer } from '@vben/utils';
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core';
import { useVbenForm, z } from '#/adapter/form';
import {
createMenu,
getMenuListForAll,
isMenuNameExists,
isMenuPathExists,
SystemMenuApi,
updateMenu,
} from '#/api';
import { componentKeys } from '#/router/routes';
import { getMenuTypeOptions } from './data';
const emit = defineEmits<{
success: [];
}>();
const formData = ref<SystemMenuApi.SystemMenu>();
const titleSuffix = ref<string>();
const schema: VbenFormSchema[] = [
{
component: 'RadioGroup',
componentProps: {
buttonStyle: 'solid',
options: getMenuTypeOptions(),
optionType: 'button',
},
defaultValue: 'menu',
fieldName: 'type',
formItemClass: 'col-span-2 md:col-span-2',
label: '类型',
},
{
component: 'Input',
fieldName: 'name',
label: 'ID名',
rules: z
.string()
.min(2, 'ID名至少2个字符')
.max(30, 'ID名最多30个字符')
.refine(
async (value: string) => {
return !(await isMenuNameExists(value, formData.value?.id));
},
(value) => ({
message: `ID名${value}已存在`,
}),
),
},
{
component: 'ApiTreeSelect',
componentProps: {
api: () => getMenuListForAll(1),
class: 'w-full',
filterTreeNode(input: string, node: Recordable<any>) {
if (!input || input.length === 0) {
return true;
}
const title: string = node.meta?.title ?? '';
if (!title) return false;
return title.includes(input) || $t(title).includes(input);
},
getPopupContainer,
labelField: 'meta.title',
showSearch: true,
treeDefaultExpandAll: true,
valueField: 'id',
childrenField: 'children',
},
fieldName: 'pid',
label: '上级菜单',
renderComponentContent() {
return {
title({ label, meta }: { label: string; meta: Recordable<any> }) {
const coms = [];
if (!label) return '';
if (meta?.icon) {
coms.push(h(IconifyIcon, { class: 'size-4', icon: meta.icon }));
}
coms.push(h('span', { class: '' }, $t(label || '')));
return h('div', { class: 'flex items-center gap-1' }, coms);
},
};
},
},
{
component: 'Input',
componentProps() {
// 不需要处理多语言时就无需这么做
return {
...(titleSuffix.value && { addonAfter: titleSuffix.value }),
onChange({ target: { value } }: ChangeEvent) {
titleSuffix.value = value && $te(value) ? $t(value) : undefined;
},
};
},
fieldName: 'meta.title',
label: '标题',
rules: 'required',
},
{
component: 'Input',
dependencies: {
show: (values) => {
return ['catalog', 'embedded', 'menu'].includes(values.type);
},
triggerFields: ['type'],
},
fieldName: 'path',
label: '路由地址',
rules: z
.string()
.min(2, '路由地址最少2个字符')
.max(100, '路由地址最多100个字符')
.refine((value: string) => {
return value.startsWith('/');
}, '路由地址必须以/开头')
.refine(
async (value: string) => {
return !(await isMenuPathExists(value, formData.value?.id));
},
(value) => ({
message: `路由地址${value}已存在`,
}),
),
},
{
component: 'Input',
dependencies: {
show: (values) => {
return ['embedded', 'menu'].includes(values.type);
},
triggerFields: ['type'],
},
fieldName: 'activePath',
help: '跳转到当前路由时,需要激活的菜单路径。 当不在导航菜单中显示时,需要指定激活路径',
label: '激活路径',
rules: z
.string()
.min(2, '路由地址最少2个字符')
.max(100, '路由地址最多100个字符')
.refine((value: string) => {
return value.startsWith('/');
}, '路由地址必须以/开头')
.refine(async (value: string) => {
return await isMenuPathExists(value, formData.value?.id);
}, '激活路径已存在')
.optional(),
},
{
component: 'IconPicker',
componentProps: {
prefix: 'carbon',
},
dependencies: {
show: (values) => {
return ['catalog', 'embedded', 'link', 'menu'].includes(values.type);
},
triggerFields: ['type'],
},
fieldName: 'meta.icon',
label: '菜单图标',
rules: 'required',
},
{
component: 'IconPicker',
componentProps: {
prefix: 'carbon',
},
dependencies: {
show: (values) => {
return ['catalog', 'embedded', 'menu'].includes(values.type);
},
triggerFields: ['type'],
},
fieldName: 'meta.activeIcon',
label: '激活图标',
},
{
component: 'AutoComplete',
componentProps: {
allowClear: true,
class: 'w-full',
filterOption(input: string, option: { value: string }) {
return option.value.toLowerCase().includes(input.toLowerCase());
},
options: componentKeys.map((v) => ({ value: v })),
},
dependencies: {
rules: (values) => {
return values.type === 'menu' ? 'required' : null;
},
show: (values) => {
return values.type === 'menu';
},
triggerFields: ['type'],
},
fieldName: 'component',
label: '页面组件',
},
{
component: 'Input',
dependencies: {
show: (values) => {
return ['embedded', 'link'].includes(values.type);
},
triggerFields: ['type'],
},
fieldName: 'linkSrc',
label: '链接地址',
help: '包含协议,例如https://',
rules: z.string().url('请输入有效的链接'),
},
{
component: 'Input',
dependencies: {
rules: (values) => {
return values.type === 'button' ? 'required' : null;
},
show: (values) => {
return ['button', 'catalog', 'embedded', 'menu'].includes(values.type);
},
triggerFields: ['type'],
},
fieldName: 'authCode',
label: '权限标识',
},
{
component: 'Select',
componentProps: {
allowClear: true,
class: 'w-full',
options: [
{ label: '红点', value: 'dot' },
{ label: '文字', value: 'normal' },
],
},
dependencies: {
show: (values) => {
return values.type !== 'button';
},
triggerFields: ['type'],
},
fieldName: 'meta.badgeType',
label: '徽章标题',
},
{
component: 'Input',
componentProps: (values) => {
return {
allowClear: true,
class: 'w-full',
disabled: values.meta?.badgeType !== 'normal',
};
},
dependencies: {
show: (values) => {
return values.type !== 'button';
},
triggerFields: ['type'],
},
fieldName: 'meta.badge',
label: '徽章内容',
},
{
component: 'Select',
componentProps: {
allowClear: true,
class: 'w-full',
options: SystemMenuApi.BadgeVariants.map((v) => ({
label: v,
value: v,
})),
},
dependencies: {
show: (values) => {
return values.type !== 'button';
},
triggerFields: ['type'],
},
fieldName: 'meta.badgeVariants',
label: '徽章样式',
},
{
component: 'Divider',
dependencies: {
show: (values) => {
return !['button', 'link'].includes(values.type);
},
triggerFields: ['type'],
},
fieldName: 'divider1',
formItemClass: 'col-span-2 md:col-span-2 pb-0',
hideLabel: true,
renderComponentContent() {
return {
default: () => '高级设置',
};
},
},
{
component: 'Checkbox',
dependencies: {
show: (values) => {
return ['menu'].includes(values.type);
},
triggerFields: ['type'],
},
fieldName: 'meta.keepAlive',
renderComponentContent() {
return {
default: () => '缓存标签页',
};
},
},
{
component: 'Checkbox',
dependencies: {
show: (values) => {
return ['embedded', 'menu'].includes(values.type);
},
triggerFields: ['type'],
},
fieldName: 'meta.affixTab',
renderComponentContent() {
return {
default: () => '固定在标签',
};
},
},
{
component: 'Checkbox',
dependencies: {
show: (values) => {
return !['button'].includes(values.type);
},
triggerFields: ['type'],
},
fieldName: 'meta.hideInMenu',
renderComponentContent() {
return {
default: () => '隐藏菜单',
};
},
},
{
component: 'Checkbox',
dependencies: {
show: (values) => {
return ['catalog', 'menu'].includes(values.type);
},
triggerFields: ['type'],
},
fieldName: 'meta.hideChildrenInMenu',
renderComponentContent() {
return {
default: () => '隐藏子菜单',
};
},
},
{
component: 'Checkbox',
dependencies: {
show: (values) => {
return !['button', 'link'].includes(values.type);
},
triggerFields: ['type'],
},
fieldName: 'meta.hideInBreadcrumb',
renderComponentContent() {
return {
default: () => '在面包屑中隐藏',
};
},
},
{
component: 'Checkbox',
dependencies: {
show: (values) => {
return !['button', 'link'].includes(values.type);
},
triggerFields: ['type'],
},
fieldName: 'meta.hideInTab',
renderComponentContent() {
return {
default: () => '在标签页中隐藏',
};
},
},
];
const breakpoints = useBreakpoints(breakpointsTailwind);
const isHorizontal = computed(() => breakpoints.greaterOrEqual('md').value);
const [Form, formApi] = useVbenForm({
commonConfig: {
colon: true,
formItemClass: 'col-span-2 md:col-span-1',
},
schema,
showDefaultActions: false,
wrapperClass: 'grid-cols-2 gap-x-4',
});
const [Drawer, drawerApi] = useVbenDrawer({
onConfirm: onSubmit,
onOpenChange(isOpen) {
if (isOpen) {
const data = drawerApi.getData<SystemMenuApi.SystemMenu>();
if (data?.type === 'link') {
data.linkSrc = data.meta?.link;
} else if (data?.type === 'embedded') {
data.linkSrc = data.meta?.iframeSrc;
}
if (data) {
formData.value = data;
formApi.setValues(formData.value);
titleSuffix.value = formData.value.meta?.title
? $t(formData.value.meta.title)
: '';
} else {
formApi.resetForm();
titleSuffix.value = '';
}
}
},
});
async function onSubmit() {
const { valid } = await formApi.validate();
if (valid) {
drawerApi.lock();
const data =
await formApi.getValues<
Omit<SystemMenuApi.SystemMenu, 'children' | 'id'>
>();
if (data.type === 'link') {
data.meta = { ...data.meta, link: data.linkSrc };
} else if (data.type === 'embedded') {
data.meta = { ...data.meta, iframeSrc: data.linkSrc };
}
delete data.linkSrc;
try {
await (formData.value?.id
? updateMenu(formData.value.id, data)
: createMenu(1,data));
drawerApi.close();
emit('success');
} finally {
drawerApi.unlock();
}
}
}
const getDrawerTitle = computed(() =>
formData.value?.id ? '修改菜单' : '新增菜单',
);
</script>
<template>
<Drawer class="w-full max-w-[800px]" :title="getDrawerTitle">
<Form class="mx-4" :layout="isHorizontal ? 'horizontal' : 'vertical'" />
</Drawer>
</template>
@@ -1,162 +0,0 @@
<script lang="ts" setup>
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import { Page, useVbenDrawer } from '@vben/common-ui';
import { IconifyIcon, Plus } from '@vben/icons';
import { $t } from '@vben/locales';
import { Button, message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import * as api from '#/api';
import { MenuBadge } from './components';
import { useColumns } from './data';
import Form from './form2.vue';
const [FormDrawer, formDrawerApi] = useVbenDrawer({
connectedComponent: Form,
destroyOnClose: true,
});
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: useColumns(onActionClick),
height: 'auto',
keepSource: true,
pagerConfig: {
enabled: false,
},
proxyConfig: {
ajax: {
query: async (_params) => {
return { items: await api.getMenuListForAll(1) };
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
custom: true,
export: false,
refresh: true,
zoom: true,
},
treeConfig: {
parentField: 'pid',
rowField: 'id',
transform: false,
},
} as VxeTableGridOptions,
});
function onActionClick({
code,
row,
}: OnActionClickParams<api.SystemMenuApi.SystemMenu>) {
switch (code) {
case 'append': {
onAppend(row);
break;
}
case 'delete': {
onDelete(row);
break;
}
case 'edit': {
onEdit(row);
break;
}
default: {
break;
}
}
}
function onRefresh() {
gridApi.query();
}
function onEdit(row: api.SystemMenuApi.SystemMenu) {
formDrawerApi.setData(row).open();
}
function onCreate() {
formDrawerApi.setData({}).open();
}
function onAppend(row: api.SystemMenuApi.SystemMenu) {
formDrawerApi.setData({ pid: row.id }).open();
}
function onDelete(row: api.SystemMenuApi.SystemMenu) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
key: 'action_process_msg',
});
api
.deleteMenu(row.id)
.then(() => {
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_process_msg',
});
onRefresh();
})
.catch(() => {
hideLoading();
});
}
</script>
<template>
<Page auto-content-height>
<FormDrawer @success="onRefresh" />
<Grid>
<template #toolbar-tools>
<Button type="primary" @click="onCreate">
<Plus class="size-5" />
新增菜单
</Button>
</template>
<template #title="{ row }">
<div class="flex w-full items-center gap-1">
<div class="size-5 flex-shrink-0">
<IconifyIcon
v-if="row.type === 'button'"
icon="carbon:security"
class="size-full"
/>
<IconifyIcon
v-else-if="row.meta?.icon"
:icon="row.meta?.icon || 'carbon:circle-dash'"
class="size-full"
/>
</div>
<span class="flex-auto">{{ $t(row.meta?.title) }}</span>
<div class="items-center justify-end"></div>
</div>
<MenuBadge
v-if="row.meta?.badgeType"
class="menu-badge"
:badge="row.meta.badge"
:badge-type="row.meta.badgeType"
:badge-variants="row.meta.badgeVariants"
/>
</template>
</Grid>
</Page>
</template>
<style lang="scss" scoped>
.menu-badge {
top: 50%;
right: 0;
transform: translateY(-50%);
& > :deep(div) {
padding-top: 0;
padding-bottom: 0;
}
}
</style>
@@ -1,237 +0,0 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemUserApi } from '#/api';
import * as api from '#/api';
/**
* 新增/修改
*/
export function useFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'license_plate',
label: '车牌',
componentProps: {
allowClear: true,
},
},
{
component: 'Input',
fieldName: 'vehicle_type',
label: '车型',
componentProps: {
allowClear: true,
},
},
{
component: 'Input',
fieldName: 'livestock_type',
label: '牲畜种类',
componentProps: {
allowClear: true,
},
},
{
component: 'Input',
fieldName: 'livestock_source',
label: '牲畜来源',
componentProps: {
allowClear: true,
},
},
{
component: 'RadioGroup',
componentProps: {
buttonStyle: 'solid',
options: [
{ label: '已检疫', value: 1 },
{ label: '未检疫', value: 0 },
],
optionType: 'button',
},
defaultValue: 0,
fieldName: 'is_inspected',
label: '检疫状态',
},
{
component: 'Textarea',
fieldName: 'remark',
label: '备注',
componentProps: {
allowClear: true,
},
},
];
}
/**
* 筛选
*/
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'id',
label: '编号',
componentProps: {
allowClear: true,
},
},
{
component: 'Input',
fieldName: 'license_plate',
label: '车牌',
componentProps: {
allowClear: true,
},
},
{
component: 'Input',
fieldName: 'vehicle_type',
label: '车型',
componentProps: {
allowClear: true,
},
},
{
component: 'ApiCombobox',
fieldName: 'livestock_type',
label: '牲畜种类',
componentProps: {
api: () => api.getDictDetailList('livestock_type'),
labelField: 'value',
valueField: 'value',
showSearch: true,
mode: 'combobox', // 可选 combobox / tags
allowClear: true,
},
},
{
component: 'Input',
fieldName: 'livestock_source',
label: '牲畜来源',
componentProps: {
allowClear: true,
},
},
{
component: 'Select',
componentProps: {
allowClear: true,
options: [
{ label: '已检疫', value: 1 },
{ label: '未检疫', value: 0 },
],
},
fieldName: 'is_inspected',
label: '检疫状态',
},
{
component: 'RangePicker',
fieldName: 'createTime',
label: '录入时间',
},
];
}
/**
* 列表展示
* @param onActionClick
* @param onStatusChange
*/
export function useColumns<T = SystemUserApi.SystemUser>(
onActionClick: OnActionClickFn<T>,
onStatusChange?: (newStatus: any, row: T) => PromiseLike<boolean | undefined>,
): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
title: '编号',
width: 100,
},
{
field: 'license_plate',
title: '车牌',
width: 150,
},
{
cellRender: { name: 'CellImage' },
field: 'license_plate_image',
title: '车牌照',
width: 150,
},
{
field: 'vehicle_type',
title: '车型',
width: 150,
},
{
cellRender: { name: 'CellImage' },
field: 'vehicle_image',
title: '车身照',
width: 150,
},
{
field: 'livestock_type',
title: '牲畜种类',
width: 100,
},
{
field: 'livestock_source',
title: '牲畜来源',
width: 200,
},
{
field: 'is_inspected',
title: '检疫状态',
width: 100,
cellRender: {
name: 'CellSwitch',
props: {
checkedChildren: '已检疫',
unCheckedChildren: '未检疫',
},
attrs: {
beforeChange: onStatusChange,
},
},
},
{
field: 'created_at',
title: '录入时间',
width: 150,
},
{
field: 'updated_at',
title: '最后更新时间',
width: 150,
},
{
field: 'dept_name',
title: '所属组织',
width: 300,
},
{
field: 'remark',
title: '备注',
width: 300,
},
{
align: 'center',
cellRender: {
attrs: {
nameField: 'name',
nameTitle: '设备名称',
onClick: onActionClick,
},
name: 'CellOperation',
},
field: 'operation',
fixed: 'right',
title: '操作',
width: 100,
},
];
}
@@ -1,128 +0,0 @@
<script lang="ts" setup>
import type { DataNode } from 'ant-design-vue/es/tree';
import type { Recordable } from '@vben/types';
import type { SentinelApi } from '#/api';
import { computed, nextTick, ref } from 'vue';
import { Tree, useVbenDrawer } from '@vben/common-ui';
import { Spin } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createSentinelRecord, updateSentinelRecord } from '#/api';
import { $t } from '#/locales';
import { useFormSchema } from './data';
const emits = defineEmits(['success']);
const formData = ref<SentinelApi.Record>();
const [Form, formApi] = useVbenForm({
schema: useFormSchema(),
showDefaultActions: false,
});
const roles = ref<DataNode[]>([]);
const loadingRoles = ref(false);
const id = ref();
const [Drawer, drawerApi] = useVbenDrawer({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) return;
const values = await formApi.getValues();
drawerApi.lock();
(id.value
? updateSentinelRecord(id.value, values)
: createSentinelRecord(values)
)
.then(() => {
emits('success');
drawerApi.close();
})
.catch(() => {
drawerApi.unlock();
});
},
async onOpenChange(isOpen) {
if (isOpen) {
const data = drawerApi.getData<SentinelApi.Record>();
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) {
formApi.setValues(data);
}
}
},
});
const getDrawerTitle = computed(() => {
return formData.value?.id
? $t('common.edit', $t('system.role.name'))
: $t('common.create', $t('system.role.name'));
});
function getNodeClass(node: Recordable<any>) {
const classes: string[] = [];
if (node.value?.type === 'button') {
classes.push('inline-flex');
}
return classes.join(' ');
}
</script>
<template>
<Drawer :title="getDrawerTitle">
<Form>
<template #roles="slotProps">
<Spin :spinning="loadingRoles" wrapper-class-name="w-full">
<Tree
:tree-data="roles"
multiple
bordered
:default-expanded-level="2"
:get-node-class="getNodeClass"
v-bind="slotProps"
value-field="id"
label-field="title"
>
<template #node="{ value }">
{{ $t(value.id) }}
</template>
</Tree>
</Spin>
</template>
</Form>
</Drawer>
</template>
<style lang="css" scoped>
:deep(.ant-tree-title) {
.tree-actions {
display: none;
margin-left: 20px;
}
}
:deep(.ant-tree-title:hover) {
.tree-actions {
display: flex;
flex: auto;
justify-content: flex-end;
margin-left: 20px;
}
}
</style>
@@ -1,143 +0,0 @@
<script lang="ts" setup>
import type { Recordable } from '@vben-core/typings';
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import { deleteDevice, deleteSentinelRecord, type SentinelApi, type SystemDeviceApi } from "#/api";
import { Page, useVbenDrawer } from '@vben/common-ui';
import { Plus } from '@vben/icons';
import { Button, message } from "ant-design-vue";
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getSentinelRecordList, updateSentinelRecordPatch } from '#/api';
import { useColumns, useGridFormSchema } from './data';
import Form from './form.vue';
const [FormDrawer, formDrawerApi] = useVbenDrawer({
connectedComponent: Form,
destroyOnClose: true,
});
/**
* 状态开关即将改变
* @param newStatus 期望改变的状态值
* @param row 行数据
* @returns 返回false则中止改变,返回其他值(undefined、true)则允许改变
*/
async function onStatusChange(newStatus: number, row: SentinelApi.Record) {
const status: Recordable<string> = {
0: '不通过',
1: '通过',
};
try {
await confirm(
`你要将记录编号为${row.id}的检查状态切换为【${status[newStatus.toString()]}】 吗?`,
`切换检查状态`,
);
await updateSentinelRecordPatch(row.id, { is_inspected: newStatus });
return true;
} catch {
return false;
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
fieldMappingTime: [['createTime', ['startTime', 'endTime']]],
schema: useGridFormSchema(),
submitOnChange: true,
},
gridOptions: {
columns: useColumns(onActionClick, onStatusChange),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
const res = await getSentinelRecordList({
page: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
return {
items: res.list,
total: res.total,
};
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
custom: true,
export: false,
refresh: true,
search: true,
zoom: true,
},
} as VxeTableGridOptions<SentinelApi.Record>,
});
function onActionClick(e: OnActionClickParams<SentinelApi.Record>) {
switch (e.code) {
case 'delete': {
onDelete(e.row);
break;
}
case 'edit': {
onEdit(e.row);
break;
}
}
}
function onEdit(row: SentinelApi.Record) {
formDrawerApi.setData(row).open();
}
function onDelete(row: SentinelApi.Record) {
const hideLoading = message.loading({
content: `正在删除记录:${[row.id]}`,
duration: 0,
key: 'action_process_msg',
});
deleteSentinelRecord(row.id)
.then(() => {
message.success({
content: `已删除记录:${[row.id]}`,
key: 'action_process_msg',
});
onRefresh();
})
.catch(() => {
hideLoading();
});
}
function onRefresh() {
gridApi.query();
}
function onCreate() {
formDrawerApi.setData({}).open();
}
</script>
<template>
<Page auto-content-height>
<FormDrawer @success="onRefresh" />
<Grid :table-title="设备列表">
<template #toolbar-tools>
<Button type="primary" @click="onCreate">
<Plus class="size-5" />
手动新增记录
</Button>
</template>
</Grid>
</Page>
</template>