This commit is contained in:
dap
2025-01-07 16:24:53 +08:00
404 changed files with 4182 additions and 1468 deletions

View File

@@ -1,4 +1,10 @@
export default {
'*.md': ['prettier --cache --ignore-unknown --write'],
'*.vue': [
'prettier --write',
'eslint --cache --fix',
'stylelint --fix --allow-empty-input',
],
'*.{js,jsx,ts,tsx}': [
'prettier --cache --ignore-unknown --write',
'eslint --cache --fix',
@@ -7,14 +13,8 @@ export default {
'prettier --cache --ignore-unknown --write',
'stylelint --fix --allow-empty-input',
],
'*.md': ['prettier --cache --ignore-unknown --write'],
'*.vue': [
'prettier --write',
'eslint --cache --fix',
'stylelint --fix --allow-empty-input',
],
'package.json': ['prettier --cache --write'],
'{!(package)*.json,*.code-snippets,.!(browserslist)*rc}': [
'prettier --cache --write--parser json',
],
'package.json': ['prettier --cache --write'],
};

8
.vscode/launch.json vendored
View File

@@ -9,7 +9,7 @@
"url": "http://localhost:5555",
"env": { "NODE_ENV": "development" },
"sourceMaps": true,
"webRoot": "${workspaceFolder}"
"webRoot": "${workspaceFolder}/playground"
},
{
"type": "chrome",
@@ -18,7 +18,7 @@
"url": "http://localhost:5666",
"env": { "NODE_ENV": "development" },
"sourceMaps": true,
"webRoot": "${workspaceFolder}"
"webRoot": "${workspaceFolder}/apps/web-antd"
},
{
"type": "chrome",
@@ -27,7 +27,7 @@
"url": "http://localhost:5777",
"env": { "NODE_ENV": "development" },
"sourceMaps": true,
"webRoot": "${workspaceFolder}"
"webRoot": "${workspaceFolder}/apps/web-ele"
},
{
"type": "chrome",
@@ -36,7 +36,7 @@
"url": "http://localhost:5888",
"env": { "NODE_ENV": "development" },
"sourceMaps": true,
"webRoot": "${workspaceFolder}"
"webRoot": "${workspaceFolder}/apps/web-naive"
}
]
}

View File

@@ -1,8 +1,22 @@
# 1.1.4
**REFACTOR**
- 菜单选择组件重构为Table形式
**Features**
- 通用的vxe-table排序事件(排序逻辑改为在排序事件中处理而非在api处理)
- getDict/getDictOptions 提取公共逻辑 减少冗余代码
**BUG FIXES**
- 字典项为空时getDict方法无限调用接口((无奈兼容 不给字典item本来就是错误用法))
- 表格排序翻页会丢失排序参数
**OTHERS**
- 用户管理 新增只获取一次(mounted)默认密码而非每次打开modal都获取
# 1.1.3

View File

@@ -43,6 +43,31 @@ export default eventHandler(async (event) => {
await sleep(600);
const { page, pageSize } = getQuery(event);
return usePageResponseSuccess(page as string, pageSize as string, mockData);
const { page, pageSize, sortBy, sortOrder } = getQuery(event);
const listData = structuredClone(mockData);
if (sortBy && Reflect.has(listData[0], sortBy as string)) {
listData.sort((a, b) => {
if (sortOrder === 'asc') {
if (sortBy === 'price') {
return (
Number.parseFloat(a[sortBy as string]) -
Number.parseFloat(b[sortBy as string])
);
} else {
return a[sortBy as string] > b[sortBy as string] ? 1 : -1;
}
} else {
if (sortBy === 'price') {
return (
Number.parseFloat(b[sortBy as string]) -
Number.parseFloat(a[sortBy as string])
);
} else {
return a[sortBy as string] < b[sortBy as string] ? 1 : -1;
}
}
});
}
return usePageResponseSuccess(page as string, pageSize as string, listData);
});

View File

@@ -4,6 +4,7 @@ export interface UserInfo {
realName: string;
roles: string[];
username: string;
homePath?: string;
}
export const MOCK_USERS: UserInfo[] = [
@@ -20,6 +21,7 @@ export const MOCK_USERS: UserInfo[] = [
realName: 'Admin',
roles: ['admin'],
username: 'admin',
homePath: '/workspace',
},
{
id: 2,
@@ -27,6 +29,7 @@ export const MOCK_USERS: UserInfo[] = [
realName: 'Jack',
roles: ['user'],
username: 'jack',
homePath: '/analytics',
},
];

View File

@@ -4,13 +4,12 @@
*/
import type { BaseFormComponentType } from '@vben/common-ui';
import type { Component, SetupContext } from 'vue';
import { h } from 'vue';
import { Tinymce as RichTextarea } from '#/components/tinymce';
import { FileUpload, ImageUpload } from '#/components/upload';
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
import { $t } from '@vben/locales';
import {
AutoComplete,
Button,
@@ -35,9 +34,7 @@ import {
TreeSelect,
Upload,
} from 'ant-design-vue';
import { Tinymce as RichTextarea } from '#/components/tinymce';
import { FileUpload, ImageUpload } from '#/components/upload';
import { h } from 'vue';
const withDefaultPlaceholder = <T extends Component>(
component: T,

View File

@@ -1,12 +1,9 @@
import { h, type Ref } from 'vue';
import {
setupVbenVxeTable,
useVbenVxeGrid,
type VxeGridDefines,
} from '@vben/plugins/vxe-table';
import type { VxeGridDefines, VxeGridPropTypes } from '@vben/plugins/vxe-table';
import type { Ref } from 'vue';
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
import { Button, Image } from 'ant-design-vue';
import { h } from 'vue';
import { useVbenForm } from './form';
@@ -133,6 +130,7 @@ export function vxeCheckboxChecked(
/**
* 通用的vxe-table排序事件 支持单/多字段排序
* @deprecated 翻页后排序会丢失使用addSortParams代替
* @param tableApi api
* @param sortParams 排序参数
*/
@@ -151,3 +149,23 @@ export function vxeSortEvent(
const isAsc = sortList.map((item) => item.order).join(',');
tableApi.query({ orderByColumn, isAsc });
}
/**
* 通用的 排序参数添加到请求参数中
* @param params 请求参数
* @param sortList vxe-table的排序参数
*/
export function addSortParams(
params: Record<string, any>,
sortList: VxeGridPropTypes.ProxyAjaxQuerySortCheckedParams[],
) {
// 这里是排序取消 length为0 就不添加参数了
if (sortList.length === 0) {
return;
}
// 支持单/多字段排序
const orderByColumn = sortList.map((item) => item.field).join(',');
const isAsc = sortList.map((item) => item.order).join(',');
params.orderByColumn = orderByColumn;
params.isAsc = isAsc;
}

View File

@@ -36,5 +36,5 @@ export function forceLogout(tokenId: string) {
* @returns void
*/
export function forceLogout2(tokenId: string) {
return requestClient.postWithMsg<void>(`${Api.root}/${tokenId}`);
return requestClient.deleteWithMsg<void>(`${Api.root}/myself/${tokenId}`);
}

View File

@@ -1,7 +1,7 @@
import type { Menu, MenuOption, MenuResp } from './model';
import type { ID, IDS } from '#/api/common';
import type { Menu, MenuOption, MenuResp } from './model';
import { requestClient } from '#/api/request';
enum Api {

View File

@@ -33,6 +33,8 @@ export interface MenuOption {
weight: number;
children: MenuOption[];
key: string; // 实际上不存在 ide报错
menuType: string;
icon: string;
}
/**

View File

@@ -1,7 +1,7 @@
import type { OssFile } from './model';
import type { ID, IDS, PageQuery, PageResult } from '#/api/common';
import type { OssFile } from './model';
import { ContentTypeEnum } from '#/api/helper';
import { requestClient } from '#/api/request';

View File

@@ -1 +1,2 @@
export { default as MenuSelectTable } from './src/menu-select-table.vue';
export { default as TreeSelectPanel } from './src/tree-select-panel.vue';

View File

@@ -0,0 +1,85 @@
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { ID } from '#/api/common';
import type { MenuOption } from '#/api/system/menu/model';
import { h, markRaw } from 'vue';
import { FolderIcon, MenuIcon, OkButtonIcon, VbenIcon } from '@vben/icons';
export interface Permission {
checked: boolean;
id: ID;
label: string;
}
export interface MenuPermissionOption extends MenuOption {
permissions: Permission[];
}
const menuTypes = {
C: { icon: markRaw(MenuIcon), value: '菜单' },
F: { icon: markRaw(OkButtonIcon), value: '按钮' },
M: { icon: markRaw(FolderIcon), value: '目录' },
};
export const nodeOptions = [
{ label: '节点关联', value: true },
{ label: '节点独立', value: false },
];
export const columns: VxeGridProps['columns'] = [
{
type: 'checkbox',
title: '菜单名称',
field: 'label',
treeNode: true,
headerAlign: 'left',
align: 'left',
width: 230,
},
{
title: '图标',
field: 'icon',
width: 80,
slots: {
default: ({ row }) => {
if (row?.icon === '#') {
return '';
}
return (
<span class={'flex justify-center'}>
<VbenIcon icon={row.icon} />
</span>
);
},
},
},
{
title: '类型',
field: 'menuType',
width: 80,
slots: {
default: ({ row }) => {
const current = menuTypes[row.menuType as 'C' | 'F' | 'M'];
if (!current) {
return '未知';
}
return (
<span class="flex items-center justify-center gap-1">
{h(current.icon, { class: 'size-[18px]' })}
<span>{current.value}</span>
</span>
);
},
},
},
{
title: '权限标识',
field: 'permissions',
headerAlign: 'left',
align: 'left',
slots: {
default: 'permissions',
},
},
];

View File

@@ -0,0 +1,133 @@
import type { MenuPermissionOption } from './data';
import type { useVbenVxeGrid } from '#/adapter/vxe-table';
import type { MenuOption } from '#/api/system/menu/model';
import { eachTree, treeToList } from '@vben/utils';
import { difference, isEmpty, isUndefined } from 'lodash-es';
/**
* 权限列设置是否全选
* @param record 行记录
* @param checked 是否选中
*/
export function setPermissionsChecked(
record: MenuPermissionOption,
checked: boolean,
) {
if (record?.permissions?.length > 0) {
// 全部设置为选中
record.permissions.forEach((permission) => {
permission.checked = checked;
});
}
}
/**
* 设置当前行 & 所有子节点选中状态
* @param record 行
* @param checked 是否选中
*/
export function rowAndChildrenChecked(
record: MenuPermissionOption,
checked: boolean,
) {
// 当前行选中
setPermissionsChecked(record, checked);
// 所有子节点选中
record?.children?.forEach?.((permission) => {
rowAndChildrenChecked(permission as MenuPermissionOption, checked);
});
}
/**
* void方法 会直接修改原始数据
* 将树结构转为 tree+permissions结构
* @param menus 后台返回的menu
*/
export function menusWithPermissions(menus: MenuOption[]) {
eachTree(menus, (item: MenuPermissionOption) => {
if (item.children && item.children.length > 0) {
/**
* 所有为按钮的节点提取出来
* 需要注意 这里需要过滤目录下直接是按钮的情况item.menuType !== 'M'
* 将按钮往children添加而非加到permissions
*/
const permissions = item.children.filter(
(child: MenuOption) => child.menuType === 'F' && item.menuType !== 'M',
);
// 取差集
const diffCollection = difference(item.children, permissions);
// 更新后的children 即去除按钮
item.children = diffCollection;
// permissions作为字段添加到item
const permissionsArr = permissions.map((permission) => {
return {
id: permission.id,
label: permission.label,
checked: false,
};
});
item.permissions = permissionsArr;
}
});
}
/**
* 设置表格选中
* @param checkedKeys 选中的keys
* @param menus 菜单 转换后的菜单
* @param tableApi api
* @param association 是否节点关联
*/
export function setTableChecked(
checkedKeys: (number | string)[],
menus: MenuPermissionOption[],
tableApi: ReturnType<typeof useVbenVxeGrid>['1'],
association: boolean,
) {
// tree转list
const menuList: MenuPermissionOption[] = treeToList(menus);
// 拿到勾选的行数据
let checkedRows = menuList.filter((item) => checkedKeys.includes(item.id));
/**
* 节点独立切换到节点关联 只需要最末尾的数据 即children为空
*/
if (!association) {
checkedRows = checkedRows.filter(
(item) => isUndefined(item.children) || isEmpty(item.children),
);
}
// 设置行选中 & permissions选中
checkedRows.forEach((item) => {
tableApi.grid.setCheckboxRow(item, true);
if (item?.permissions?.length > 0) {
item.permissions.forEach((permission) => {
if (checkedKeys.includes(permission.id)) {
permission.checked = true;
}
});
}
});
/**
* 节点独立切换到节点关联
* 勾选后还需要过滤权限没有任何勾选的情况 这时候取消行的勾选
*/
if (!association) {
const emptyRows = checkedRows.filter((item) => {
if (isUndefined(item.permissions) || isEmpty(item.permissions)) {
return false;
}
return item.permissions.every(
(permission) => permission.checked === false,
);
});
// 设置为不选中
tableApi.grid.setCheckboxRow(emptyRows, false);
}
}

View File

@@ -0,0 +1,57 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import type { TourProps } from 'ant-design-vue';
import { defineComponent, ref } from 'vue';
import { useLocalStorage } from '@vueuse/core';
import { Tour } from 'ant-design-vue';
/**
* 全屏引导
* @returns value
*/
export function useFullScreenGuide() {
const open = ref(false);
/**
* 是否已读 只显示一次
*/
const read = useLocalStorage('menu_select_fullscreen_read', false);
function openGuide() {
if (!read.value) {
open.value = true;
}
}
function closeGuide() {
open.value = false;
read.value = true;
}
const steps: TourProps['steps'] = [
{
title: '提示',
description: '点击这里可以全屏',
target: () =>
document.querySelector(
'div#menu-select-table .vxe-tools--operate > button[title="全屏"]',
)!,
},
];
const FullScreenGuide = defineComponent({
name: 'FullScreenGuide',
inheritAttrs: false,
setup() {
return () => (
<Tour onClose={closeGuide} open={open.value} steps={steps} />
);
},
});
return {
FullScreenGuide,
openGuide,
closeGuide,
};
}

View File

@@ -0,0 +1,412 @@
<!--
不兼容也不会兼容一些错误用法
比如: 菜单下放目录 菜单下放菜单
比如: 按钮下放目录 按钮下放菜单 按钮下放按钮
-->
<script setup lang="tsx">
import type { RadioChangeEvent } from 'ant-design-vue';
import type { MenuPermissionOption } from './data';
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { MenuOption } from '#/api/system/menu/model';
import { nextTick, onMounted, ref, shallowRef, watch } from 'vue';
import { cloneDeep, findGroupParentIds } from '@vben/utils';
import { Alert, Checkbox, RadioGroup, Space } from 'ant-design-vue';
import { uniq } from 'lodash-es';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { columns, nodeOptions } from './data';
import {
menusWithPermissions,
rowAndChildrenChecked,
setPermissionsChecked,
setTableChecked,
} from './helper';
import { useFullScreenGuide } from './hook';
defineOptions({
name: 'MenuSelectTable',
inheritAttrs: false,
});
const props = withDefaults(
defineProps<{
checkedKeys: (number | string)[];
defaultExpandAll?: boolean;
menus: MenuOption[];
}>(),
{
/**
* 是否默认展开全部
*/
defaultExpandAll: true,
/**
* 注意这里不是双向绑定 需要调用getCheckedKeys实例方法来获取真正选中的节点
*/
checkedKeys: () => [],
},
);
/**
* 是否节点关联
*/
const association = defineModel('association', {
type: Boolean,
default: true,
});
const gridOptions: VxeGridProps = {
checkboxConfig: {
// checkbox显示的字段
labelField: 'label',
// 是否严格模式 即节点不关联
checkStrictly: !association.value,
},
size: 'small',
columns,
height: 'auto',
keepSource: true,
pagerConfig: {
enabled: false,
},
proxyConfig: {
enabled: false,
},
toolbarConfig: {
// 自定义列
custom: false,
// 最大化
zoom: true,
// 刷新
refresh: false,
},
rowConfig: {
isHover: false,
isCurrent: false,
keyField: 'id',
},
/**
* 开启虚拟滚动
* 数据量小可以选择关闭
* 如果遇到样式问题(空白、错位 滚动等)可以选择关闭虚拟滚动
*/
scrollY: {
enabled: true,
gt: 0,
},
treeConfig: {
parentField: 'parentId',
rowField: 'id',
transform: false,
},
// 溢出换行显示
showOverflow: false,
};
/**
* 用于界面显示选中的数量
*/
const checkedNum = ref(0);
/**
* 更新选中的数量
*/
function updateCheckedNumber() {
checkedNum.value = getCheckedKeys().length;
}
const [BasicTable, tableApi] = useVbenVxeGrid({
gridOptions,
gridEvents: {
// 勾选事件
checkboxChange: (params) => {
// 选中还是取消选中
const checked = params.checked;
// 行
const record = params.row;
if (association.value) {
// 节点关联
// 设置所有子节点选中状态
rowAndChildrenChecked(record, checked);
} else {
// 节点独立
// 点行会勾选/取消全部权限 点权限不会勾选行
setPermissionsChecked(record, checked);
}
updateCheckedNumber();
},
// 全选事件
checkboxAll: (params) => {
const records = params.$grid.getData();
records.forEach((item) => {
rowAndChildrenChecked(item, params.checked);
});
updateCheckedNumber();
},
},
});
/**
* 设置表格选中
* @param menus menu
* @param keys 选中的key
* @param triggerOnchange 节点独立情况 不需要触发onChange(false)
*/
function setCheckedByKeys(
menus: MenuPermissionOption[],
keys: (number | string)[],
triggerOnchange: boolean,
) {
menus.forEach((item) => {
// 设置行选中
if (keys.includes(item.id)) {
tableApi.grid.setCheckboxRow(item, true);
}
// 设置权限columns选中
if (item.permissions && item.permissions.length > 0) {
// 遍历 设置勾选
item.permissions.forEach((permission) => {
if (keys.includes(permission.id)) {
permission.checked = true;
// 手动触发onChange来选中 节点独立情况不需要处理
triggerOnchange && handlePermissionChange(item);
}
});
}
// 设置children选中
if (item.children && item.children.length > 0) {
setCheckedByKeys(item.children as any, keys, triggerOnchange);
}
});
}
const { FullScreenGuide, openGuide } = useFullScreenGuide();
onMounted(() => {
/**
* 加载表格数据 转为指定结构
*/
watch(
() => props.menus,
async (menus) => {
const clonedMenus = cloneDeep(menus);
menusWithPermissions(clonedMenus);
// console.log(clonedMenus);
await tableApi.grid.loadData(clonedMenus);
// 展开全部 默认true
if (props.defaultExpandAll) {
await nextTick();
setExpandOrCollapse(true);
}
},
);
/**
* 节点关联变动 更新表格勾选效果
*/
watch(association, (value) => {
tableApi.setGridOptions({
checkboxConfig: {
checkStrictly: !value,
},
});
});
/**
* checkedKeys依赖menus
* 要注意加载顺序
* !!!要在外部确保menus先加载!!!
*/
watch(
() => props.checkedKeys,
(value) => {
const allCheckedKeys = uniq([...value]);
// 获取表格data 如果checkedKeys在menus的watch之前触发 这里会拿到空 导致勾选异常
const records = tableApi.grid.getData();
setCheckedByKeys(records, allCheckedKeys, association.value);
updateCheckedNumber();
// 全屏引导
setTimeout(openGuide, 1000);
},
);
});
// 缓存上次(切换节点关系前)选中的keys
const lastCheckedKeys = shallowRef<(number | string)[]>([]);
/**
* 节点关联变动 事件
*/
async function handleAssociationChange(e: RadioChangeEvent) {
lastCheckedKeys.value = getCheckedKeys();
// 清空全部permissions选中
const records = tableApi.grid.getData();
records.forEach((item) => {
rowAndChildrenChecked(item, false);
});
// 需要清空全部勾选
await tableApi.grid.clearCheckboxRow();
// 滚动到顶部
await tableApi.grid.scrollTo(0, 0);
// 节点切换 不同的选中
setTableChecked(lastCheckedKeys.value, records, tableApi, !e.target.value);
updateCheckedNumber();
}
/**
* 全部展开/折叠
* @param expand 是否展开
*/
function setExpandOrCollapse(expand: boolean) {
tableApi.grid?.setAllTreeExpand(expand);
}
/**
* 权限列表 checkbox勾选的事件
* @param row 行
*/
function handlePermissionChange(row: any) {
// 节点关联
if (association.value) {
const checkedPermissions = row.permissions.filter(
(item: any) => item.checked === true,
);
// 有一条选中 则整个行选中
if (checkedPermissions.length > 0) {
tableApi.grid.setCheckboxRow(row, true);
}
// 无任何选中 则整个行不选中
if (checkedPermissions.length === 0) {
tableApi.grid.setCheckboxRow(row, false);
}
}
// 节点独立 不处理
updateCheckedNumber();
}
/**
* 获取勾选的key
* @param records 行记录列表
* @param addCurrent 是否添加当前行的id
*/
function getKeys(records: MenuPermissionOption[], addCurrent: boolean) {
const allKeys: (number | string)[] = [];
records.forEach((item) => {
// 处理children
if (item.children && item.children.length > 0) {
const keys = getKeys(item.children as MenuPermissionOption[], addCurrent);
allKeys.push(...keys);
} else {
// 当前行的id
addCurrent && allKeys.push(item.id);
// 当前行权限id 获取已经选中的
if (item.permissions && item.permissions.length > 0) {
const ids = item.permissions
.filter((m) => m.checked === true)
.map((m) => m.id);
allKeys.push(...ids);
}
}
});
return uniq(allKeys);
}
/**
* 获取选中的key
*/
function getCheckedKeys() {
// 节点关联
if (association.value) {
const records = tableApi?.grid?.getCheckboxRecords?.() ?? [];
// 子节点
const nodeKeys = getKeys(records, true);
// 所有父节点
const parentIds = findGroupParentIds(props.menus, nodeKeys as number[]);
// 拼接 去重
const realKeys = uniq([...parentIds, ...nodeKeys]);
return realKeys;
}
// 节点独立
// 勾选的行
const records = tableApi?.grid?.getCheckboxRecords?.() ?? [];
// 全部数据 用于获取permissions
const allRecords = tableApi?.grid?.getData?.() ?? [];
// 表格已经选中的行ids
const checkedIds = records.map((item) => item.id);
// 所有已经勾选权限的ids
const permissionIds = getKeys(allRecords, false);
// 合并 去重
const allIds = uniq([...checkedIds, ...permissionIds]);
return allIds;
}
/**
* 暴露给外部使用 获取已选中的key
*/
defineExpose({
getCheckedKeys,
});
</script>
<template>
<div class="flex h-full flex-col" id="menu-select-table">
<BasicTable>
<template #toolbar-actions>
<RadioGroup
v-model:value="association"
:options="nodeOptions"
button-style="solid"
option-type="button"
@change="handleAssociationChange"
/>
<Alert class="mx-2" type="info">
<template #message>
<div>
已选中
<span class="text-primary mx-1 font-semibold">
{{ checkedNum }}
</span>
个节点
</div>
</template>
</Alert>
</template>
<template #toolbar-tools>
<Space>
<a-button @click="setExpandOrCollapse(false)">
{{ $t('pages.common.collapse') }}
</a-button>
<a-button @click="setExpandOrCollapse(true)">
{{ $t('pages.common.expand') }}
</a-button>
</Space>
</template>
<template #permissions="{ row }">
<div class="flex flex-wrap gap-x-3 gap-y-1">
<Checkbox
v-for="permission in row.permissions"
:key="permission.id"
v-model:checked="permission.checked"
@change="() => handlePermissionChange(row)"
>
{{ permission.label }}
</Checkbox>
</div>
</template>
</BasicTable>
<!-- 全屏引导 -->
<FullScreenGuide />
</div>
</template>
<style scoped>
:deep(.ant-alert) {
padding: 4px 8px;
}
</style>

View File

@@ -3,7 +3,9 @@ import type { CheckboxChangeEvent } from 'ant-design-vue/es/checkbox/interface';
import type { DataNode } from 'ant-design-vue/es/tree';
import type { CheckInfo } from 'ant-design-vue/es/vc-tree/props';
import { computed, nextTick, onMounted, type PropType, ref, watch } from 'vue';
import type { PropType } from 'vue';
import { computed, nextTick, onMounted, ref, watch } from 'vue';
import { findGroupParentIds, treeToList } from '@vben/utils';
@@ -108,8 +110,8 @@ const stop = watch([checkedKeys, () => props.treeData], () => {
* @param info info.halfCheckedKeys为父节点的ID
*/
type CheckedState<T = number | string> =
| { checked: T[]; halfChecked: T[] }
| T[];
| T[]
| { checked: T[]; halfChecked: T[] };
function handleChecked(checkedStateKeys: CheckedState, info: CheckInfo) {
// 数组的话为节点关联
if (Array.isArray(checkedStateKeys)) {

View File

@@ -2,15 +2,13 @@
import type { UploadFile, UploadProps } from 'ant-design-vue';
import type { UploadRequestOption } from 'ant-design-vue/lib/vc-upload/interface';
import { ref, toRefs, watch } from 'vue';
import { $t } from '@vben/locales';
import { PlusOutlined } from '@ant-design/icons-vue';
import { message, Modal, Upload } from 'ant-design-vue';
import { isArray, isFunction, isObject, isString } from 'lodash-es';
import { uploadApi } from '#/api';
import { ossInfo } from '#/api/system/oss';
import { PlusOutlined } from '@ant-design/icons-vue';
import { $t } from '@vben/locales';
import { message, Modal, Upload } from 'ant-design-vue';
import { isArray, isFunction, isObject, isString, uniqueId } from 'lodash-es';
import { ref, toRefs, watch } from 'vue';
import { checkImageFileType, defaultImageAccept } from './helper';
import { UploadResultStatus } from './typing';
@@ -37,7 +35,7 @@ const props = withDefaults(
multiple?: boolean;
// support xxx.xxx.xx
// 返回的字段 默认url
resultField?: 'fileName' | 'ossId' | 'url' | string;
resultField?: 'fileName' | 'ossId' | 'url';
value?: string | string[];
}>(),
{
@@ -50,7 +48,7 @@ const props = withDefaults(
accept: () => defaultImageAccept,
multiple: false,
api: uploadApi,
resultField: '',
resultField: 'url',
},
);
const emit = defineEmits(['change', 'update:value', 'delete']);
@@ -74,7 +72,7 @@ const isFirstRender = ref<boolean>(true);
watch(
() => props.value,
(v) => {
async (v) => {
if (isInnerOperate.value) {
isInnerOperate.value = false;
return;
@@ -90,19 +88,40 @@ watch(
}
// 直接赋值 可能为string | string[]
value = v;
fileList.value = _fileList.map((item, i) => {
const withUrlList: UploadProps['fileList'] = [];
for (const item of _fileList) {
// ossId情况
if (props.resultField === 'ossId') {
const resp = await ossInfo([item]);
if (item && isString(item)) {
return {
uid: `${-i}`,
withUrlList.push({
uid: item, // ossId作为uid 方便getValue获取
name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)),
status: 'done',
url: resp?.[0]?.url,
});
} else if (item && isObject(item)) {
withUrlList.push({
...(item as any),
uid: item,
url: resp?.[0]?.url,
});
}
} else {
// 非ossId情况
if (item && isString(item)) {
withUrlList.push({
uid: uniqueId(),
name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)),
status: 'done',
url: item,
};
});
} else if (item && isObject(item)) {
return item;
withUrlList.push(item);
}
return null;
}) as UploadProps['fileList'];
}
}
fileList.value = withUrlList;
}
if (!isFirstRender.value) {
emit('change', value);
@@ -200,12 +219,17 @@ async function customRequest(info: UploadRequestOption<any>) {
}
function getValue() {
console.log(fileList.value);
const list = (fileList.value || [])
.filter((item) => item?.status === UploadResultStatus.DONE)
.map((item: any) => {
if (item?.response && props?.resultField) {
return item?.response?.[props.resultField];
}
// ossId兼容 uid为ossId直接返回
if (props.resultField === 'ossId' && item.uid) {
return item.uid;
}
// 适用于已经有图片 回显的情况 会默认在init处理为{url: 'xx'}
if (item?.url) {
return item.url;

View File

@@ -1,8 +1,6 @@
import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';
import type { Locale } from 'ant-design-vue/es/locale';
import type { App } from 'vue';
import { ref } from 'vue';
import {
$t,
@@ -10,10 +8,10 @@ import {
loadLocalesMapFromDir,
} from '@vben/locales';
import { preferences } from '@vben/preferences';
import antdEnLocale from 'ant-design-vue/es/locale/en_US';
import antdDefaultLocale from 'ant-design-vue/es/locale/zh_CN';
import dayjs from 'dayjs';
import { ref } from 'vue';
const antdLocale = ref<Locale>(antdDefaultLocale);

View File

@@ -54,7 +54,9 @@ function setupAccessGuard(router: Router) {
if (coreRouteNames.includes(to.name as string)) {
if (to.path === LOGIN_PATH && accessStore.accessToken) {
return decodeURIComponent(
(to.query?.redirect as string) || DEFAULT_HOME_PATH,
(to.query?.redirect as string) ||
userStore.userInfo?.homePath ||
DEFAULT_HOME_PATH,
);
}
return true;
@@ -72,7 +74,10 @@ function setupAccessGuard(router: Router) {
return {
path: LOGIN_PATH,
// 如不需要,直接删除 query
query: { redirect: encodeURIComponent(to.fullPath) },
query:
to.fullPath === DEFAULT_HOME_PATH
? {}
: { redirect: encodeURIComponent(to.fullPath) },
// 携带当前跳转的页面,登录后重新跳转该页面
replace: true,
};
@@ -102,7 +107,10 @@ function setupAccessGuard(router: Router) {
accessStore.setAccessMenus(accessibleMenus);
accessStore.setAccessRoutes(accessibleRoutes);
accessStore.setIsAccessChecked(true);
const redirectPath = (from.query.redirect ?? to.fullPath) as string;
const redirectPath = (from.query.redirect ??
(to.path === DEFAULT_HOME_PATH
? userInfo.homePath || DEFAULT_HOME_PATH
: to.fullPath)) as string;
return {
...router.resolve(decodeURIComponent(redirectPath)),

View File

@@ -1,15 +1,22 @@
import type { DictData } from '#/api/system/dict/dict-data-model';
import { dictDataInfo } from '#/api/system/dict/dict-data';
import { type Option, useDictStore } from '#/store/dict';
import { useDictStore } from '#/store/dict';
/**
* 抽取公共逻辑的基础方法
* @param dictName 字典名称
* @param dataGetter 获取字典数据的函数
* @returns 数据
*/
function fetchAndCacheDictData<T>(
dictName: string,
dataGetter: () => T[],
): T[] {
const { dictRequestCache, setDictInfo } = useDictStore();
// 有调用方决定如何获取数据
const dataList = dataGetter();
// todo 重复代码的封装
export function getDict(dictName: string): DictData[] {
const { dictRequestCache, getDict, setDictInfo } = useDictStore();
// 这里拿到
const dictList = getDict(dictName);
// 检查请求状态缓存
if (dictList.length === 0 && !dictRequestCache.has(dictName)) {
if (dataList.length === 0 && !dictRequestCache.has(dictName)) {
dictRequestCache.set(
dictName,
dictDataInfo(dictName)
@@ -20,31 +27,36 @@ export function getDict(dictName: string): DictData[] {
})
.finally(() => {
// 移除请求状态缓存
/**
* 这里主要判断字典item为空的情况(无奈兼容 不给字典item本来就是错误用法)
* 会导致if一直进入逻辑导致接口无限刷新
* 在这里dictList为空时 不删除缓存
*/
if (dataList.length > 0) {
dictRequestCache.delete(dictName);
}
}),
);
}
return dictList;
return dataList;
}
export function getDictOptions(dictName: string): Option[] {
const { dictRequestCache, getDictOptions, setDictInfo } = useDictStore();
const dictOptionList = getDictOptions(dictName);
// 检查请求状态缓存
if (dictOptionList.length === 0 && !dictRequestCache.has(dictName)) {
dictRequestCache.set(
dictName,
dictDataInfo(dictName)
.then((resp) => {
// 缓存到store 这样就不用重复获取了
// 内部处理了push的逻辑 这里不用push
setDictInfo(dictName, resp);
})
.finally(() => {
// 移除请求状态缓存
dictRequestCache.delete(dictName);
}),
);
/**
* 这里是提供给渲染标签使用的方法
* @param dictName 字典名称
* @returns 字典信息
*/
export function getDict(dictName: string) {
const { getDict } = useDictStore();
return fetchAndCacheDictData(dictName, () => getDict(dictName));
}
return dictOptionList;
/**
* 一般是Select, Radio, Checkbox等组件使用
* @param dictName 字典名称
* @returns Options数组
*/
export function getDictOptions(dictName: string) {
const { getDictOptions } = useDictStore();
return fetchAndCacheDictData(dictName, () => getDictOptions(dictName));
}

View File

@@ -15,6 +15,7 @@ import { useAuthStore } from '#/store';
defineOptions({ name: 'CodeLogin' });
const loading = ref(false);
const CODE_LENGTH = 6;
const tenantInfo = ref<TenantResp>({
tenantEnabled: false,
@@ -98,7 +99,9 @@ const formSchema = computed((): VbenFormSchema[] => {
},
fieldName: 'code',
label: $t('authentication.code'),
rules: z.string().min(1, { message: $t('authentication.codeTip') }),
rules: z.string().length(CODE_LENGTH, {
message: $t('authentication.codeTip', [CODE_LENGTH]),
}),
},
];
});

View File

@@ -10,7 +10,9 @@ import { columns } from '#/views/monitor/online/data';
const gridOptions: VxeGridProps = {
columns,
keepSource: true,
pagerConfig: {},
pagerConfig: {
enabled: false,
},
proxyConfig: {
ajax: {
query: async () => {

View File

@@ -1,18 +1,34 @@
import { defineComponent } from 'vue';
import { Fallback } from '@vben/common-ui';
import { Page } from '@vben/common-ui';
import { openWindow } from '@vben/utils';
import { Result } from 'ant-design-vue';
export default defineComponent({
name: 'CommonSkeleton',
setup() {
return () => (
<div class="flex h-[600px] w-full items-center justify-center">
<Fallback
description="等待后端重构工作流后开发"
status="coming-soon"
title="等待开发"
/>
<Page autoContentHeight={true}>
<Result
status="success"
sub-title="等待后端发布"
title="已经开发完毕(warmflow分支)"
>
{{
extra: (
<div>
<a-button
onClick={() => openWindow('http://106.55.255.76')}
type="primary"
>
</a-button>
</div>
),
}}
</Result>
</Page>
);
},
});

View File

@@ -1,11 +1,8 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import type { EchartsUIType } from '@vben/plugins/echarts';
import {
EchartsUI,
type EchartsUIType,
useEcharts,
} from '@vben/plugins/echarts';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
import { onMounted, ref } from 'vue';
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);

View File

@@ -1,11 +1,8 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import type { EchartsUIType } from '@vben/plugins/echarts';
import {
EchartsUI,
type EchartsUIType,
useEcharts,
} from '@vben/plugins/echarts';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
import { onMounted, ref } from 'vue';
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);

View File

@@ -1,11 +1,8 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import type { EchartsUIType } from '@vben/plugins/echarts';
import {
EchartsUI,
type EchartsUIType,
useEcharts,
} from '@vben/plugins/echarts';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
import { onMounted, ref } from 'vue';
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);

View File

@@ -1,11 +1,8 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import type { EchartsUIType } from '@vben/plugins/echarts';
import {
EchartsUI,
type EchartsUIType,
useEcharts,
} from '@vben/plugins/echarts';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
import { onMounted, ref } from 'vue';
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);

View File

@@ -1,11 +1,8 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import type { EchartsUIType } from '@vben/plugins/echarts';
import {
EchartsUI,
type EchartsUIType,
useEcharts,
} from '@vben/plugins/echarts';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
import { onMounted, ref } from 'vue';
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);

View File

@@ -26,18 +26,22 @@ const gridOptions: VxeGridProps = {
columns,
height: 'auto',
keepSource: true,
pagerConfig: {},
pagerConfig: {
enabled: false,
},
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
query: async (_, formValues) => {
return await onlineList({
pageNum: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
scrollY: {
enabled: true,
gt: 0,
},
rowConfig: {
isHover: true,
keyField: 'tokenId',
@@ -51,11 +55,24 @@ async function handleForceOffline(row: Recordable<any>) {
await forceLogout(row.tokenId);
await tableApi.query();
}
function onlineCount() {
return tableApi?.grid?.getData?.()?.length ?? 0;
}
</script>
<template>
<Page :auto-content-height="true">
<BasicTable table-title="在线用户列表">
<BasicTable>
<template #toolbar-actions>
<div class="mr-1 pl-1 text-[1rem]">
<div>
在线用户列表 (
<span class="text-primary font-bold">{{ onlineCount() }}</span>
人在线)
</div>
</div>
</template>
<template #action="{ row }">
<Popconfirm
:get-popup-container="getVxePopupContainer"

View File

@@ -9,10 +9,10 @@ import { $t } from '@vben/locales';
import { Modal, Space } from 'ant-design-vue';
import {
addSortParams,
useVbenVxeGrid,
vxeCheckboxChecked,
type VxeGridProps,
vxeSortEvent,
} from '#/adapter/vxe-table';
import {
operLogClean,
@@ -60,12 +60,14 @@ const gridOptions: VxeGridProps<OperationLog> = {
pagerConfig: {},
proxyConfig: {
ajax: {
query: async ({ page }, formValues = {}) => {
query: async ({ page, sorts }, formValues = {}) => {
const params: any = {
pageNum: page.currentPage,
pageSize: page.pageSize,
...formValues,
};
// 添加排序参数
addSortParams(params, sorts);
return await operLogList(params);
},
},
@@ -87,7 +89,8 @@ const [BasicTable, tableApi] = useVbenVxeGrid({
formOptions,
gridOptions,
gridEvents: {
sortChange: (sortParams) => vxeSortEvent(tableApi, sortParams),
// 排序 重新请求接口
sortChange: () => tableApi.query(),
},
});

View File

@@ -48,7 +48,12 @@ async function setupMenuSelect() {
item.menuName = $t(item.menuName);
});
// const folderArray = menuArray.filter((item) => item.menuType === 'M');
const menuTree = listToTree(menuArray, { id: 'menuId', pid: 'parentId' });
/**
* 这里需要过滤掉按钮类型
* 不允许在按钮下添加数据
*/
const filteredList = menuArray.filter((item) => item.menuType !== 'F');
const menuTree = listToTree(filteredList, { id: 'menuId', pid: 'parentId' });
const fullMenuTree = [
{
menuId: 0,

View File

@@ -128,6 +128,12 @@ export const drawerSchema: FormSchemaGetter = () => [
fieldName: 'domain',
label: '自定义域名',
},
{
component: 'Input',
fieldName: 'tip',
label: '占位作为提示使用',
hideLabel: true,
},
{
component: 'Divider',
componentProps: {

View File

@@ -5,6 +5,8 @@ import { useVbenDrawer } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { cloneDeep } from '@vben/utils';
import { Alert } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
ossConfigAdd,
@@ -79,7 +81,17 @@ async function handleCancel() {
<template>
<BasicDrawer :close-on-click-modal="false" :title="title" class="w-[650px]">
<BasicForm />
<BasicForm>
<template #tip>
<div class="ml-7 w-full">
<Alert
message="私有桶使用自定义域名无法预览, 但可以正常上传/下载"
show-icon
type="warning"
/>
</div>
</template>
</BasicForm>
</BasicDrawer>
</template>

View File

@@ -19,10 +19,10 @@ import {
} from 'ant-design-vue';
import {
addSortParams,
useVbenVxeGrid,
vxeCheckboxChecked,
type VxeGridProps,
vxeSortEvent,
} from '#/adapter/vxe-table';
import { configInfoByKey } from '#/api/system/config';
import { ossDownload, ossList, ossRemove } from '#/api/system/oss';
@@ -66,12 +66,14 @@ const gridOptions: VxeGridProps = {
pagerConfig: {},
proxyConfig: {
ajax: {
query: async ({ page }, formValues = {}) => {
query: async ({ page, sorts }, formValues = {}) => {
const params: any = {
pageNum: page.currentPage,
pageSize: page.pageSize,
...formValues,
};
// 添加排序参数
addSortParams(params, sorts);
return await ossList(params);
},
},
@@ -94,7 +96,8 @@ const [BasicTable, tableApi] = useVbenVxeGrid({
formOptions,
gridOptions,
gridEvents: {
sortChange: (sortParams) => vxeSortEvent(tableApi, sortParams),
// 排序 重新请求接口
sortChange: () => tableApi.query(),
},
});
@@ -134,8 +137,8 @@ function handleToSettings() {
const preview = ref(false);
onMounted(async () => {
const resp = await configInfoByKey('sys.oss.previewListResource');
preview.value = Boolean(resp);
const previewStr = await configInfoByKey('sys.oss.previewListResource');
preview.value = previewStr === 'true';
});
function isImageFile(ext: string) {

View File

@@ -1,3 +1,4 @@
import type { FormSchemaGetter } from '#/adapter/form';
import type { VxeGridProps } from '#/adapter/vxe-table';
import { DictEnum } from '@vben/constants';
@@ -5,7 +6,6 @@ import { getPopupContainer } from '@vben/utils';
import { Tag } from 'ant-design-vue';
import { type FormSchemaGetter } from '#/adapter/form';
import { getDictOptions } from '#/utils/dict';
/**
@@ -17,6 +17,7 @@ export const authScopeOptions = [
{ color: 'orange', label: '本部门数据权限', value: '3' },
{ color: 'cyan', label: '本部门及以下数据权限', value: '4' },
{ color: 'error', label: '仅本人数据权限', value: '5' },
{ color: 'default', label: '部门及以下或本人数据权限', value: '6' },
];
export const querySchema: FormSchemaGetter = () => [

View File

@@ -1,16 +1,14 @@
<script setup lang="ts">
import type { VbenFormProps } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import type { VxeGridProps } from '#/adapter/vxe-table';
import { computed } from 'vue';
import { useRouter } from 'vue-router';
import { useAccess } from '@vben/access';
import {
Page,
useVbenDrawer,
useVbenModal,
type VbenFormProps,
} from '@vben/common-ui';
import { Page, useVbenDrawer, useVbenModal } from '@vben/common-ui';
import { getVxePopupContainer } from '@vben/utils';
import {
@@ -22,11 +20,7 @@ import {
Space,
} from 'ant-design-vue';
import {
useVbenVxeGrid,
vxeCheckboxChecked,
type VxeGridProps,
} from '#/adapter/vxe-table';
import { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';
import {
roleChangeStatus,
roleExport,

View File

@@ -1,5 +1,7 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import type { MenuOption } from '#/api/system/menu/model';
import { computed, nextTick, ref } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import { $t } from '@vben/locales';
@@ -8,7 +10,7 @@ import { cloneDeep, eachTree } from '@vben/utils';
import { useVbenForm } from '#/adapter/form';
import { menuTreeSelect, roleMenuTreeSelect } from '#/api/system/menu';
import { roleAdd, roleInfo, roleUpdate } from '#/api/system/role';
import { TreeSelectPanel } from '#/components/tree';
import { MenuSelectTable } from '#/components/tree';
import { drawerSchema } from './data';
@@ -32,11 +34,10 @@ const [BasicForm, formApi] = useVbenForm({
wrapperClass: 'grid-cols-2 gap-x-4',
});
const menuTree = ref<any[]>([]);
const menuTree = ref<MenuOption[]>([]);
async function setupMenuTree(id?: number | string) {
if (id) {
const resp = await roleMenuTreeSelect(id);
formApi.setFieldValue('menuIds', resp.checkedKeys);
const menus = resp.menus;
// i18n处理
eachTree(menus, (node) => {
@@ -44,15 +45,20 @@ async function setupMenuTree(id?: number | string) {
});
// 设置菜单信息
menuTree.value = resp.menus;
// keys依赖于menu 需要先加载menu
await nextTick();
await formApi.setFieldValue('menuIds', resp.checkedKeys);
} else {
const resp = await menuTreeSelect();
formApi.setFieldValue('menuIds', []);
// i18n处理
eachTree(resp, (node) => {
node.label = $t(node.label);
});
// 设置菜单信息
menuTree.value = resp;
// keys依赖于menu 需要先加载menu
await nextTick();
await formApi.setFieldValue('menuIds', []);
}
}
@@ -78,11 +84,7 @@ const [BasicDrawer, drawerApi] = useVbenDrawer({
},
});
/**
* 这里拿到的是一个数组ref
*/
const menuSelectRef = ref();
const menuSelectRef = ref<InstanceType<typeof MenuSelectTable>>();
async function handleConfirm() {
try {
drawerApi.drawerLoading(true);
@@ -91,7 +93,7 @@ async function handleConfirm() {
return;
}
// 这个用于提交
const menuIds = menuSelectRef.value?.[0]?.getCheckedKeys() ?? [];
const menuIds = menuSelectRef.value?.getCheckedKeys?.() ?? [];
// formApi.getValues拿到的是一个readonly对象不能直接修改需要cloneDeep
const data = cloneDeep(await formApi.getValues());
data.menuIds = menuIds;
@@ -120,17 +122,19 @@ function handleMenuCheckStrictlyChange(value: boolean) {
</script>
<template>
<BasicDrawer :close-on-click-modal="false" :title="title" class="w-[600px]">
<BasicDrawer :close-on-click-modal="false" :title="title" class="w-[800px]">
<BasicForm>
<template #menuIds="slotProps">
<!-- check-strictly为readonly 不能通过v-model绑定 -->
<TreeSelectPanel
<div class="h-[600px] w-full">
<!-- association为readonly 不能通过v-model绑定 -->
<MenuSelectTable
ref="menuSelectRef"
v-bind="slotProps"
:check-strictly="formApi.form.values.menuCheckStrictly"
:tree-data="menuTree"
@check-strictly-change="handleMenuCheckStrictlyChange"
:checked-keys="slotProps.value"
:association="formApi.form.values.menuCheckStrictly"
:menus="menuTree"
@update:association="handleMenuCheckStrictlyChange"
/>
</div>
</template>
</BasicForm>
</BasicDrawer>

View File

@@ -1,23 +1,24 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import type { MenuOption } from '#/api/system/menu/model';
import { computed, nextTick, ref } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { cloneDeep, eachTree, listToTree } from '@vben/utils';
import { cloneDeep, eachTree } from '@vben/utils';
import { omit } from 'lodash-es';
import { useVbenForm } from '#/adapter/form';
import { menuList, tenantPackageMenuTreeSelect } from '#/api/system/menu';
import { menuTreeSelect, tenantPackageMenuTreeSelect } from '#/api/system/menu';
import {
packageAdd,
packageInfo,
packageUpdate,
} from '#/api/system/tenant-package';
import { TreeSelectPanel } from '#/components/tree';
import { MenuSelectTable } from '#/components/tree';
import { drawerSchema } from './data';
import TreeItem from './tree-item';
const emit = defineEmits<{ reload: [] }>();
@@ -36,23 +37,32 @@ const [BasicForm, formApi] = useVbenForm({
wrapperClass: 'grid-cols-2',
});
async function setupMenuTreeSelect(id?: number | string) {
const menuTree = ref<MenuOption[]>([]);
async function setupMenuTree(id?: number | string) {
if (id) {
const resp = await tenantPackageMenuTreeSelect(id);
await formApi.setFieldValue('menuIds', resp.checkedKeys);
}
}
const menuTree = ref<any[]>([]);
async function setupMenuTree() {
const resp = await menuList();
const treeData = listToTree(resp, { id: 'menuId' });
const menus = resp.menus;
// i18n处理
eachTree(treeData, (node) => {
node.menuName = $t(node.menuName);
eachTree(menus, (node) => {
node.label = $t(node.label);
});
// 设置菜单信息
menuTree.value = treeData;
menuTree.value = resp.menus;
// keys依赖于menu 需要先加载menu
await nextTick();
await formApi.setFieldValue('menuIds', resp.checkedKeys);
} else {
const resp = await menuTreeSelect();
// i18n处理
eachTree(resp, (node) => {
node.label = $t(node.label);
});
// 设置菜单信息
menuTree.value = resp;
// keys依赖于menu 需要先加载menu
await nextTick();
await formApi.setFieldValue('menuIds', []);
}
}
const [BasicDrawer, drawerApi] = useVbenDrawer({
@@ -72,20 +82,14 @@ const [BasicDrawer, drawerApi] = useVbenDrawer({
// 通过setupMenuTreeSelect设置
await formApi.setValues(omit(record, ['menuIds']));
}
/**
* 加载菜单树和已勾选菜单
*/
await Promise.all([setupMenuTree(), setupMenuTreeSelect(id)]);
// init菜单 注意顺序要放在赋值record之后 内部watch会依赖record
await setupMenuTree(id);
drawerApi.drawerLoading(false);
},
});
/**
* 这里拿到的是一个数组ref
*/
const menuSelectRef = ref();
const menuSelectRef = ref<InstanceType<typeof MenuSelectTable>>();
async function handleConfirm() {
try {
drawerApi.drawerLoading(true);
@@ -94,7 +98,7 @@ async function handleConfirm() {
return;
}
// 这个用于提交
const menuIds = menuSelectRef.value?.[0]?.getCheckedKeys() ?? [];
const menuIds = menuSelectRef.value?.getCheckedKeys?.() ?? [];
// formApi.getValues拿到的是一个readonly对象不能直接修改需要cloneDeep
const data = cloneDeep(await formApi.getValues());
data.menuIds = menuIds;
@@ -123,22 +127,19 @@ function handleMenuCheckStrictlyChange(value: boolean) {
</script>
<template>
<BasicDrawer :close-on-click-modal="false" :title="title" class="w-[600px]">
<BasicDrawer :close-on-click-modal="false" :title="title" class="w-[800px]">
<BasicForm>
<template #menuIds="slotProps">
<TreeSelectPanel
<div class="h-[600px] w-full">
<!-- association为readonly 不能通过v-model绑定 -->
<MenuSelectTable
ref="menuSelectRef"
v-bind="slotProps"
:check-strictly="formApi.form.values.menuCheckStrictly"
:expand-all-on-init="false"
:field-names="{ title: 'menuName', key: 'menuId' }"
:tree-data="menuTree"
@check-strictly-change="handleMenuCheckStrictlyChange"
>
<template #title="data">
<TreeItem :data="data" />
</template>
</TreeSelectPanel>
:checked-keys="slotProps.value"
:association="formApi.form.values.menuCheckStrictly"
:menus="menuTree"
@update:association="handleMenuCheckStrictlyChange"
/>
</div>
</template>
</BasicForm>
</BasicDrawer>

View File

@@ -1,14 +1,6 @@
<script setup lang="ts">
import type { Role } from '#/api/system/user/model';
import { computed, h, ref } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { addFullName, cloneDeep, getPopupContainer } from '@vben/utils';
import { Tag } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { configInfoByKey } from '#/api/system/config';
import { postOptionSelect } from '#/api/system/post';
@@ -19,6 +11,11 @@ import {
userUpdate,
} from '#/api/system/user';
import { authScopeOptions } from '#/views/system/role/data';
import { useVbenDrawer } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { addFullName, cloneDeep, getPopupContainer } from '@vben/utils';
import { Tag } from 'ant-design-vue';
import { computed, h, onMounted, ref } from 'vue';
import { drawerSchema } from './data';
@@ -117,13 +114,20 @@ async function setupDeptSelect() {
]);
}
const defaultPassword = ref('');
onMounted(async () => {
const password = await configInfoByKey('sys.user.initPassword');
if (password) {
defaultPassword.value = password;
}
});
/**
* 新增时候 从参数设置获取默认密码
*/
async function loadDefaultPassword(update: boolean) {
if (!update) {
const defaultPassword = await configInfoByKey('sys.user.initPassword');
defaultPassword && formApi.setFieldValue('password', defaultPassword);
if (!update && defaultPassword.value) {
formApi.setFieldValue('password', defaultPassword.value);
}
}

View File

@@ -0,0 +1,27 @@
<script setup lang="ts">
import type { MenuOption } from '#/api/system/menu/model';
import { roleMenuTreeSelect } from '#/api/system/menu';
import { MenuSelectTable } from '#/components/tree';
import { Page } from '@vben/common-ui';
import { onMounted, ref, shallowRef } from 'vue';
const checkedKeys = ref<number[]>([]);
const menus = shallowRef<MenuOption[]>([]);
onMounted(async () => {
const resp = await roleMenuTreeSelect(3);
menus.value = resp.menus;
checkedKeys.value = resp.checkedKeys;
});
</script>
<template>
<Page :auto-content-height="true">
<MenuSelectTable
:menus="menus"
v-model:checked-keys="checkedKeys"
:association="true"
/>
</Page>
</template>

View File

@@ -1,11 +1,8 @@
<script setup lang="ts">
import { ref } from 'vue';
import { JsonPreview, Page } from '@vben/common-ui';
import { Alert, RadioGroup } from 'ant-design-vue';
import { FileUpload, ImageUpload } from '#/components/upload';
import { JsonPreview, Page } from '@vben/common-ui';
import { Alert, RadioGroup } from 'ant-design-vue';
import { ref } from 'vue';
const resultField = ref<'ossId' | 'url'>('ossId');
@@ -17,7 +14,7 @@ const fieldOptions = [
];
const fileAccept = ['xlsx', 'word', 'pdf'];
const signleImage = ref<string>('');
const signleImage = ref<string>('1745443704356782081');
</script>
<template>
@@ -27,7 +24,11 @@ const signleImage = ref<string>('');
:show-icon="true"
message="新特性: 设置max-number为1时, 会被绑定为string而非string[]类型 省去手动转换"
/>
<ImageUpload v-model:value="signleImage" :max-number="1" />
<ImageUpload
v-model:value="signleImage"
:max-number="1"
result-field="ossId"
/>
<JsonPreview :data="signleImage" />
</div>
<div class="bg-background flex flex-col gap-[12px] rounded-lg p-6">

View File

@@ -0,0 +1,234 @@
<script setup lang="tsx">
import type { VxeGridProps } from '#/adapter/vxe-table';
import { nextTick, onMounted } from 'vue';
import { JsonPreview } from '@vben/common-ui';
import { getPopupContainer } from '@vben/utils';
import {
Button,
Input,
InputNumber,
message,
Modal,
Select,
Space,
} from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
const gridOptions: VxeGridProps = {
editConfig: {
// 触发编辑的方式
trigger: 'click',
// 触发编辑的模式
mode: 'row',
showStatus: true,
},
border: true,
rowConfig: {
drag: true,
},
checkboxConfig: {},
editRules: {
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
age: [
{ required: true, message: '请输入年龄', trigger: 'blur' },
{ min: 0, max: 200, message: '年龄必须为1-200' },
],
job: [{ required: true, message: '请选择工作', trigger: 'blur' }],
},
columns: [
{
type: 'checkbox',
width: 60,
},
{
dragSort: true,
title: '排序',
width: 60,
},
{
field: 'name',
title: '姓名',
align: 'left',
editRender: {},
slots: {
default: ({ row }) => {
if (!row.name) {
return <span class="text-red-500">未填写</span>;
}
return <span>{row.name}</span>;
},
edit: ({ row }) => {
return <Input placeholder={'请输入'} v-model:value={row.name} />;
},
},
},
{
field: 'age',
title: '年龄',
align: 'left',
editRender: {},
slots: {
default: ({ row }) => {
if (!row.age) {
return <span class="text-red-500">未填写</span>;
}
return <span>{row.age}</span>;
},
edit: ({ row }) => {
return (
<InputNumber
class="w-full"
placeholder={'请输入'}
v-model:value={row.age}
/>
);
},
},
},
{
field: '工作',
title: 'job',
align: 'left',
editRender: {},
slots: {
default: ({ row }) => {
if (!row.job) {
return <span class="text-red-500">未选择</span>;
}
return <span>{row.job}</span>;
},
edit: ({ row }) => {
const options = ['前端佬', '后端佬', '组长'].map((item) => ({
label: item,
value: item,
}));
return (
<Select
class="w-full"
getPopupContainer={getPopupContainer}
options={options}
placeholder={'请选择'}
v-model:value={row.job}
/>
);
},
},
},
{
field: 'action',
title: '操作',
width: 100,
slots: {
default: ({ $table, row }) => {
function handleDelete() {
$table.remove(row);
}
return (
<Button danger={true} onClick={handleDelete} size={'small'}>
删除
</Button>
);
},
},
},
],
height: 'auto',
keepSource: true,
pagerConfig: {
enabled: false,
},
proxyConfig: {
enabled: false,
},
toolbarConfig: {
// 自定义列
custom: false,
// 最大化
zoom: false,
// 刷新
refresh: false,
},
showOverflow: false,
};
const [BasicTable, tableApi] = useVbenVxeGrid({
gridOptions,
});
onMounted(async () => {
const data = [
{
name: '张三',
age: 18,
job: '前端佬',
},
{
name: '李四',
age: 19,
job: '后端佬',
},
{
name: '王五',
age: 20,
job: '组长',
},
];
await nextTick();
await tableApi.grid.loadData(data);
});
async function handleAdd() {
const record = { name: '', age: undefined, job: undefined };
const { row: newRow } = await tableApi.grid.insert(record);
await tableApi.grid.setEditCell(newRow, 'name');
}
async function handleRemove() {
await tableApi.grid.removeCheckboxRow();
}
async function handleValidate() {
const result = await tableApi.grid.validate(true);
if (result) {
message.error('校验失败');
} else {
message.success('校验成功');
}
}
function getData() {
const data = tableApi.grid.getTableData();
const { fullData } = data;
console.log(fullData);
Modal.info({
title: '提示',
content: (
<div class="max-h-[350px] overflow-y-auto">
<JsonPreview data={fullData} />
</div>
),
});
}
</script>
<template>
<BasicTable>
<template #toolbar-tools>
<Space>
<a-button @click="getData">获取表格数据</a-button>
<a-button @click="handleValidate">校验</a-button>
<a-button danger @click="handleRemove"> 删除勾选 </a-button>
<a-button
type="primary"
v-access:code="['system:config:add']"
@click="handleAdd"
>
{{ $t('pages.common.add') }}
</a-button>
</Space>
</template>
</BasicTable>
</template>

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
import { Page } from '@vben/common-ui';
import { Card } from 'ant-design-vue';
import EditTable from './edit-table.vue';
</script>
<template>
<Page>
<div class="flex flex-col gap-4">
<Card title="可编辑表格" size="small">
<EditTable class="h-[500px]" />
</Card>
</div>
</Page>
</template>

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/web-ele",
"version": "5.5.0",
"version": "5.5.2",
"homepage": "https://vben.pro",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -5,13 +5,10 @@
import type { BaseFormComponentType } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import type { Component, SetupContext } from 'vue';
import { h } from 'vue';
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
import { $t } from '@vben/locales';
import {
ElButton,
ElCheckbox,
@@ -32,6 +29,7 @@ import {
ElTreeSelect,
ElUpload,
} from 'element-plus';
import { h } from 'vue';
const withDefaultPlaceholder = <T extends Component>(
component: T,

View File

@@ -1,8 +1,6 @@
import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';
import type { Language } from 'element-plus/es/locale';
import type { App } from 'vue';
import { ref } from 'vue';
import {
$t,
@@ -10,10 +8,10 @@ import {
loadLocalesMapFromDir,
} from '@vben/locales';
import { preferences } from '@vben/preferences';
import dayjs from 'dayjs';
import enLocale from 'element-plus/es/locale/lang/en';
import defaultLocale from 'element-plus/es/locale/lang/zh-cn';
import { ref } from 'vue';
const elementLocale = ref<Language>(defaultLocale);

View File

@@ -54,7 +54,9 @@ function setupAccessGuard(router: Router) {
if (coreRouteNames.includes(to.name as string)) {
if (to.path === LOGIN_PATH && accessStore.accessToken) {
return decodeURIComponent(
(to.query?.redirect as string) || DEFAULT_HOME_PATH,
(to.query?.redirect as string) ||
userStore.userInfo?.homePath ||
DEFAULT_HOME_PATH,
);
}
return true;
@@ -72,7 +74,10 @@ function setupAccessGuard(router: Router) {
return {
path: LOGIN_PATH,
// 如不需要,直接删除 query
query: { redirect: encodeURIComponent(to.fullPath) },
query:
to.fullPath === DEFAULT_HOME_PATH
? {}
: { redirect: encodeURIComponent(to.fullPath) },
// 携带当前跳转的页面,登录后重新跳转该页面
replace: true,
};
@@ -102,7 +107,10 @@ function setupAccessGuard(router: Router) {
accessStore.setAccessMenus(accessibleMenus);
accessStore.setAccessRoutes(accessibleRoutes);
accessStore.setIsAccessChecked(true);
const redirectPath = (from.query.redirect ?? to.fullPath) as string;
const redirectPath = (from.query.redirect ??
(to.path === DEFAULT_HOME_PATH
? userInfo.homePath || DEFAULT_HOME_PATH
: to.fullPath)) as string;
return {
...router.resolve(decodeURIComponent(redirectPath)),

View File

@@ -10,6 +10,7 @@ import { $t } from '@vben/locales';
defineOptions({ name: 'CodeLogin' });
const loading = ref(false);
const CODE_LENGTH = 6;
const formSchema = computed((): VbenFormSchema[] => {
return [
@@ -30,6 +31,7 @@ const formSchema = computed((): VbenFormSchema[] => {
{
component: 'VbenPinInput',
componentProps: {
codeLength: CODE_LENGTH,
createText: (countdown: number) => {
const text =
countdown > 0
@@ -41,7 +43,9 @@ const formSchema = computed((): VbenFormSchema[] => {
},
fieldName: 'code',
label: $t('authentication.code'),
rules: z.string().min(1, { message: $t('authentication.codeTip') }),
rules: z.string().length(CODE_LENGTH, {
message: $t('authentication.codeTip', [CODE_LENGTH]),
}),
},
];
});

View File

@@ -1,11 +1,8 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import type { EchartsUIType } from '@vben/plugins/echarts';
import {
EchartsUI,
type EchartsUIType,
useEcharts,
} from '@vben/plugins/echarts';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
import { onMounted, ref } from 'vue';
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);

View File

@@ -1,11 +1,8 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import type { EchartsUIType } from '@vben/plugins/echarts';
import {
EchartsUI,
type EchartsUIType,
useEcharts,
} from '@vben/plugins/echarts';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
import { onMounted, ref } from 'vue';
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);

View File

@@ -1,11 +1,8 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import type { EchartsUIType } from '@vben/plugins/echarts';
import {
EchartsUI,
type EchartsUIType,
useEcharts,
} from '@vben/plugins/echarts';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
import { onMounted, ref } from 'vue';
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);

View File

@@ -1,11 +1,8 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import type { EchartsUIType } from '@vben/plugins/echarts';
import {
EchartsUI,
type EchartsUIType,
useEcharts,
} from '@vben/plugins/echarts';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
import { onMounted, ref } from 'vue';
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);

View File

@@ -1,11 +1,8 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import type { EchartsUIType } from '@vben/plugins/echarts';
import {
EchartsUI,
type EchartsUIType,
useEcharts,
} from '@vben/plugins/echarts';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
import { onMounted, ref } from 'vue';
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);

View File

@@ -139,6 +139,7 @@ const [Form, formApi] = useVbenForm({
fieldName: 'select',
label: 'Select',
componentProps: {
filterable: true,
options: [
{ value: 'A', label: '选项A' },
{ value: 'B', label: '选项B' },

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/web-naive",
"version": "5.5.0",
"version": "5.5.2",
"homepage": "https://vben.pro",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -4,13 +4,11 @@
*/
import type { BaseFormComponentType } from '@vben/common-ui';
import type { Component, SetupContext } from 'vue';
import { h } from 'vue';
import { message } from '#/adapter/naive';
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
import { $t } from '@vben/locales';
import {
NButton,
NCheckbox,
@@ -29,8 +27,7 @@ import {
NTreeSelect,
NUpload,
} from 'naive-ui';
import { message } from '#/adapter/naive';
import { h } from 'vue';
const withDefaultPlaceholder = <T extends Component>(
component: T,

View File

@@ -10,8 +10,6 @@ import { $t } from '@vben/locales';
setupVbenForm<ComponentType>({
config: {
// naive-ui组件不接受onChang事件所以需要禁用
disabledOnChangeListener: true,
// naive-ui组件的空值为null,不能是undefined否则重置表单时不生效
emptyStateValue: null,
baseModelPropName: 'value',

View File

@@ -1,5 +1,4 @@
import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';
import type { App } from 'vue';
import {

View File

@@ -54,7 +54,9 @@ function setupAccessGuard(router: Router) {
if (coreRouteNames.includes(to.name as string)) {
if (to.path === LOGIN_PATH && accessStore.accessToken) {
return decodeURIComponent(
(to.query?.redirect as string) || DEFAULT_HOME_PATH,
(to.query?.redirect as string) ||
userStore.userInfo?.homePath ||
DEFAULT_HOME_PATH,
);
}
return true;
@@ -72,7 +74,10 @@ function setupAccessGuard(router: Router) {
return {
path: LOGIN_PATH,
// 如不需要,直接删除 query
query: { redirect: encodeURIComponent(to.fullPath) },
query:
to.fullPath === DEFAULT_HOME_PATH
? {}
: { redirect: encodeURIComponent(to.fullPath) },
// 携带当前跳转的页面,登录后重新跳转该页面
replace: true,
};
@@ -101,7 +106,10 @@ function setupAccessGuard(router: Router) {
accessStore.setAccessMenus(accessibleMenus);
accessStore.setAccessRoutes(accessibleRoutes);
accessStore.setIsAccessChecked(true);
const redirectPath = (from.query.redirect ?? to.fullPath) as string;
const redirectPath = (from.query.redirect ??
(to.path === DEFAULT_HOME_PATH
? userInfo.homePath || DEFAULT_HOME_PATH
: to.fullPath)) as string;
return {
...router.resolve(decodeURIComponent(redirectPath)),

View File

@@ -10,6 +10,7 @@ import { $t } from '@vben/locales';
defineOptions({ name: 'CodeLogin' });
const loading = ref(false);
const CODE_LENGTH = 6;
const formSchema = computed((): VbenFormSchema[] => {
return [
@@ -30,6 +31,7 @@ const formSchema = computed((): VbenFormSchema[] => {
{
component: 'VbenPinInput',
componentProps: {
codeLength: CODE_LENGTH,
createText: (countdown: number) => {
const text =
countdown > 0
@@ -41,7 +43,9 @@ const formSchema = computed((): VbenFormSchema[] => {
},
fieldName: 'code',
label: $t('authentication.code'),
rules: z.string().min(1, { message: $t('authentication.codeTip') }),
rules: z.string().length(CODE_LENGTH, {
message: $t('authentication.codeTip', [CODE_LENGTH]),
}),
},
];
});

View File

@@ -1,11 +1,8 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import type { EchartsUIType } from '@vben/plugins/echarts';
import {
EchartsUI,
type EchartsUIType,
useEcharts,
} from '@vben/plugins/echarts';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
import { onMounted, ref } from 'vue';
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);

View File

@@ -1,11 +1,8 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import type { EchartsUIType } from '@vben/plugins/echarts';
import {
EchartsUI,
type EchartsUIType,
useEcharts,
} from '@vben/plugins/echarts';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
import { onMounted, ref } from 'vue';
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);

View File

@@ -1,11 +1,8 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import type { EchartsUIType } from '@vben/plugins/echarts';
import {
EchartsUI,
type EchartsUIType,
useEcharts,
} from '@vben/plugins/echarts';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
import { onMounted, ref } from 'vue';
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);

View File

@@ -1,11 +1,8 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import type { EchartsUIType } from '@vben/plugins/echarts';
import {
EchartsUI,
type EchartsUIType,
useEcharts,
} from '@vben/plugins/echarts';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
import { onMounted, ref } from 'vue';
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);

View File

@@ -1,11 +1,8 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import type { EchartsUIType } from '@vben/plugins/echarts';
import {
EchartsUI,
type EchartsUIType,
useEcharts,
} from '@vben/plugins/echarts';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
import { onMounted, ref } from 'vue';
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import { Page } from '@vben/common-ui';
import type { NotificationType } from 'naive-ui';
import { type NotificationType } from 'naive-ui';
import { Page } from '@vben/common-ui';
import { NButton, NCard, NSpace, useMessage, useNotification } from 'naive-ui';
const notification = useNotification();

View File

@@ -1,4 +1,6 @@
import { type DefaultTheme, defineConfig } from 'vitepress';
import type { DefaultTheme } from 'vitepress';
import { defineConfig } from 'vitepress';
import { version } from '../../../package.json';

View File

@@ -1,4 +1,6 @@
import { type DefaultTheme, defineConfig } from 'vitepress';
import type { DefaultTheme } from 'vitepress';
import { defineConfig } from 'vitepress';
import { version } from '../../../package.json';
@@ -186,6 +188,10 @@ function sidebarComponents(): DefaultTheme.SidebarItem[] {
link: 'common-ui/vben-count-to-animator',
text: 'CountToAnimator 数字动画',
},
{
link: 'common-ui/vben-ellipsis-text',
text: 'EllipsisText 省略文本',
},
],
},
];

View File

@@ -10,7 +10,6 @@ import {
// import { useAntdDesignTokens } from '@vben/hooks';
// import { initPreferences } from '@vben/preferences';
import { ConfigProvider, theme } from 'ant-design-vue';
import mediumZoom from 'medium-zoom';
import { useRoute } from 'vitepress';

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/docs",
"version": "5.5.0",
"version": "5.5.2",
"private": true,
"scripts": {
"build": "vitepress build",

View File

@@ -4,13 +4,10 @@
*/
import type { BaseFormComponentType } from '@vben/common-ui';
import type { Component, SetupContext } from 'vue';
import { h } from 'vue';
import { globalShareState } from '@vben/common-ui';
import { $t } from '@vben/locales';
import {
AutoComplete,
Button,
@@ -35,6 +32,7 @@ import {
TreeSelect,
Upload,
} from 'ant-design-vue';
import { h } from 'vue';
const withDefaultPlaceholder = <T extends Component>(
component: T,

View File

@@ -14,8 +14,6 @@ initComponentAdapter();
setupVbenForm<ComponentType>({
config: {
baseModelPropName: 'value',
// naive-ui组件不接受onChang事件所以需要禁用
disabledOnChangeListener: true,
// naive-ui组件的空值为null,不能是undefined否则重置表单时不生效
emptyStateValue: null,
modelPropNameMap: {

View File

@@ -123,6 +123,8 @@ function fetchApi(): Promise<Record<string, any>> {
:::
## API
### Props
| 属性名 | 描述 | 类型 | 默认值 |

View File

@@ -54,6 +54,7 @@ Drawer 内的内容一般业务中,会比较复杂,所以我们可以将 dra
- `VbenDrawer` 组件对与参数的处理优先级是 `slot` > `props` > `state`(通过api更新的状态以及useVbenDrawer参数)。如果你已经传入了 `slot` 或者 `props`,那么 `setState` 将不会生效,这种情况下你可以通过 `slot` 或者 `props` 来更新状态。
- 如果你使用到了 `connectedComponent` 参数,那么会存在 2 个`useVbenDrawer`, 此时,如果同时设置了相同的参数,那么以内部为准(也就是没有设置 connectedComponent 的代码)。比如 同时设置了 `onConfirm`,那么以内部的 `onConfirm` 为准。`onOpenChange`事件除外,内外都会触发。
- 使用了`connectedComponent`参数时,可以配置`destroyOnClose`属性来决定当关闭弹窗时,是否要销毁`connectedComponent`组件(重新创建`connectedComponent`组件,这将会把其内部所有的变量、状态、数据等恢复到初始状态。)。
:::
@@ -75,12 +76,15 @@ const [Drawer, drawerApi] = useVbenDrawer({
| 属性名 | 描述 | 类型 | 默认值 |
| --- | --- | --- | --- |
| appendToMain | 是否挂载到内容区域默认挂载到body | `boolean` | `false` |
| connectedComponent | 连接另一个Modal组件 | `Component` | - |
| destroyOnClose | 关闭时销毁`connectedComponent` | `boolean` | `false` |
| title | 标题 | `string\|slot` | - |
| titleTooltip | 标题提示信息 | `string\|slot` | - |
| description | 描述信息 | `string\|slot` | - |
| isOpen | 弹窗打开状态 | `boolean` | `false` |
| loading | 弹窗加载状态 | `boolean` | `false` |
| closable | 显示关闭按钮 | `boolean` | `true` |
| closeIconPlacement | 关闭按钮位置 | `'left'\|'right'` | `right` |
| modal | 显示遮罩 | `boolean` | `true` |
| header | 显示header | `boolean` | `true` |
| footer | 显示footer | `boolean\|slot` | `true` |
@@ -108,12 +112,14 @@ const [Drawer, drawerApi] = useVbenDrawer({
以下事件,只有在 `useVbenDrawer({onCancel:()=>{}})` 中传入才会生效。
| 事件名 | 描述 | 类型 |
| --- | --- | --- |
| onBeforeClose | 关闭前触发,返回 `false`则禁止关闭 | `()=>boolean` |
| onCancel | 点击取消按钮触发 | `()=>void` |
| onConfirm | 点击确认按钮触发 | `()=>void` |
| onOpenChange | 关闭或者打开弹窗时触发 | `(isOpen:boolean)=>void` |
| 事件名 | 描述 | 类型 | 版本限制 |
| --- | --- | --- | --- |
| onBeforeClose | 关闭前触发,返回 `false`则禁止关闭 | `()=>boolean` | --- |
| onCancel | 点击取消按钮触发 | `()=>void` | --- |
| onClosed | 关闭动画播放完毕时触发 | `()=>void` | >5.5.2 |
| onConfirm | 点击确认按钮触发 | `()=>void` | --- |
| onOpenChange | 关闭或者打开弹窗时触发 | `(isOpen:boolean)=>void` | --- |
| onOpened | 打开动画播放完毕时触发 | `()=>void` | >5.5.2 |
### Slots
@@ -124,6 +130,8 @@ const [Drawer, drawerApi] = useVbenDrawer({
| default | 默认插槽 - 弹窗内容 |
| prepend-footer | 取消按钮左侧 |
| append-footer | 取消按钮右侧 |
| close-icon | 关闭按钮图标 |
| extra | 额外内容(标题右侧) |
### modalApi

View File

@@ -0,0 +1,56 @@
---
outline: deep
---
# Vben EllipsisText 省略文本
框架提供的文本展示组件可配置超长省略、tooltip提示、展开收起等功能。
> 如果文档内没有参数说明,可以尝试在在线示例内寻找
## 基础用法
通过默认插槽设置文本内容,`maxWidth`属性设置最大宽度。
<DemoPreview dir="demos/vben-ellipsis-text/line" />
## 可折叠的文本块
通过`line`设置折叠后的行数,`expand`属性设置是否支持展开收起。
<DemoPreview dir="demos/vben-ellipsis-text/expand" />
## 自定义提示浮层
通过名为`tooltip`的插槽定制提示信息。
<DemoPreview dir="demos/vben-ellipsis-text/tooltip" />
## API
### Props
| 属性名 | 描述 | 类型 | 默认值 |
| --- | --- | --- | --- |
| expand | 支持点击展开或收起 | `boolean` | `false` |
| line | 文本最大行数 | `number` | `1` |
| maxWidth | 文本区域最大宽度 | `number \| string` | `'100%'` |
| placement | 提示浮层的位置 | `'bottom'\|'left'\|'right'\|'top'` | `'top'` |
| tooltip | 启用文本提示 | `boolean` | `true` |
| tooltipBackgroundColor | 提示文本的背景颜色 | `string` | - |
| tooltipColor | 提示文本的颜色 | `string` | - |
| tooltipFontSize | 提示文本的大小 | `string` | - |
| tooltipMaxWidth | 提示浮层的最大宽度。如不设置则保持与文本宽度一致 | `number` | - |
| tooltipOverlayStyle | 提示框内容区域样式 | `CSSProperties` | `{ textAlign: 'justify' }` |
### Events
| 事件名 | 描述 | 类型 |
| ------------ | ------------ | -------------------------- |
| expandChange | 展开状态改变 | `(isExpand:boolean)=>void` |
### Slots
| 插槽名 | 描述 |
| ------- | -------------------------------- |
| tooltip | 启用文本提示时,用来定制提示内容 |

View File

@@ -287,6 +287,8 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
| setValues | 设置表单值, 默认会过滤不在schema中定义的field, 可通过filterFields形参关闭过滤 | `(fields: Record<string, any>, filterFields?: boolean, shouldValidate?: boolean) => Promise<void>` |
| getValues | 获取表单值 | `(fields:Record<string, any>,shouldValidate: boolean = false)=>Promise<void>` |
| validate | 表单校验 | `()=>Promise<void>` |
| validateField | 校验指定字段 | `(fieldName: string)=>Promise<ValidationResult<unknown>>` |
| isFieldValid | 检查某个字段是否已通过校验 | `(fieldName: string)=>Promise<boolean>` |
| resetValidate | 重置表单校验 | `()=>Promise<void>` |
| updateSchema | 更新formSchema | `(schema:FormSchema[])=>void` |
| setFieldValue | 设置字段值 | `(field: string, value: any, shouldValidate?: boolean)=>Promise<void>` |
@@ -311,14 +313,14 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
| resetButtonOptions | 重置按钮组件参数 | `ActionButtonOptions` | - |
| submitButtonOptions | 提交按钮组件参数 | `ActionButtonOptions` | - |
| showDefaultActions | 是否显示默认操作按钮 | `boolean` | `true` |
| collapsed | 是否折叠,在`是否展开,在showCollapseButton=true`时生效 | `boolean` | `false` |
| collapsed | 是否折叠,在`showCollapseButton``true`时生效 | `boolean` | `false` |
| collapseTriggerResize | 折叠时,触发`resize`事件 | `boolean` | `false` |
| collapsedRows | 折叠时保持的行数 | `number` | `1` |
| fieldMappingTime | 用于将表单内时间区域的应设成 2 个字段 | `[string, [string, string], string?][]` | - |
| fieldMappingTime | 用于将表单内时间区域组件的数组值映射成 2 个字段 | `[string, [string, string], string?][]` | - |
| commonConfig | 表单项的通用配置,每个配置都会传递到每个表单项,表单项可覆盖 | `FormCommonConfig` | - |
| schema | 表单项的每一项配置 | `FormSchema` | - |
| schema | 表单项的每一项配置 | `FormSchema[]` | - |
| submitOnEnter | 按下回车健时提交表单 | `boolean` | false |
| submitOnChange | 字段值改变时提交表单 | `boolean` | false |
| submitOnChange | 字段值改变时提交表单(内部防抖,这个属性一般用于表格的搜索表单) | `boolean` | false |
### TS 类型说明
@@ -355,10 +357,21 @@ export interface FormCommonConfig {
* 所有表单项的props
*/
componentProps?: ComponentProps;
/**
* 是否紧凑模式(移除表单底部为显示校验错误信息所预留的空间)。
* 在有设置校验规则的场景下建议不要将其设置为true
* 默认为false。但用作表格的搜索表单时默认为true
* @default false
*/
compact?: boolean;
/**
* 所有表单项的控件样式
*/
controlClass?: string;
/**
* 在表单项的Label后显示一个冒号
*/
colon?: boolean;
/**
* 所有表单项的禁用状态
* @default false
@@ -418,7 +431,7 @@ export interface FormSchema<
dependencies?: FormItemDependencies;
/** 描述 */
description?: string;
/** 字段名 */
/** 字段名,也作为自定义插槽的名称 */
fieldName: string;
/** 帮助信息 */
help?: string;
@@ -441,7 +454,7 @@ export interface FormSchema<
```ts
dependencies: {
// 只有当 name 字段值变时,才会触发联动
// 触发字段。只有这些字段值变时,联动才会触发
triggerFields: ['name'],
// 动态判断当前字段是否需要显示,不显示则直接销毁
if(values,formApi){},
@@ -462,11 +475,11 @@ dependencies: {
### 表单校验
表单联动需要通过 schema 内的 `rules` 属性进行配置。
表单校验需要通过 schema 内的 `rules` 属性进行配置。
rules的值可以是一个字符串也可以是一个zod的schema。
rules的值可以是字符串(预定义的校验规则名称)也可以是一个zod的schema。
#### 字符串
#### 预定义的校验规则
```ts
// 表示字段必填默认会根据适配器的required进行国际化
@@ -492,11 +505,16 @@ import { z } from '#/adapter/form';
rules: z.string().min(1, { message: '请输入字符串' });
}
// 可选,并且携带默认值
// 可选(可以是undefined)并且携带默认值。注意zod的optional不包括空字符串''
{
rules: z.string().default('默认值').optional(),
}
// 可以是空字符串、undefined或者一个邮箱地址
{
rules: z.union(z.string().email().optional(), z.literal(""))
}
// 复杂校验
{
z.string().min(1, { message: "请输入" })

View File

@@ -60,6 +60,7 @@ Modal 内的内容一般业务中,会比较复杂,所以我们可以将 moda
- `VbenModal` 组件对与参数的处理优先级是 `slot` > `props` > `state`(通过api更新的状态以及useVbenModal参数)。如果你已经传入了 `slot` 或者 `props`,那么 `setState` 将不会生效,这种情况下你可以通过 `slot` 或者 `props` 来更新状态。
- 如果你使用到了 `connectedComponent` 参数,那么会存在 2 个`useVbenModal`, 此时,如果同时设置了相同的参数,那么以内部为准(也就是没有设置 connectedComponent 的代码)。比如 同时设置了 `onConfirm`,那么以内部的 `onConfirm` 为准。`onOpenChange`事件除外,内外都会触发。
- 使用了`connectedComponent`参数时,可以配置`destroyOnClose`属性来决定当关闭弹窗时,是否要销毁`connectedComponent`组件(重新创建`connectedComponent`组件,这将会把其内部所有的变量、状态、数据等恢复到初始状态。)。
:::
@@ -81,6 +82,8 @@ const [Modal, modalApi] = useVbenModal({
| 属性名 | 描述 | 类型 | 默认值 |
| --- | --- | --- | --- |
| appendToMain | 是否挂载到内容区域默认挂载到body | `boolean` | `false` |
| connectedComponent | 连接另一个Modal组件 | `Component` | - |
| destroyOnClose | 关闭时销毁`connectedComponent` | `boolean` | `false` |
| title | 标题 | `string\|slot` | - |
| titleTooltip | 标题提示信息 | `string\|slot` | - |
| description | 描述信息 | `string\|slot` | - |

View File

@@ -165,6 +165,8 @@ vxeUI.renderer.add('CellLink', {
**表单搜索** 部分采用了`Vben Form 表单`,参考 [Vben Form 表单文档](/components/common-ui/vben-form)。
当启用了表单搜索时可以在toolbarConfig中配置`search``true`来让表格在工具栏区域显示一个搜索表单控制按钮。
<DemoPreview dir="demos/vben-vxe-table/form" />
## 单元格编辑
@@ -215,14 +217,15 @@ const [Grid, gridApi] = useVbenVxeGrid({
useVbenVxeGrid 返回的第二个参数,是一个对象,包含了一些表单的方法。
| 方法名 | 描述 | 类型 |
| --- | --- | --- |
| setLoading | 设置loading状态 | `(loading)=>void` |
| setGridOptions | 设置vxe-table grid组件参数 | `(options: Partial<VxeGridProps['gridOptions'])=>void` |
| reload | 重载表格,会进行初始化 | `(params:any)=>void` |
| query | 重载表格,会保留当前分页 | `(params:any)=>void` |
| grid | vxe-table grid实例 | `VxeGridInstance` |
| formApi | vbenForm api实例 | `FormApi` |
| 方法名 | 描述 | 类型 | 说明 |
| --- | --- | --- | --- |
| setLoading | 设置loading状态 | `(loading)=>void` | - |
| setGridOptions | 设置vxe-table grid组件参数 | `(options: Partial<VxeGridProps['gridOptions'])=>void` | - |
| reload | 重载表格,会进行初始化 | `(params:any)=>void` | - |
| query | 重载表格,会保留当前分页 | `(params:any)=>void` | - |
| grid | vxe-table grid实例 | `VxeGridInstance` | - |
| formApi | vbenForm api实例 | `FormApi` | - |
| toggleSearchForm | 设置搜索表单显示状态 | `(show?: boolean)=>boolean` | 当省略参数时,则将表单在显示和隐藏两种状态之间切换 |
## Props
@@ -236,3 +239,4 @@ useVbenVxeGrid 返回的第二个参数,是一个对象,包含了一些表
| gridOptions | grid组件的参数 | `VxeTableGridProps` |
| gridEvents | grid组件的触发的⌚ | `VxeGridListeners` |
| formOptions | 表单参数 | `VbenFormProps` |
| showSearchForm | 是否显示搜索表单 | `boolean` |

View File

@@ -0,0 +1,10 @@
<script lang="ts" setup>
import { EllipsisText } from '@vben/common-ui';
const text = `
Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、多主题配置、按钮级别权限控制等功能。项目会使用前端较新的技术栈可以作为项目的启动模版以帮助你快速搭建企业级中后台产品原型。也可以作为一个示例用于学习 vue3、vite、ts 等主流技术。该项目会持续跟进最新技术并将其应用在项目中。Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、多主题配置、按钮级别权限控制等功能。项目会使用前端较新的技术栈可以作为项目的启动模版以帮助你快速搭建企业级中后台产品原型。也可以作为一个示例用于学习 vue3、vite、ts 等主流技术。该项目会持续跟进最新技术并将其应用在项目中。Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、多主题配置、按钮级别权限控制等功能。项目会使用前端较新的技术栈可以作为项目的启动模版以帮助你快速搭建企业级中后台产品原型。也可以作为一个示例用于学习 vue3、vite、ts 等主流技术。该项目会持续跟进最新技术并将其应用在项目中。Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、多主题配置、按钮级别权限控制等功能。项目会使用前端较新的技术栈可以作为项目的启动模版以帮助你快速搭建企业级中后台产品原型。也可以作为一个示例用于学习 vue3、vite、ts 等主流技术。该项目会持续跟进最新技术,并将其应用在项目中。
`;
</script>
<template>
<EllipsisText :line="3" expand>{{ text }}</EllipsisText>
</template>

View File

@@ -0,0 +1,10 @@
<script lang="ts" setup>
import { EllipsisText } from '@vben/common-ui';
const text = `
Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、多主题配置、按钮级别权限控制等功能。项目会使用前端较新的技术栈可以作为项目的启动模版以帮助你快速搭建企业级中后台产品原型。也可以作为一个示例用于学习 vue3、vite、ts 等主流技术。该项目会持续跟进最新技术并将其应用在项目中。Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、多主题配置、按钮级别权限控制等功能。项目会使用前端较新的技术栈可以作为项目的启动模版以帮助你快速搭建企业级中后台产品原型。也可以作为一个示例用于学习 vue3、vite、ts 等主流技术。该项目会持续跟进最新技术并将其应用在项目中。Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、多主题配置、按钮级别权限控制等功能。项目会使用前端较新的技术栈可以作为项目的启动模版以帮助你快速搭建企业级中后台产品原型。也可以作为一个示例用于学习 vue3、vite、ts 等主流技术。该项目会持续跟进最新技术并将其应用在项目中。Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、多主题配置、按钮级别权限控制等功能。项目会使用前端较新的技术栈可以作为项目的启动模版以帮助你快速搭建企业级中后台产品原型。也可以作为一个示例用于学习 vue3、vite、ts 等主流技术。该项目会持续跟进最新技术,并将其应用在项目中。
`;
</script>
<template>
<EllipsisText :max-width="500">{{ text }}</EllipsisText>
</template>

View File

@@ -0,0 +1,14 @@
<script lang="ts" setup>
import { EllipsisText } from '@vben/common-ui';
</script>
<template>
<EllipsisText :max-width="240">
住在我心里孤独的 孤独的海怪 痛苦之王 开始厌倦 深海的光 停滞的海浪
<template #tooltip>
<div style="text-align: center">
秦皇岛<br />住在我心里孤独的<br />孤独的海怪 痛苦之王<br />开始厌倦
深海的光 停滞的海浪
</div>
</template>
</EllipsisText>
</template>

View File

@@ -110,6 +110,11 @@ const gridOptions: VxeGridProps<RowType> = {
},
},
},
toolbarConfig: {
// 是否显示搜索表单控制按钮
// @ts-ignore 正式环境时有完整的类型声明
search: true,
},
};
const [Grid] = useVbenVxeGrid({ formOptions, gridOptions });

View File

@@ -217,6 +217,7 @@ const defaultPreferences: Preferences = {
globalSearch: true,
},
sidebar: {
autoActivateChild: false,
collapsed: false,
collapsedShowTitle: false,
enable: true,

View File

@@ -240,6 +240,7 @@ const defaultPreferences: Preferences = {
globalSearch: true,
},
sidebar: {
autoActivateChild: false,
collapsed: false,
collapsedShowTitle: false,
enable: true,

View File

@@ -4,7 +4,7 @@
## 新增组件库应用
如果你想用其他别的组件库,你只需要按下步骤进行操作:
如果你想用其他别的组件库,你只需要按下步骤进行操作:
1.`apps`内创建一个新的文件夹,例如`apps/web-xxx`
2. 更改`apps/web-xxx/package.json``name`字段为`web-xxx`

View File

@@ -67,7 +67,7 @@ pnpm install
::: tip 注意
- 项目只支持使用 `pnpm` 进行依赖安装,默认会使用 `corepack` 来安装指定版本的 `pnpm`。:
- 如果你的网络环境无法访问npm源你可以设置系统的环境变量`COREPACK_REGISTRY=https://registry.npmmirror.com`,然后再执行`pnpm install`。
- 如果你的网络环境无法访问npm源你可以设置系统的环境变量`COREPACK_NPM_REGISTRY=https://registry.npmmirror.com`,然后再执行`pnpm install`。
- 如果你不想使用`corepack`,你需要禁用`corepack`,然后使用你自己的`pnpm`进行安装。
:::

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/commitlint-config",
"version": "5.5.0",
"version": "5.5.2",
"private": true,
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",

View File

@@ -10,6 +10,7 @@ export async function importPluginConfig(): Promise<Linter.Config[]> {
import: pluginImport,
},
rules: {
'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
'import/first': 'error',
'import/newline-after-import': 'error',
'import/no-duplicates': 'error',

View File

@@ -1,6 +1,5 @@
import type { Linter } from 'eslint';
// @ts-expect-error - no types
import js from '@eslint/js';
import pluginUnusedImports from 'eslint-plugin-unused-imports';
import globals from 'globals';

View File

@@ -1,8 +1,13 @@
import type { Linter } from 'eslint';
import perfectionistPlugin from 'eslint-plugin-perfectionist';
import { interopDefault } from '../util';
export async function perfectionist(): Promise<Linter.Config[]> {
const perfectionistPlugin = await interopDefault(
// @ts-expect-error - no types
import('eslint-plugin-perfectionist'),
);
return [
perfectionistPlugin.configs['recommended-natural'],
{
@@ -19,21 +24,28 @@ export async function perfectionist(): Promise<Linter.Config[]> {
{
customGroups: {
type: {
vben: 'vben',
vue: 'vue',
'vben-core-type': ['^@vben-core/.+'],
'vben-type': ['^@vben/.+'],
'vue-type': ['^vue$', '^vue-.+', '^@vue/.+'],
},
value: {
vben: ['@vben*', '@vben/**/**', '@vben-core/**/**'],
vue: ['vue', 'vue-*', '@vue*'],
vben: ['^@vben/.+'],
'vben-core': ['^@vben-core/.+'],
vue: ['^vue$', '^vue-.+', '^@vue/.+'],
},
},
environment: 'node',
groups: [
['external-type', 'builtin-type', 'type'],
'vue-type',
'vben-type',
'vben-core-type',
['parent-type', 'sibling-type', 'index-type'],
['internal-type'],
'builtin',
'vue',
'vben',
'vben-core',
'external',
'internal',
['parent', 'sibling', 'index'],
@@ -43,12 +55,13 @@ export async function perfectionist(): Promise<Linter.Config[]> {
'object',
'unknown',
],
internalPattern: ['#*', '#*/**'],
internalPattern: ['^#/.+'],
newlinesBetween: 'always',
order: 'asc',
type: 'natural',
},
],
'perfectionist/sort-modules': 'off',
'perfectionist/sort-named-exports': [
'error',
{
@@ -67,42 +80,6 @@ export async function perfectionist(): Promise<Linter.Config[]> {
groups: ['unknown', 'items', 'list', 'children'],
ignorePattern: ['children'],
order: 'asc',
partitionByComment: 'Part:**',
type: 'natural',
},
],
'perfectionist/sort-vue-attributes': [
'error',
{
// Based on: https://vuejs.org/style-guide/rules-recommended.html#element-attribute-order
customGroups: {
/* eslint-disable perfectionist/sort-objects */
DEFINITION: '*(is|:is|v-is)',
LIST_RENDERING: 'v-for',
CONDITIONALS: 'v-*(else-if|if|else|show|cloak)',
RENDER_MODIFIERS: 'v-*(pre|once)',
GLOBAL: '*(:id|id)',
UNIQUE: '*(ref|key|:ref|:key)',
SLOT: '*(v-slot|slot)',
TWO_WAY_BINDING: '*(v-model|v-model:*)',
// OTHER_DIRECTIVES e.g. 'v-custom-directive'
EVENTS: '*(v-on|@*)',
CONTENT: 'v-*(html|text)',
/* eslint-enable perfectionist/sort-objects */
},
groups: [
'DEFINITION',
'LIST_RENDERING',
'CONDITIONALS',
'RENDER_MODIFIERS',
'GLOBAL',
'UNIQUE',
'SLOT',
'TWO_WAY_BINDING',
'unknown',
'EVENTS',
'CONTENT',
],
type: 'natural',
},
],

View File

@@ -4,7 +4,6 @@ import { interopDefault } from '../util';
export async function vue(): Promise<Linter.Config[]> {
const [pluginVue, parserVue, parserTs] = await Promise.all([
// @ts-expect-error missing types
interopDefault(import('eslint-plugin-vue')),
interopDefault(import('vue-eslint-parser')),
// @ts-expect-error missing types

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/stylelint-config",
"version": "5.5.0",
"version": "5.5.2",
"private": true,
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/node-utils",
"version": "5.5.0",
"version": "5.5.2",
"private": true,
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",

View File

@@ -1,4 +1,6 @@
import ora, { type Ora } from 'ora';
import type { Ora } from 'ora';
import ora from 'ora';
interface SpinnerOptions {
failedText?: string;

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/tailwind-config",
"version": "5.5.0",
"version": "5.5.2",
"private": true,
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/tsconfig",
"version": "5.5.0",
"version": "5.5.2",
"private": true,
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/vite-config",
"version": "5.5.0",
"version": "5.5.2",
"private": true,
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",

View File

@@ -1,4 +1,4 @@
import type { UserConfig } from 'vite';
import type { CSSOptions, UserConfig } from 'vite';
import type { DefineApplicationOptions } from '../typing';
@@ -100,7 +100,7 @@ function defineApplicationConfig(userConfigPromise?: DefineApplicationOptions) {
});
}
function createCssOptions(injectGlobalScss = true) {
function createCssOptions(injectGlobalScss = true): CSSOptions {
const root = findMonorepoRoot();
return {
preprocessorOptions: injectGlobalScss

View File

@@ -1,3 +1,5 @@
import type { PluginOption } from 'vite';
import fs from 'node:fs';
import fsp from 'node:fs/promises';
import { join } from 'node:path';
@@ -5,8 +7,6 @@ import { fileURLToPath } from 'node:url';
import { readPackageJSON } from '@vben/node-utils';
import { type PluginOption } from 'vite';
/**
* 用于生成将loading样式注入到项目中
* 为多app提供loading样式无需在每个 app -> index.html单独引入

Some files were not shown because too many files have changed in this diff Show More