chore: 升级 shadcn-vue 组件到v4最新版 (#7972)

* fix: useStore is deprecated

* chore: update deps

* feat: 升级shadcn-ui v4

* fix: workbench style

* feat: 升级shadcn-ui v4 step2

* feat: 升级shadcn-ui v4 step3

* chore: 升级shadcn v4

* fix: pagination

* fix: dark style

* fix: doc import

* feat: 增加详情组件,参考 antdv-next

* docs: descriptions docs

* docs: Browser Support

* feat: add table action

* feat: icon use vbenIcon

* fix: type error

* fix: dropdown popConfirm

* feat: 使用默认的文字交互

* feat: 优化渲染性能
This commit is contained in:
xingyu
2026-05-31 15:18:46 +08:00
committed by GitHub
parent 108d7ff335
commit 04fbb7a556
239 changed files with 6015 additions and 3205 deletions

View File

@@ -1,10 +1,13 @@
import type { TableActionProps } from '@vben/common-ui';
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
import type { Recordable } from '@vben/types';
import type { ComponentPropsMap, ComponentType } from './component';
import { h } from 'vue';
import { defineComponent, h } from 'vue';
import { useAccess } from '@vben/access';
import { VbenTableAction as VbenTableActionCore } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import { $te } from '@vben/locales';
import {
@@ -156,9 +159,12 @@ setupVbenVxeTable({
edit: {
text: $t('common.edit'),
},
detail: {
text: $t('common.detail'),
},
};
const operations: Array<Recordable<any>> = (
options || ['edit', 'delete']
options || ['edit', 'detail', 'delete']
)
.map((opt) => {
if (isString(opt)) {
@@ -285,6 +291,29 @@ export const useVbenVxeGrid = <T extends Record<string, any>>(
...rest: Parameters<typeof useGrid<T, ComponentType, ComponentPropsMap>>
) => useGrid<T, ComponentType, ComponentPropsMap>(...rest);
/**
* 表格操作按钮组件
*
* 在适配器内部统一注入权限判断hasPermission使用方无需再传入 `:has-permission`。
* 通过 action 的 `auth` 字段声明权限码,结合 `useAccess().hasAccessByCodes` 判断是否展示。
* 如需自定义权限逻辑,仍可显式传入 `:has-permission` 覆盖默认行为。
*/
export const VbenTableAction = defineComponent(
(props: TableActionProps, { attrs, slots }) => {
const { hasAccessByCodes } = useAccess();
function hasPermission(auth?: string | string[]) {
if (!auth) return true;
return hasAccessByCodes(Array.isArray(auth) ? auth : [auth]);
}
return () =>
h(VbenTableActionCore, { hasPermission, ...props, ...attrs }, slots);
},
{
name: 'VbenTableAction',
inheritAttrs: false,
},
);
export type OnActionClickParams<T = Recordable<any>> = {
code: string;
row: T;

View File

@@ -86,7 +86,7 @@
"owner": "Owner"
},
"preferencesExtensionConfig": {
"tabLabel": "Playground Extension",
"tabLabel": "Extension",
"title": "Playground Business Preferences",
"fields": {
"reportTitle": {

View File

@@ -87,7 +87,7 @@
"owner": "负责人"
},
"preferencesExtensionConfig": {
"tabLabel": "Playground 拓展",
"tabLabel": "拓展",
"title": "Playground 业务偏好",
"fields": {
"reportTitle": {

View File

@@ -252,7 +252,7 @@ function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
<div class="w-full lg:w-2/5">
<WorkbenchQuickNav
:items="quickNavItems"
class="mt-5 lg:mt-0"
class="lg:mt-0"
title="快捷导航"
@click="navTo"
/>

View File

@@ -141,7 +141,7 @@ async function fillPartialData() {
</template>
<div class="space-y-4">
<div class="rounded-sm bg-blue-50 p-4">
<div class="rounded-sm bg-blue-50 dark:bg-blue-900 p-4">
<h3 class="mb-2 font-medium">测试说明</h3>
<ul class="list-inside list-disc space-y-1 text-sm">
<li>所有验证方法在验证失败时都会自动滚动到第一个错误字段</li>

View File

@@ -64,10 +64,12 @@ const leftMaxWidth = ref(props.leftMaxWidth || 100);
<Card class="ml-2" title="基本使用">
<div class="flex flex-col gap-2">
<div class="flex gap-2">
<Checkbox v-model="props.resizable">可拖动调整宽度</Checkbox>
<Checkbox v-model="props.splitLine">显示拖动分隔线</Checkbox>
<Checkbox v-model="props.splitHandle">显示拖动手柄</Checkbox>
<Checkbox v-model="props.leftCollapsible"> 左侧可折叠 </Checkbox>
<Checkbox v-model:checked="props.resizable">可拖动调整宽度</Checkbox>
<Checkbox v-model:checked="props.splitLine">显示拖动分隔线</Checkbox>
<Checkbox v-model:checked="props.splitHandle">显示拖动手柄</Checkbox>
<Checkbox v-model:checked="props.leftCollapsible">
左侧可折叠
</Checkbox>
</div>
<div class="flex items-center gap-2">
<span>左侧最小宽度百分比</span>

View File

@@ -1,7 +1,13 @@
import type { DescriptionsItemType } from '@vben/common-ui';
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridColumns } from '#/adapter/vxe-table';
import type { VxeTableGridColumns } from '#/adapter/vxe-table';
import type { SystemUserApi } from '#/api';
import { h } from 'vue';
import { Tag } from 'antdv-next';
import { getDeptList } from '#/api';
import { $t } from '#/locales';
@@ -82,8 +88,38 @@ export function useGridFormSchema(): VbenFormSchema[] {
];
}
/**
* 用户详情描述列表项
* @param row 用户数据
*/
export function useDescriptionItems(
row?: SystemUserApi.SystemUser,
): DescriptionsItemType[] {
const enabled = row?.status === 1;
return [
{ label: $t('system.user.name'), content: row?.name },
{ label: $t('system.user.id'), content: row?.id },
{ label: $t('system.user.dept'), content: row?.deptId },
{
label: $t('system.user.status'),
content: () =>
h(
Tag,
{
color: enabled ? 'success' : 'error',
},
{
default: () =>
enabled ? $t('common.enabled') : $t('common.disabled'),
},
),
},
{ label: $t('system.user.createTime'), content: row?.createTime },
{ label: $t('system.user.remark'), content: row?.remark },
];
}
export function useColumns<T = SystemUserApi.SystemUser>(
onActionClick: OnActionClickFn<T>,
onStatusChange?: (newStatus: any, row: T) => PromiseLike<boolean | undefined>,
): VxeTableGridColumns {
return [
@@ -118,18 +154,11 @@ export function useColumns<T = SystemUserApi.SystemUser>(
},
{
align: 'center',
cellRender: {
attrs: {
nameField: 'name',
nameTitle: $t('system.user.name'),
onClick: onActionClick,
},
name: 'CellOperation',
},
field: 'operation',
fixed: 'right',
slots: { default: 'action' },
title: $t('system.user.operation'),
width: 130,
width: 180,
},
];
}

View File

@@ -1,10 +1,7 @@
<script lang="ts" setup>
import type { Recordable } from '@vben/types';
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemDeptApi, SystemUserApi } from '#/api';
import { onMounted, ref, watch } from 'vue';
@@ -14,11 +11,12 @@ import { Plus } from '@vben/icons';
import { Button, Card, InputSearch, message, Modal } from 'antdv-next';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { useVbenVxeGrid, VbenTableAction } from '#/adapter/vxe-table';
import { deleteUser, getDeptList, getUserList, updateUser } from '#/api';
import { $t } from '#/locales';
import { useColumns, useGridFormSchema } from './data';
import Detail from './modules/detail.vue';
import Form from './modules/form.vue';
const deptList = ref<SystemDeptApi.SystemDept[]>([]);
@@ -30,6 +28,11 @@ const [FormDrawer, formDrawerApi] = useVbenDrawer({
destroyOnClose: true,
});
const [DetailDrawer, detailDrawerApi] = useVbenDrawer({
connectedComponent: Detail,
destroyOnClose: true,
});
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
fieldMappingTime: [['createTime', ['startTime', 'endTime']]],
@@ -37,7 +40,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
submitOnChange: true,
},
gridOptions: {
columns: useColumns(onActionClick, onStatusChange),
columns: useColumns(onStatusChange),
height: 'auto',
keepSource: true,
proxyConfig: {
@@ -66,19 +69,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
} as VxeTableGridOptions<SystemUserApi.SystemUser>,
});
function onActionClick(e: OnActionClickParams<SystemUserApi.SystemUser>) {
switch (e.code) {
case 'delete': {
onDelete(e.row);
break;
}
case 'edit': {
onEdit(e.row);
break;
}
}
}
/**
* 将Antd的Modal.confirm封装为promise方便在异步函数中调用。
* @param content 提示内容
@@ -129,6 +119,10 @@ function onEdit(row: SystemUserApi.SystemUser) {
formDrawerApi.setData(row).open();
}
function onDetail(row: SystemUserApi.SystemUser) {
detailDrawerApi.setData(row).open();
}
function onDelete(row: SystemUserApi.SystemUser) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
@@ -192,6 +186,7 @@ watch(inputSearchValue, (value) => {
<template>
<Page auto-content-height>
<FormDrawer @success="onRefresh" />
<DetailDrawer @success="onRefresh" />
<div class="flex size-full">
<Card class="w-1/6">
<InputSearch
@@ -215,6 +210,35 @@ watch(inputSearchValue, (value) => {
{{ $t('ui.actionTitle.create', [$t('system.user.name')]) }}
</Button>
</template>
<template #action="{ row }">
<VbenTableAction
:actions="[
{
text: $t('common.detail'),
icon: 'lucide:eye',
onClick: () => onDetail(row),
},
{
text: $t('common.edit'),
icon: 'lucide:edit',
onClick: () => onEdit(row),
},
]"
:dropdown-actions="[
{
text: $t('common.delete'),
icon: 'lucide:trash-2',
danger: true,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: () => onDelete(row),
},
auth: ['AC_100100'],
},
]"
align="center"
/>
</template>
</Grid>
</div>
</div>

View File

@@ -0,0 +1,28 @@
<script lang="ts" setup>
import type { SystemUserApi } from '#/api/system/user';
import { computed, ref } from 'vue';
import { useVbenDrawer, VbenDescriptions } from '@vben/common-ui';
import { $t } from '#/locales';
import { useDescriptionItems } from '../data';
const detailData = ref<SystemUserApi.SystemUser>();
const items = computed(() => useDescriptionItems(detailData.value));
const [Drawer, drawerApi] = useVbenDrawer({
onOpenChange(isOpen) {
if (isOpen) {
detailData.value = drawerApi.getData<SystemUserApi.SystemUser>();
}
},
});
</script>
<template>
<Drawer :footer="false" :title="$t('common.detail')">
<VbenDescriptions bordered :column="1" :items="items" />
</Drawer>
</template>

View File

@@ -8,7 +8,6 @@ import type { SystemUserApi } from '#/api/system/user';
import { computed, nextTick, ref } from 'vue';
import { Tree, useVbenDrawer } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import { Spin } from 'antdv-next';
@@ -110,29 +109,10 @@ function getNodeClass(node: Recordable<any>) {
:get-node-class="getNodeClass"
v-bind="slotProps"
value-field="id"
label-field="meta.title"
icon-field="meta.icon"
>
<template #node="{ value }">
<IconifyIcon v-if="value.meta.icon" :icon="value.meta.icon" />
{{ $t(value.meta.title) }}
</template>
</Tree>
label-field="name"
/>
</Spin>
</template>
</Form>
</Drawer>
</template>
<style lang="css" scoped>
:deep(.ant-tree-title) {
.tree-actions {
@apply ml-5 hidden;
}
}
:deep(.ant-tree-title:hover) {
.tree-actions {
@apply ml-5 flex flex-auto justify-end;
}
}
</style>