mirror of
https://github.com/vbenjs/vue-vben-admin.git
synced 2025-08-25 16:16:20 +08:00
Compare commits
12 Commits
v5.5.9
...
dependabot
Author | SHA1 | Date | |
---|---|---|---|
![]() |
6d4d5653c1 | ||
![]() |
cf6c4c9aae | ||
![]() |
ffaf85c8f3 | ||
![]() |
2cc78f925f | ||
![]() |
93f0eea4e7 | ||
![]() |
58e3941810 | ||
![]() |
3ad433a50b | ||
![]() |
8ac2db5b7c | ||
![]() |
a441dcebae | ||
![]() |
ff4704d5ea | ||
![]() |
6ddfbd84b0 | ||
![]() |
1e6417f95b |
@@ -140,8 +140,12 @@ pnpm build
|
||||
|
||||
## 貢献者
|
||||
|
||||
<a href="https://openomy.app/github/vbenjs/vue-vben-admin" target="_blank" style="display: block; width: 100%;" align="center">
|
||||
<img src="https://openomy.app/svg?repo=vbenjs/vue-vben-admin&chart=bubble&latestMonth=3" target="_blank" alt="Contribution Leaderboard" style="display: block; width: 100%;" />
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
|
||||
<img alt="Contributors" src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
|
||||
<img alt="Contributors" src="https://contrib.rocks/image?repo=vbenjs/vue-vben-admin" />
|
||||
</a>
|
||||
|
||||
## Discord
|
||||
|
@@ -140,8 +140,12 @@ If you think this project is helpful to you, you can help the author buy a cup o
|
||||
|
||||
## Contributors
|
||||
|
||||
<a href="https://openomy.app/github/vbenjs/vue-vben-admin" target="_blank" style="display: block; width: 100%;" align="center">
|
||||
<img src="https://openomy.app/svg?repo=vbenjs/vue-vben-admin&chart=bubble&latestMonth=3" target="_blank" alt="Contribution Leaderboard" style="display: block; width: 100%;" />
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
|
||||
<img alt="Contributors" src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
|
||||
<img alt="Contributors" src="https://contrib.rocks/image?repo=vbenjs/vue-vben-admin" />
|
||||
</a>
|
||||
|
||||
## Discord
|
||||
|
@@ -140,8 +140,12 @@ pnpm build
|
||||
|
||||
## 贡献者
|
||||
|
||||
<a href="https://openomy.app/github/vbenjs/vue-vben-admin" target="_blank" style="display: block; width: 100%;" align="center">
|
||||
<img src="https://openomy.app/svg?repo=vbenjs/vue-vben-admin&chart=bubble&latestMonth=3" target="_blank" alt="Contribution Leaderboard" style="display: block; width: 100%;" />
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
|
||||
<img alt="Contributors" src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
|
||||
<img alt="Contributors" src="https://contrib.rocks/image?repo=vbenjs/vue-vben-admin" />
|
||||
</a>
|
||||
|
||||
## Discord
|
||||
|
@@ -90,30 +90,52 @@ import { h } from 'vue';
|
||||
import { globalShareState, IconPicker } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
AutoComplete,
|
||||
Button,
|
||||
Checkbox,
|
||||
CheckboxGroup,
|
||||
DatePicker,
|
||||
Divider,
|
||||
Input,
|
||||
InputNumber,
|
||||
InputPassword,
|
||||
Mentions,
|
||||
notification,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
RangePicker,
|
||||
Rate,
|
||||
Select,
|
||||
Space,
|
||||
Switch,
|
||||
Textarea,
|
||||
TimePicker,
|
||||
TreeSelect,
|
||||
Upload,
|
||||
} from 'ant-design-vue';
|
||||
const AutoComplete = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/auto-complete'),
|
||||
);
|
||||
const Button = defineAsyncComponent(() => import('ant-design-vue/es/button'));
|
||||
const Checkbox = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/checkbox'),
|
||||
);
|
||||
const CheckboxGroup = defineAsyncComponent(() =>
|
||||
import('ant-design-vue/es/checkbox').then((res) => res.CheckboxGroup),
|
||||
);
|
||||
const DatePicker = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/date-picker'),
|
||||
);
|
||||
const Divider = defineAsyncComponent(() => import('ant-design-vue/es/divider'));
|
||||
const Input = defineAsyncComponent(() => import('ant-design-vue/es/input'));
|
||||
const InputNumber = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/input-number'),
|
||||
);
|
||||
const InputPassword = defineAsyncComponent(() =>
|
||||
import('ant-design-vue/es/input').then((res) => res.InputPassword),
|
||||
);
|
||||
const Mentions = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/mentions'),
|
||||
);
|
||||
const Radio = defineAsyncComponent(() => import('ant-design-vue/es/radio'));
|
||||
const RadioGroup = defineAsyncComponent(() =>
|
||||
import('ant-design-vue/es/radio').then((res) => res.RadioGroup),
|
||||
);
|
||||
const RangePicker = defineAsyncComponent(() =>
|
||||
import('ant-design-vue/es/date-picker').then((res) => res.RangePicker),
|
||||
);
|
||||
const Rate = defineAsyncComponent(() => import('ant-design-vue/es/rate'));
|
||||
const Select = defineAsyncComponent(() => import('ant-design-vue/es/select'));
|
||||
const Space = defineAsyncComponent(() => import('ant-design-vue/es/space'));
|
||||
const Switch = defineAsyncComponent(() => import('ant-design-vue/es/switch'));
|
||||
const Textarea = defineAsyncComponent(() =>
|
||||
import('ant-design-vue/es/input').then((res) => res.Textarea),
|
||||
);
|
||||
const TimePicker = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/time-picker'),
|
||||
);
|
||||
const TreeSelect = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/tree-select'),
|
||||
);
|
||||
const Upload = defineAsyncComponent(() => import('ant-design-vue/es/upload'));
|
||||
|
||||
|
||||
const withDefaultPlaceholder = <T extends Component>(
|
||||
component: T,
|
||||
@@ -304,7 +326,7 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
|
||||
|
||||
| 属性名 | 描述 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
| layout | 表单项布局 | `'horizontal' \| 'vertical'` | `horizontal` |
|
||||
| layout | 表单项布局 | `'horizontal' \| 'vertical'\| 'inline'` | `horizontal` |
|
||||
| showCollapseButton | 是否显示折叠按钮 | `boolean` | `false` |
|
||||
| wrapperClass | 表单的布局,基于tailwindcss | `any` | - |
|
||||
| actionWrapperClass | 表单操作区域class | `any` | - |
|
||||
|
@@ -98,7 +98,7 @@
|
||||
"node": ">=20.10.0",
|
||||
"pnpm": ">=9.12.0"
|
||||
},
|
||||
"packageManager": "pnpm@10.12.4",
|
||||
"packageManager": "pnpm@10.14.0",
|
||||
"pnpm": {
|
||||
"peerDependencyRules": {
|
||||
"allowedVersions": {
|
||||
|
@@ -30,7 +30,7 @@ function openWindow(url: string, options: OpenWindowOptions = {}): void {
|
||||
function openRouteInNewWindow(path: string) {
|
||||
const { hash, origin } = location;
|
||||
const fullPath = path.startsWith('/') ? path : `/${path}`;
|
||||
const url = `${origin}${hash ? '/#' : ''}${fullPath}`;
|
||||
const url = `${origin}${hash && !fullPath.startsWith('/#') ? '/#' : ''}${fullPath}`;
|
||||
openWindow(url, { target: '_blank' });
|
||||
}
|
||||
|
||||
|
@@ -82,11 +82,11 @@ const actionWrapperClass = computed(() => {
|
||||
|
||||
const cls = [
|
||||
'flex',
|
||||
'w-full',
|
||||
'items-center',
|
||||
'gap-3',
|
||||
props.compact ? 'pb-2' : 'pb-4',
|
||||
props.layout === 'vertical' ? 'self-end' : 'self-center',
|
||||
props.layout === 'inline' ? '' : 'w-full',
|
||||
props.actionWrapperClass,
|
||||
];
|
||||
|
||||
|
@@ -59,7 +59,7 @@ const values = useFormValues();
|
||||
const errors = useFieldError(fieldName);
|
||||
const fieldComponentRef = useTemplateRef<HTMLInputElement>('fieldComponentRef');
|
||||
const formApi = formRenderProps.form;
|
||||
const compact = formRenderProps.compact;
|
||||
const compact = computed(() => formRenderProps.compact);
|
||||
const isInValid = computed(() => errors.value?.length > 0);
|
||||
|
||||
const FieldComponent = computed(() => {
|
||||
|
@@ -42,11 +42,11 @@ const emits = defineEmits<{
|
||||
}>();
|
||||
|
||||
const wrapperClass = computed(() => {
|
||||
const cls = ['flex flex-col'];
|
||||
if (props.layout === 'vertical') {
|
||||
cls.push(props.compact ? 'gap-x-2' : 'gap-x-4');
|
||||
const cls = ['flex'];
|
||||
if (props.layout === 'inline') {
|
||||
cls.push('flex-wrap gap-x-2');
|
||||
} else {
|
||||
cls.push('gap-2');
|
||||
cls.push(props.compact ? 'gap-x-2' : 'gap-x-4', 'flex-col grid');
|
||||
}
|
||||
return cn(...cls, props.wrapperClass);
|
||||
});
|
||||
@@ -170,7 +170,7 @@ const computedSchema = computed(
|
||||
|
||||
<template>
|
||||
<component :is="formComponent" v-bind="formComponentProps">
|
||||
<div ref="wrapperRef" :class="wrapperClass" class="grid">
|
||||
<div ref="wrapperRef" :class="wrapperClass">
|
||||
<template v-for="cSchema in computedSchema" :key="cSchema.fieldName">
|
||||
<!-- <div v-if="$slots[cSchema.fieldName]" :class="cSchema.formItemClass">
|
||||
<slot :definition="cSchema" :name="cSchema.fieldName"> </slot>
|
||||
|
@@ -8,7 +8,7 @@ import type { ClassType, MaybeComputedRef } from '@vben-core/typings';
|
||||
|
||||
import type { FormApi } from './form-api';
|
||||
|
||||
export type FormLayout = 'horizontal' | 'vertical';
|
||||
export type FormLayout = 'horizontal' | 'inline' | 'vertical';
|
||||
|
||||
export type BaseFormComponentType =
|
||||
| 'DefaultButton'
|
||||
|
@@ -107,7 +107,6 @@ export class ModalApi {
|
||||
this.store.setState((prev) => ({
|
||||
...prev,
|
||||
isOpen: false,
|
||||
submitting: false,
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -162,7 +161,11 @@ export class ModalApi {
|
||||
}
|
||||
|
||||
open() {
|
||||
this.store.setState((prev) => ({ ...prev, isOpen: true }));
|
||||
this.store.setState((prev) => ({
|
||||
...prev,
|
||||
isOpen: true,
|
||||
submitting: false,
|
||||
}));
|
||||
}
|
||||
|
||||
setData<T>(payload: T) {
|
||||
|
@@ -8,12 +8,7 @@ import { computed, ref } from 'vue';
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
import { X } from 'lucide-vue-next';
|
||||
import {
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogPortal,
|
||||
useForwardPropsEmits,
|
||||
} from 'radix-vue';
|
||||
import { DialogClose, DialogContent, useForwardPropsEmits } from 'radix-vue';
|
||||
|
||||
import DialogOverlay from './DialogOverlay.vue';
|
||||
|
||||
@@ -87,7 +82,7 @@ defineExpose({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogPortal :to="appendTo">
|
||||
<Teleport :to="appendTo">
|
||||
<Transition name="fade">
|
||||
<DialogOverlay
|
||||
v-if="open && modal"
|
||||
@@ -132,5 +127,5 @@ defineExpose({
|
||||
<X class="h-4 w-4" />
|
||||
</DialogClose>
|
||||
</DialogContent>
|
||||
</DialogPortal>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
@@ -7,7 +7,7 @@ import { computed, ref } from 'vue';
|
||||
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
import { DialogContent, DialogPortal, useForwardPropsEmits } from 'radix-vue';
|
||||
import { DialogContent, useForwardPropsEmits } from 'radix-vue';
|
||||
|
||||
import { sheetVariants } from './sheet';
|
||||
import SheetOverlay from './SheetOverlay.vue';
|
||||
@@ -73,7 +73,7 @@ function onAnimationEnd(event: AnimationEvent) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogPortal :to="appendTo">
|
||||
<Teleport :to="appendTo">
|
||||
<Transition name="fade">
|
||||
<SheetOverlay
|
||||
v-if="open && modal"
|
||||
@@ -103,5 +103,5 @@ function onAnimationEnd(event: AnimationEvent) {
|
||||
<Cross2Icon class="h-5 w-" />
|
||||
</DialogClose> -->
|
||||
</DialogContent>
|
||||
</DialogPortal>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
@@ -29,7 +29,8 @@ function useNavigation() {
|
||||
return true;
|
||||
}
|
||||
const route = routeMetaMap.get(path);
|
||||
return route?.meta?.openInNewWindow ?? false;
|
||||
// 如果有外链或者设置了在新窗口打开,返回 true
|
||||
return !!(route?.meta?.link || route?.meta?.openInNewWindow);
|
||||
};
|
||||
|
||||
const resolveHref = (path: string): string => {
|
||||
@@ -39,7 +40,13 @@ function useNavigation() {
|
||||
const navigation = async (path: string) => {
|
||||
try {
|
||||
const route = routeMetaMap.get(path);
|
||||
const { openInNewWindow = false, query = {} } = route?.meta ?? {};
|
||||
const { openInNewWindow = false, query = {}, link } = route?.meta ?? {};
|
||||
|
||||
// 检查是否有外链
|
||||
if (link && typeof link === 'string') {
|
||||
openWindow(link, { target: '_blank' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (isHttpUrl(path)) {
|
||||
openWindow(path, { target: '_blank' });
|
||||
|
@@ -86,6 +86,62 @@ const [QueryForm] = useVbenForm({
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
||||
});
|
||||
|
||||
const [InlineForm] = useVbenForm({
|
||||
layout: 'inline',
|
||||
schema: [
|
||||
{
|
||||
// 组件需要在 #/adapter.ts内注册,并加上类型
|
||||
component: 'Input',
|
||||
// 对应组件的参数
|
||||
componentProps: {
|
||||
placeholder: '请输入用户名',
|
||||
},
|
||||
// 字段名
|
||||
fieldName: 'username',
|
||||
// 界面显示的label
|
||||
label: '字符串',
|
||||
},
|
||||
{
|
||||
component: 'InputPassword',
|
||||
componentProps: {
|
||||
placeholder: '请输入密码',
|
||||
},
|
||||
fieldName: 'password',
|
||||
label: '密码',
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
placeholder: '请输入',
|
||||
},
|
||||
fieldName: 'number',
|
||||
label: '数字(带后缀)',
|
||||
suffix: () => '¥',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
filterOption: true,
|
||||
options: [
|
||||
{
|
||||
label: '选项1',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
label: '选项2',
|
||||
value: '2',
|
||||
},
|
||||
],
|
||||
placeholder: '请选择',
|
||||
showSearch: true,
|
||||
},
|
||||
fieldName: 'options',
|
||||
label: '下拉选',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const [QueryForm1] = useVbenForm({
|
||||
// 默认展开
|
||||
collapsed: true,
|
||||
@@ -205,6 +261,10 @@ function onSubmit(values: Record<string, any>) {
|
||||
<QueryForm />
|
||||
</Card>
|
||||
|
||||
<Card class="mb-5" title="查询表单,单行表单">
|
||||
<InlineForm />
|
||||
</Card>
|
||||
|
||||
<Card class="mb-5" title="查询表单,默认展开,垂直布局">
|
||||
<QueryForm2 />
|
||||
</Card>
|
||||
|
@@ -5,7 +5,7 @@ import type { Recordable } from '@vben/types';
|
||||
|
||||
import type { SystemRoleApi } from '#/api/system/role';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
import { computed, nextTick, ref } from 'vue';
|
||||
|
||||
import { useVbenDrawer, VbenTree } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
@@ -47,20 +47,26 @@ const [Drawer, drawerApi] = useVbenDrawer({
|
||||
drawerApi.unlock();
|
||||
});
|
||||
},
|
||||
onOpenChange(isOpen) {
|
||||
|
||||
async onOpenChange(isOpen) {
|
||||
if (isOpen) {
|
||||
const data = drawerApi.getData<SystemRoleApi.SystemRole>();
|
||||
formApi.resetForm();
|
||||
|
||||
if (data) {
|
||||
formData.value = data;
|
||||
id.value = data.id;
|
||||
formApi.setValues(data);
|
||||
} else {
|
||||
id.value = undefined;
|
||||
}
|
||||
|
||||
if (permissions.value.length === 0) {
|
||||
loadPermissions();
|
||||
await loadPermissions();
|
||||
}
|
||||
// Wait for Vue to flush DOM updates (form fields mounted)
|
||||
await nextTick();
|
||||
if (data) {
|
||||
formApi.setValues(data);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
701
pnpm-lock.yaml
generated
701
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -58,8 +58,8 @@ catalog:
|
||||
'@typescript-eslint/parser': ^8.35.1
|
||||
'@vee-validate/zod': ^4.15.1
|
||||
'@vite-pwa/vitepress': ^1.0.0
|
||||
'@vitejs/plugin-vue': ^5.2.4
|
||||
'@vitejs/plugin-vue-jsx': ^4.2.0
|
||||
'@vitejs/plugin-vue': ^6.0.1
|
||||
'@vitejs/plugin-vue-jsx': ^5.0.1
|
||||
'@vue/reactivity': ^3.5.17
|
||||
'@vue/shared': ^3.5.17
|
||||
'@vue/test-utils': ^2.4.6
|
||||
@@ -72,7 +72,7 @@ catalog:
|
||||
axios: ^1.10.0
|
||||
axios-mock-adapter: ^2.1.0
|
||||
cac: ^6.7.14
|
||||
chalk: ^5.4.1
|
||||
chalk: ^5.6.0
|
||||
cheerio: ^1.1.0
|
||||
circular-dependency-scanner: ^2.3.0
|
||||
class-variance-authority: ^0.7.1
|
||||
@@ -167,10 +167,10 @@ catalog:
|
||||
tippy.js: ^6.3.7
|
||||
turbo: ^2.5.4
|
||||
typescript: ^5.8.3
|
||||
unbuild: ^3.5.0
|
||||
unbuild: ^3.6.1
|
||||
unplugin-element-plus: ^0.10.0
|
||||
vee-validate: ^4.15.1
|
||||
vite: ^6.3.5
|
||||
vite: ^7.1.2
|
||||
vite-plugin-compression: ^0.5.1
|
||||
vite-plugin-dts: ^4.5.4
|
||||
vite-plugin-html: ^3.2.2
|
||||
|
@@ -3,30 +3,104 @@ import { join, normalize } from 'node:path';
|
||||
|
||||
const rootDir = process.cwd();
|
||||
|
||||
// 控制并发数量,避免创建过多的并发任务
|
||||
const CONCURRENCY_LIMIT = 10;
|
||||
|
||||
// 需要跳过的目录,避免进入这些目录进行清理
|
||||
const SKIP_DIRS = new Set(['.DS_Store', '.git', '.idea', '.vscode']);
|
||||
|
||||
/**
|
||||
* 递归查找并删除目标目录
|
||||
* 处理单个文件/目录项
|
||||
* @param {string} currentDir - 当前目录路径
|
||||
* @param {string} item - 文件/目录名
|
||||
* @param {string[]} targets - 要删除的目标列表
|
||||
* @param {number} _depth - 当前递归深度
|
||||
* @returns {Promise<boolean>} - 是否需要进一步递归处理
|
||||
*/
|
||||
async function processItem(currentDir, item, targets, _depth) {
|
||||
// 跳过特殊目录
|
||||
if (SKIP_DIRS.has(item)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const itemPath = normalize(join(currentDir, item));
|
||||
|
||||
if (targets.includes(item)) {
|
||||
// 匹配到目标目录或文件时直接删除
|
||||
await fs.rm(itemPath, { force: true, recursive: true });
|
||||
console.log(`✅ Deleted: ${itemPath}`);
|
||||
return false; // 已删除,无需递归
|
||||
}
|
||||
|
||||
// 使用 readdir 的 withFileTypes 选项,避免额外的 lstat 调用
|
||||
return true; // 可能需要递归,由调用方决定
|
||||
} catch (error) {
|
||||
// 更详细的错误信息
|
||||
if (error.code === 'ENOENT') {
|
||||
// 文件不存在,可能已被删除,这是正常情况
|
||||
return false;
|
||||
} else if (error.code === 'EPERM' || error.code === 'EACCES') {
|
||||
console.error(`❌ Permission denied: ${item} in ${currentDir}`);
|
||||
} else {
|
||||
console.error(
|
||||
`❌ Error handling item ${item} in ${currentDir}: ${error.message}`,
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归查找并删除目标目录(并发优化版本)
|
||||
* @param {string} currentDir - 当前遍历的目录路径
|
||||
* @param {string[]} targets - 要删除的目标列表
|
||||
* @param {number} depth - 当前递归深度,避免过深递归
|
||||
*/
|
||||
async function cleanTargetsRecursively(currentDir, targets) {
|
||||
const items = await fs.readdir(currentDir);
|
||||
async function cleanTargetsRecursively(currentDir, targets, depth = 0) {
|
||||
// 限制递归深度,避免无限递归
|
||||
if (depth > 10) {
|
||||
console.warn(`Max recursion depth reached at: ${currentDir}`);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const item of items) {
|
||||
try {
|
||||
const itemPath = normalize(join(currentDir, item));
|
||||
const stat = await fs.lstat(itemPath);
|
||||
let dirents;
|
||||
try {
|
||||
// 使用 withFileTypes 选项,一次性获取文件类型信息,避免后续 lstat 调用
|
||||
dirents = await fs.readdir(currentDir, { withFileTypes: true });
|
||||
} catch (error) {
|
||||
// 如果无法读取目录,可能已被删除或权限不足
|
||||
console.warn(`Cannot read directory ${currentDir}: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (targets.includes(item)) {
|
||||
// 匹配到目标目录或文件时直接删除
|
||||
await fs.rm(itemPath, { force: true, recursive: true });
|
||||
console.log(`Deleted: ${itemPath}`);
|
||||
} else if (stat.isDirectory()) {
|
||||
// 只对目录进行递归处理
|
||||
await cleanTargetsRecursively(itemPath, targets);
|
||||
// 分批处理,控制并发数量
|
||||
for (let i = 0; i < dirents.length; i += CONCURRENCY_LIMIT) {
|
||||
const batch = dirents.slice(i, i + CONCURRENCY_LIMIT);
|
||||
|
||||
const tasks = batch.map(async (dirent) => {
|
||||
const item = dirent.name;
|
||||
const shouldRecurse = await processItem(currentDir, item, targets, depth);
|
||||
|
||||
// 如果是目录且没有被删除,则递归处理
|
||||
if (shouldRecurse && dirent.isDirectory()) {
|
||||
const itemPath = normalize(join(currentDir, item));
|
||||
return cleanTargetsRecursively(itemPath, targets, depth + 1);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error handling item ${item} in ${currentDir}: ${error.message}`,
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
// 并发执行当前批次的任务
|
||||
const results = await Promise.allSettled(tasks);
|
||||
|
||||
// 检查是否有失败的任务(可选:用于调试)
|
||||
const failedTasks = results.filter(
|
||||
(result) => result.status === 'rejected',
|
||||
);
|
||||
if (failedTasks.length > 0) {
|
||||
console.warn(
|
||||
`${failedTasks.length} tasks failed in batch starting at index ${i} in directory: ${currentDir}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -43,14 +117,25 @@ async function cleanTargetsRecursively(currentDir, targets) {
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Starting cleanup of targets: ${cleanupTargets.join(', ')} from root: ${rootDir}`,
|
||||
`🚀 Starting cleanup of targets: ${cleanupTargets.join(', ')} from root: ${rootDir}`,
|
||||
);
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// 先统计要删除的目标数量
|
||||
console.log('📊 Scanning for cleanup targets...');
|
||||
|
||||
await cleanTargetsRecursively(rootDir, cleanupTargets);
|
||||
console.log('Cleanup process completed successfully.');
|
||||
|
||||
const endTime = Date.now();
|
||||
const duration = (endTime - startTime) / 1000;
|
||||
|
||||
console.log(
|
||||
`✨ Cleanup process completed successfully in ${duration.toFixed(2)}s`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(`Unexpected error during cleanup: ${error.message}`);
|
||||
console.error(`💥 Unexpected error during cleanup: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
|
Reference in New Issue
Block a user