Pre Merge pull request !58 from chenminjie/dev-v5_cmj

pull/58/MERGE
chenminjie 2024-12-12 11:49:28 +00:00 committed by Gitee
commit 8eb6743d19
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
28 changed files with 423 additions and 1247 deletions

135
.cursorrules Normal file
View File

@ -0,0 +1,135 @@
You are an expert in TypeScript, Node.js, Vite, Vue.js, Vue Router, Pinia, VueUse, and UI frameworks (Ant Design Vue, Element Plus, Naive UI), with a deep understanding of best practices and performance optimization techniques in these technologies.
When analyzing and modifying code, you should:
Package Structure Understanding
- Core Package (@core):
- Analyze base/ for core utilities and implementations
- Review ui-kit/ for framework-agnostic components
- Examine composables/ for shared Vue hooks
- Check preferences/ for app-wide settings
- Utility Packages:
- Understand utils/ for common functions
- Review types/ for shared type definitions
- Check constants/ for shared constants
- Examine effects/ for shared animations
- Feature Packages:
- Analyze stores/ for Pinia store implementations
- Review locales/ for i18n resources
- Check icons/ for icon components
- Examine styles/ for theme implementations
Framework Adaptation Patterns
- UI Framework Handling:
- Identify the target UI framework in apps/ (Ant Design Vue, Element Plus, Naive UI)
- Identify the target UI framework in packages/ (packages/@core/ui-kit/**)
- Use framework-specific component patterns
- Maintain consistent APIs across frameworks
- Implement proper component props
- Handle framework-specific events
- Follow framework-specific styling
Component Development Rules
- Base Components:
- Use framework-agnostic design where possible
- Implement proper type definitions
- Include accessibility features
- Handle component composition
- Manage component state
- Document component APIs
- Framework Components:
- Follow framework conventions
- Use framework-specific features
- Implement proper slots
- Handle framework events
- Use framework themes
- Document framework specifics
State and Store Patterns
- Store Implementation:
- Use Pinia store patterns
- Implement proper typing
- Handle state persistence
- Manage store modules
- Handle store reset
- Document store usage
- State Management:
- Use composition store helpers
- Implement state watchers
- Handle state updates
- Manage side effects
- Document state flow
- Test store functionality
Composable Development
- Hook Patterns:
- Create reusable hooks
- Implement proper typing
- Handle lifecycle
- Manage dependencies
- Document usage
- Test hook behavior
- Utility Functions:
- Create pure functions
- Use proper typing
- Handle edge cases
- Document parameters
- Test functionality
- Consider performance
Style and Theme Handling
- Theme Implementation:
- Support dark mode
- Handle framework themes
- Use CSS variables
- Implement transitions
- Handle responsive styles
- Document theme usage
- Style Management:
- Use framework classes
- Handle style conflicts
- Implement utilities
- Manage overrides
- Document style usage
- Test theme switching
Code Quality Standards
- TypeScript Usage:
- Use strict mode
- Implement interfaces
- Handle type guards
- Document types
- Test type safety
- Maintain type files
- Testing Requirements:
- Write unit tests
- Test components
- Test utilities
- Test hooks
- Document testing
- Maintain coverage
Response Guidelines
- Code Changes:
- Explain modifications
- Show code examples
- Document impacts
- Suggest alternatives
- Consider performance
- Note security implications
- Documentation:
- Update comments
- Provide examples
- Document APIs
- Note changes
- Include references
- Maintain clarity
When making improvements:
- Follow existing patterns
- Maintain consistency
- Consider compatibility
- Think about scaling
- Document changes
- Test thoroughly

View File

@ -5,8 +5,6 @@
import type { BaseFormComponentType } from '@vben/common-ui'; import type { BaseFormComponentType } from '@vben/common-ui';
import type { CustomComponentType } from '#/components/form/types';
import type { Component, SetupContext } from 'vue'; import type { Component, SetupContext } from 'vue';
import { h } from 'vue'; import { h } from 'vue';
@ -38,8 +36,6 @@ import {
Upload, Upload,
} from 'ant-design-vue'; } from 'ant-design-vue';
import { registerComponent as registerCustomFormComponent } from '#/components/form/component-map';
const withDefaultPlaceholder = <T extends Component>( const withDefaultPlaceholder = <T extends Component>(
component: T, component: T,
type: 'input' | 'select', type: 'input' | 'select',
@ -52,6 +48,7 @@ const withDefaultPlaceholder = <T extends Component>(
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明 // 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
export type ComponentType = export type ComponentType =
| 'ApiCheckbox'
| 'ApiSelect' | 'ApiSelect'
| 'ApiTreeSelect' | 'ApiTreeSelect'
| 'AutoComplete' | 'AutoComplete'
@ -77,14 +74,26 @@ export type ComponentType =
| 'TimePicker' | 'TimePicker'
| 'TreeSelect' | 'TreeSelect'
| 'Upload' | 'Upload'
| BaseFormComponentType | BaseFormComponentType;
| CustomComponentType;
async function initComponentAdapter() { async function initComponentAdapter() {
const components: Partial<Record<ComponentType, Component>> = { const components: Partial<Record<ComponentType, Component>> = {
// 如果你的组件体积比较大,可以使用异步加载 // 如果你的组件体积比较大,可以使用异步加载
// Button: () => // Button: () =>
// import('xxx').then((res) => res.Button), // import('xxx').then((res) => res.Button),
ApiCheckbox: (props, { attrs, slots }) => {
return h(
ApiComponent,
{
...props,
...attrs,
component: CheckboxGroup,
modelPropName: 'value',
},
slots,
);
},
ApiSelect: (props, { attrs, slots }) => { ApiSelect: (props, { attrs, slots }) => {
return h( return h(
ApiComponent, ApiComponent,
@ -94,8 +103,8 @@ async function initComponentAdapter() {
...attrs, ...attrs,
component: Select, component: Select,
loadingSlot: 'suffixIcon', loadingSlot: 'suffixIcon',
visibleEvent: 'onDropdownVisibleChange',
modelPropName: 'value', modelPropName: 'value',
visibleEvent: 'onVisibleChange',
}, },
slots, slots,
); );
@ -154,9 +163,6 @@ async function initComponentAdapter() {
Upload, Upload,
}; };
// 注册自定义组件
registerCustomFormComponent(components);
// 将组件注册到全局共享状态中 // 将组件注册到全局共享状态中
globalShareState.setComponents(components); globalShareState.setComponents(components);

View File

@ -0,0 +1,46 @@
import { type PageParam, requestClient } from '#/api/request';
export namespace FileApi {
export interface FilePageReqVO extends PageParam {
path?: string;
type?: string;
createTime?: Date[];
}
// 文件预签名地址 Response VO
export interface FilePresignedUrlRespVO {
// 文件配置编号
configId: number;
// 文件上传 URL
uploadUrl: string;
// 文件 URL
url: string;
}
}
// 查询文件列表
export function getFilePage(params: FileApi.FilePageReqVO) {
return requestClient.get('/infra/file/page', { params });
}
// 删除文件
export function deleteFile(id: number) {
return requestClient.delete(`/infra/file/delete?id=${id}`);
}
// 获取文件预签名地址
export function getFilePresignedUrl(path: string) {
return requestClient.get<FileApi.FilePresignedUrlRespVO>(
'/infra/file/presigned-url',
{ params: { path } },
);
}
export function createFile(data: any) {
return requestClient.post('/infra/file/create', data);
}
// 上传文件
export function uploadFile(data: any) {
return requestClient.upload('/infra/file/upload', data);
}

View File

@ -1,77 +0,0 @@
<script setup lang="ts">
import type { DescItem } from './types';
import type { PropType } from 'vue';
import { Descriptions, DescriptionsItem } from 'ant-design-vue';
import { componentMap } from '#/components/view/component-map';
defineProps({
title: { type: String, default: '' },
bordered: { type: Boolean, default: true },
size: {
type: String as PropType<'default' | 'middle' | 'small'>,
default: undefined,
},
column: {
type: [Number, Object],
default: () => {
// return { xxl: 4, xl: 3, lg: 3, md: 3, sm: 2, xs: 1 };
return 12;
},
},
labelStyle: {
type: Object,
default() {
return {
width: '120px',
};
},
},
contentStyle: {
type: Object,
default() {
return {
width: '0px',
};
},
},
schema: {
type: Array as PropType<DescItem[]>,
default: () => [],
},
data: { type: Object, default: undefined },
});
</script>
<template>
<Descriptions
:bordered="bordered"
:column="column"
:content-style="contentStyle"
:label-style="labelStyle"
:size="size"
:title="title ? title : undefined"
>
<template v-for="item in schema" :key="item.field">
<DescriptionsItem :label="item.label" :span="item.span">
<component
:is="(componentMap as Map<String, any>).get(item.component)"
v-if="(componentMap as Map<String, any>).has(item.component)"
:value="data?.[item.field]"
v-bind="{ ...item.componentProps }"
/>
<component
:is="item.render(data?.[item.field], data)"
v-else-if="
!(componentMap as Map<String, any>).has(item.component) &&
item.render
"
:value="data?.[item.field]"
v-bind="{ ...item.componentProps }"
/>
<template v-else>{{ data?.[item.field] }}</template>
</DescriptionsItem>
</template>
</Descriptions>
</template>

View File

@ -1,2 +0,0 @@
export { default as Description } from './description.vue';
export type * from './types';

View File

@ -1,54 +0,0 @@
import type { CollapseContainerOptions } from '@/components/Container';
import type { DescriptionsProps } from 'ant-design-vue/es/descriptions';
import type { CSSProperties, VNode } from 'vue';
export interface DescItem {
labelMinWidth?: number;
contentMinWidth?: number;
labelStyle?: CSSProperties;
field: string;
label: JSX.Element | string | VNode;
// Merge column
span?: number;
show?: (...arg: any) => boolean;
// render
render?: (
val: any,
data: Recordable,
) => Element | JSX.Element | number | string | undefined | VNode;
component: string;
componentProps?: any;
children?: DescItem[];
}
export interface DescriptionProps extends DescriptionsProps {
// Whether to include the collapse component
useCollapse?: boolean;
/**
* item configuration
* @type DescItem
*/
schema: DescItem[];
/**
*
* @type object
*/
data: Recordable;
/**
* Built-in CollapseContainer component configuration
* @type CollapseContainerOptions
*/
collapseOptions?: CollapseContainerOptions;
}
export interface DescInstance {
setDescProps(descProps: Partial<DescriptionProps>): void;
}
export type Register = (descInstance: DescInstance) => void;
/**
* @description:
*/
export type UseDescReturnType = [Register, DescInstance];

View File

@ -1,37 +0,0 @@
import type { CustomComponentType } from './types';
import type { Component } from 'vue';
import { capitalizeFirstLetter, kebabToCamelCase } from '@vben/utils';
const componentMap = new Map<CustomComponentType | string, Component>();
// import.meta.glob() 直接引入所有的模块 Vite 独有的功能
const modules = import.meta.glob('./components/**/*.vue', { eager: true });
// 加入到路由集合中
Object.keys(modules).forEach((key) => {
if (!key.includes('-ignore')) {
const mod = (modules as any)[key].default || {};
// ./components/ApiDict.vue
// 获取ApiDict
const compName = key.replace('./components/', '').replace('.vue', '');
componentMap.set(capitalizeFirstLetter(kebabToCamelCase(compName)), mod);
}
});
export function add(compName: string, component: Component) {
componentMap.set(compName, component);
}
export function del(compName: string) {
componentMap.delete(compName);
}
/**
*
* @param components
*/
export const registerComponent = (components: any) => {
componentMap.forEach((value, key) => {
components[key] = value as Component;
});
};
export { componentMap };

View File

@ -1,147 +0,0 @@
<script setup lang="ts">
import type { CheckboxValueType } from 'ant-design-vue/es/checkbox/interface';
import { computed, type PropType, ref, watch, watchEffect } from 'vue';
import { getNestedValue, isFunction } from '@vben/utils';
import { objectOmit, useVModel } from '@vueuse/core';
import { CheckboxGroup, Spin } from 'ant-design-vue';
import { requestClient } from '#/api/request';
type OptionsItem = { disabled?: boolean; label: string; value: string };
const props = defineProps({
value: {
type: [Array] as PropType<CheckboxValueType[]>,
default: undefined,
},
numberToString: {
type: Boolean,
default: false,
},
api: {
type: [Function, String] as PropType<
(arg?: any) => Promise<OptionsItem[]> | String
>,
default: null,
},
// api params
params: {
type: Object as PropType<any>,
default: () => ({}),
},
requestMethod: {
//
type: String,
default: 'post',
},
// support xxx.xxx.xx
resultField: {
type: String,
default: '',
},
labelField: {
type: String,
default: 'label',
},
valueField: {
type: String,
default: 'value',
},
immediate: {
type: Boolean,
default: true,
},
});
const emits = defineEmits(['update:value', 'optionsChange']);
const mValue = useVModel(props, 'value', emits, {
defaultValue: props.value,
passive: true,
});
const options = ref<OptionsItem[]>([]);
const loading = ref(false);
const isFirstLoad = ref(true);
const getOptions = computed(() => {
const { labelField, valueField, numberToString } = props;
const res: OptionsItem[] = [];
options.value.forEach((item: any) => {
const value = item[valueField];
res.push({
...objectOmit(item, [labelField, valueField]),
label: item[labelField],
value: numberToString ? `${value}` : value,
disabled: item.disabled || false,
});
});
return res;
});
const fetch = async () => {
const api: any =
typeof props.api === 'string' && props.api
? (params: any) => {
return (requestClient as any)[props.requestMethod](
props.api as any,
params,
);
}
: props.api;
if (!api || !isFunction(api)) return;
try {
loading.value = true;
const params =
props.requestMethod === 'get' ? { params: props.params } : props.params;
const res = await api(params);
if (Array.isArray(res)) {
options.value = res;
emits('optionsChange', options.value);
} else {
options.value = props.resultField
? getNestedValue(res, props.resultField)
: [];
emits('optionsChange', options.value);
}
} catch (error) {
console.warn(error);
} finally {
loading.value = false;
}
};
watchEffect(() => {
props.immediate && fetch();
});
watch(
() => props.params,
() => {
!isFirstLoad.value && fetch();
},
{ deep: true },
);
// value mValue options value
watch(
() => props.value,
() => {
if (props.numberToString && Array.isArray(mValue.value)) {
mValue.value = mValue.value.map((item) => `${item}`);
}
},
);
</script>
<template>
<Spin :spinning="loading" style="margin-left: 20px">
<CheckboxGroup
v-bind="$attrs"
v-model:value="mValue"
:options="getOptions"
class="w-full"
>
<template v-for="item in Object.keys($slots)" #[item]="data">
<slot :name="item" v-bind="data || {}"></slot>
</template>
</CheckboxGroup>
</Spin>
</template>

View File

@ -1,54 +0,0 @@
<script setup lang="ts">
import { computed, type PropType } from 'vue';
import { useDictStore } from '@vben/stores';
import { ApiCheckboxGroup, ApiRadioGroup, ApiSelect } from '..';
type OptionsItem = { disabled?: boolean; label: string; value: string };
const props = defineProps({
renderType: {
type: String as PropType<'CheckboxGroup' | 'RadioGroup' | 'Select'>,
default: 'Select',
},
api: {
type: [Function, String] as PropType<
(arg?: any) => Promise<OptionsItem[]> | String
>,
default: null,
},
requestMethod: {
//
type: String,
default: 'post',
},
// api params
params: {
type: Object as PropType<any>,
default: () => ({}),
},
code: {
type: String,
default: undefined,
},
});
const DictComponent = computed(() => {
if (props.renderType === 'RadioGroup') {
return ApiRadioGroup;
} else if (props.renderType === 'CheckboxGroup') {
return ApiCheckboxGroup;
}
return ApiSelect;
});
const fetch = () => {
return new Promise<OptionsItem[]>((resolve) => {
const dict = useDictStore().getDictOptions(props.code!);
const options: OptionsItem[] = dict as OptionsItem[];
resolve(options);
});
};
</script>
<template>
<DictComponent :api="props.code ? fetch : props.api" />
</template>

View File

@ -1,156 +0,0 @@
<script setup lang="ts">
import type { SelectValue } from 'ant-design-vue/es/select';
import { computed, type PropType, ref, watch, watchEffect } from 'vue';
import { getNestedValue, isFunction } from '@vben/utils';
import { objectOmit, useVModel } from '@vueuse/core';
import { RadioGroup, Spin } from 'ant-design-vue';
import { requestClient } from '#/api/request';
type OptionsItem = { disabled?: boolean; label: string; value: string };
const props = defineProps({
value: {
type: [String, Number, Array] as PropType<SelectValue>,
default: undefined,
},
numberToString: {
type: Boolean,
default: false,
},
api: {
type: [Function, String] as PropType<
(arg?: any) => Promise<OptionsItem[]> | String
>,
default: null,
},
requestMethod: {
//
type: String,
default: 'post',
},
// api params
params: {
type: Object as PropType<any>,
default: () => ({}),
},
// support xxx.xxx.xx
resultField: {
type: String,
default: '',
},
labelField: {
type: String,
default: 'label',
},
valueField: {
type: String,
default: 'value',
},
immediate: {
type: Boolean,
default: true,
},
isBtn: {
type: Boolean,
default: false,
},
});
const emits = defineEmits(['update:value', 'optionsChange']);
const mValue = useVModel(props, 'value', emits, {
defaultValue: props.value,
passive: true,
});
const options = ref<OptionsItem[]>([]);
const loading = ref(false);
const isFirstLoad = ref(true);
const getOptions = computed(() => {
const { labelField, valueField, numberToString } = props;
const res: OptionsItem[] = [];
options.value.forEach((item: any) => {
const value = item[valueField];
res.push({
...objectOmit(item, [labelField, valueField]),
label: item[labelField],
value: numberToString ? `${value}` : value,
disabled: item.disabled || false,
});
});
return res;
});
const fetch = async () => {
const api: any =
typeof props.api === 'string' && props.api
? (params: any) => {
return (requestClient as any)[props.requestMethod](
props.api as any,
params,
);
}
: props.api;
if (!api || !isFunction(api)) return;
try {
loading.value = true;
const params =
props.requestMethod === 'get' ? { params: props.params } : props.params;
const res = await api(params);
if (Array.isArray(res)) {
options.value = res;
emits('optionsChange', options.value);
} else {
options.value = props.resultField
? getNestedValue(res, props.resultField)
: [];
emits('optionsChange', options.value);
}
} catch (error) {
console.warn(error);
} finally {
loading.value = false;
}
};
watchEffect(() => {
props.immediate && fetch();
});
watch(
() => props.params,
() => {
!isFirstLoad.value && fetch();
},
{ deep: true },
);
// value mValue options value
watch(
() => props.value,
() => {
if (props.numberToString && typeof mValue.value === 'number') {
mValue.value = `${mValue.value}`;
}
if (props.numberToString && Array.isArray(mValue.value)) {
mValue.value = mValue.value.map((item) => `${item}`);
}
},
);
</script>
<template>
<Spin :spinning="loading" style="margin-left: 20px">
<RadioGroup
v-bind="$attrs"
v-model:value="mValue"
:button-style="isBtn ? 'solid' : 'outline'"
:option-type="isBtn ? 'button' : 'default'"
:options="getOptions"
class="w-full"
>
<template v-for="item in Object.keys($slots)" #[item]="data">
<slot :name="item" v-bind="data || {}"></slot>
</template>
</RadioGroup>
</Spin>
</template>

View File

@ -1,163 +0,0 @@
<script setup lang="ts">
import type { SelectValue } from 'ant-design-vue/es/select';
import { computed, type PropType, ref, watch, watchEffect } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { getNestedValue, isFunction } from '@vben/utils';
import { objectOmit, useVModel } from '@vueuse/core';
import { Select } from 'ant-design-vue';
import { requestClient } from '#/api/request';
type OptionsItem = { disabled?: boolean; label: string; value: string };
const props = defineProps({
value: {
type: [String, Number, Array] as PropType<SelectValue>,
default: undefined,
},
numberToString: {
type: Boolean,
default: false,
},
api: {
type: [Function, String] as PropType<
(arg?: any) => Promise<OptionsItem[]> | String
>,
default: null,
},
requestMethod: {
type: String,
default: 'post',
},
// api params
params: {
type: Object as PropType<any>,
default: () => ({}),
},
// support xxx.xxx.xx
resultField: {
type: String,
default: '',
},
labelField: {
type: String,
default: 'label',
},
valueField: {
type: String,
default: 'value',
},
immediate: {
type: Boolean,
default: true,
},
});
const emits = defineEmits(['update:value', 'optionsChange']);
const mValue = useVModel(props, 'value', emits, {
defaultValue: props.value,
passive: true,
});
const options = ref<OptionsItem[]>([]);
const loading = ref(false);
const isFirstLoad = ref(true);
const getOptions = computed(() => {
const { labelField, valueField, numberToString } = props;
const res: OptionsItem[] = [];
options.value.forEach((item: any) => {
const value = item[valueField];
res.push({
...objectOmit(item, [labelField, valueField]),
label: item[labelField],
value: numberToString ? `${value}` : value,
disabled: item.disabled || false,
});
});
return res;
});
const fetch = async () => {
const api: any =
typeof props.api === 'string' && props.api
? (params: any) => {
return (requestClient as any)[props.requestMethod](
props.api as any,
params,
);
}
: props.api;
if (!api || !isFunction(api)) return;
try {
loading.value = true;
const params =
props.requestMethod === 'get' ? { params: props.params } : props.params;
const res = await api(params);
if (Array.isArray(res)) {
options.value = res;
emits('optionsChange', options.value);
} else {
options.value = props.resultField
? getNestedValue(res, props.resultField)
: [];
emits('optionsChange', options.value);
}
} catch (error) {
console.warn(error);
} finally {
loading.value = false;
}
};
async function handleFetch() {
if (!props.immediate && isFirstLoad.value) {
await fetch();
isFirstLoad.value = false;
}
}
watchEffect(() => {
props.immediate && fetch();
});
watch(
() => props.params,
() => {
!isFirstLoad.value && fetch();
},
{ deep: true },
);
// value mValue options value
watch(
() => props.value,
() => {
if (props.numberToString && typeof mValue.value === 'number') {
mValue.value = `${mValue.value}`;
}
if (props.numberToString && Array.isArray(mValue.value)) {
mValue.value = mValue.value.map((item) => `${item}`);
}
},
);
</script>
<template>
<Select
v-model:value="mValue"
:options="getOptions"
class="w-full"
@dropdown-visible-change="handleFetch"
>
<template v-for="item in Object.keys($slots)" #[item]="data">
<slot :name="item" v-bind="data || {}"></slot>
</template>
<template v-if="loading" #suffixIcon>
<IconifyIcon icon="ant-design:loading-outlined" spin />
</template>
<template v-if="loading" #notFoundContent>
<span>
<IconifyIcon icon="ant-design:loading-outlined" spin />
请等待数据加载完成
</span>
</template>
</Select>
</template>

View File

@ -1,140 +0,0 @@
<script setup lang="ts">
import type { SelectValue } from 'ant-design-vue/es/select';
import { computed, type PropType, ref, watch, watchEffect } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { getNestedValue, isFunction } from '@vben/utils';
import { useVModel } from '@vueuse/core';
import { TreeSelect } from 'ant-design-vue';
import { requestClient } from '#/api/request';
const props = defineProps({
value: {
type: [String, Number, Array] as PropType<SelectValue>,
default: undefined,
},
api: {
type: [Function, String] as PropType<(arg?: any) => Promise<any> | String>,
default: null,
},
requestMethod: {
//
type: String,
default: 'post',
},
// api params
params: {
type: Object as PropType<any>,
default: () => ({}),
},
// support xxx.xxx.xx
resultField: {
type: String,
default: '',
},
labelField: {
type: String,
default: 'title',
},
valueField: {
type: String,
default: 'value',
},
childrenField: {
type: String,
default: 'children',
},
immediate: {
type: Boolean,
default: true,
},
});
const emits = defineEmits(['update:value', 'treeDataChange']);
const mValue = useVModel(props, 'value', emits, {
defaultValue: props.value,
passive: true,
});
const treeData = ref<any>([]);
const loading = ref(false);
const isFirstLoad = ref(true);
const fieldNames = computed(() => {
return {
label: props.labelField,
value: props.valueField,
children: props.childrenField,
};
});
const getTreeData = computed(() => {
return treeData.value;
});
const fetch = async () => {
const api: any =
typeof props.api === 'string' && props.api
? (params: any) => {
return (requestClient as any)[props.requestMethod](
props.api as any,
params,
);
}
: props.api;
if (!api || !isFunction(api)) return;
try {
loading.value = true;
const params =
props.requestMethod === 'get' ? { params: props.params } : props.params;
const res = await api(params);
if (Array.isArray(res)) {
treeData.value = res;
emits('treeDataChange', treeData.value);
} else {
treeData.value = props.resultField
? getNestedValue(res, props.resultField)
: [];
emits('treeDataChange', treeData.value);
}
} catch (error) {
console.warn(error);
} finally {
loading.value = false;
}
};
async function handleFetch() {
if (!props.immediate && isFirstLoad.value) {
await fetch();
isFirstLoad.value = false;
}
}
watchEffect(() => {
props.immediate && fetch();
});
watch(
() => props.params,
() => {
!isFirstLoad.value && fetch();
},
{ deep: true },
);
</script>
<template>
<TreeSelect
v-model:value="mValue"
:field-names="fieldNames"
:tree-data="getTreeData"
:tree-node-filter-prop="labelField"
class="w-full"
@dropdown-visible-change="handleFetch"
>
<template v-for="item in Object.keys($slots)" #[item]="data">
<slot :name="item" v-bind="data || {}"></slot>
</template>
<template v-if="loading" #suffixIcon>
<IconifyIcon icon="ant-design:loading-outlined" spin />
</template>
</TreeSelect>
</template>

View File

@ -1,5 +0,0 @@
export { default as ApiCheckboxGroup } from './components/api-checkbox-group.vue';
export { default as ApiDict } from './components/api-dict.vue';
export { default as ApiRadioGroup } from './components/api-radio-group.vue';
export { default as ApiSelect } from './components/api-select.vue';
export { default as ApiTreeSelect } from './components/api-tree-select.vue';

View File

@ -1,6 +0,0 @@
export type CustomComponentType =
| 'ApiCheckboxGroup'
| 'ApiDict'
| 'ApiRadioGroup'
| 'ApiSelect'
| 'ApiTreeSelect';

View File

@ -1,27 +0,0 @@
import type { Component } from 'vue';
import { toPascalCase } from '#/util/tool';
const componentMap = new Map<string, Component>();
// import.meta.glob() 直接引入所有的模块 Vite 独有的功能
const modules = import.meta.glob('./components/**/*.vue', { eager: true });
// 加入到路由集合中
Object.keys(modules).forEach((key) => {
if (!key.includes('-ignore')) {
const mod = (modules as any)[key].default || {};
// ./components/ApiDict.vue
// 获取ApiDict
const compName = key.replace('./components/', '').replace('.vue', '');
componentMap.set(toPascalCase(compName), mod);
}
});
export function add(compName: string, component: Component) {
componentMap.set(compName, component);
}
export function del(compName: string) {
componentMap.delete(compName);
}
export { componentMap };

View File

@ -1,6 +0,0 @@
<script setup lang="ts">
import ApiSelect from './api-select.vue';
</script>
<template>
<ApiSelect />
</template>

View File

@ -1,64 +0,0 @@
<script setup lang="ts">
import { computed } from 'vue';
import { type DictItem, useDictStore } from '@vben/stores';
const props = defineProps({
code: {
type: String,
default: undefined,
},
data: {
type: Object,
default() {
return {};
},
},
value: {
//
type: [String, Number, Array],
default: undefined,
},
split: {
//
type: String,
default: ',',
},
join: {
//
type: String,
default: ',',
},
});
const dictStore = useDictStore();
const cValue = computed(() => {
if (!props.value && props.value !== 0) {
return '';
}
const arr: Array<any> = [];
if (Array.isArray(props.value)) {
arr.push(...props.value);
} else {
arr.push(...props.value.toString().split(props.split));
}
const dictData = dictStore.getDictData(props.code as string) as DictItem[];
const res: Array<any> = [];
arr.forEach((item) => {
for (let i = 0; i < dictData.length; i++) {
if (dictData[i]?.value?.toString() === item?.toString()) {
res.push(dictData[i]?.label);
break;
}
if (i === dictData.length - 1) {
res.push(item);
}
}
});
return res.join(props.join);
});
</script>
<template>
<div>{{ cValue }}</div>
</template>
<style lang="less" scoped></style>

View File

@ -1,6 +0,0 @@
<script setup lang="ts">
import ApiSelect from './api-select.vue';
</script>
<template>
<ApiSelect />
</template>

View File

@ -1,154 +0,0 @@
<script setup lang="ts">
import { computed, onMounted, type PropType, watch } from 'vue';
import { type DictItem, useDictStore } from '@vben/stores';
import { requestClient } from '#/api/request';
const props = defineProps({
code: {
type: String,
default: undefined,
},
data: {
type: Object,
default() {
return {};
},
},
value: {
//
type: [String, Number, Array],
default: undefined,
},
split: {
//
type: String,
default: ',',
},
join: {
//
type: String,
default: ',',
},
api: {
//
type: [Function, String] as PropType<
((...arg: any) => Promise<any>) | String
>,
default() {
return () => {
return new Promise((resolve) => {
resolve([]);
});
};
},
},
params: {
type: Object,
default() {
return {};
},
},
cacheKey: {
type: String,
default: '',
},
requestMethod: {
type: String,
default: 'post',
},
});
const dictStore = useDictStore();
/**
* 获取包含的id
*/
const getIncludeIds = () => {
if (!props.value && props.value !== 0) {
return [];
}
const arr: Array<any> = [];
if (Array.isArray(props.value)) {
arr.push(...props.value);
} else {
arr.push(...props.value.toString().split(props.split));
}
return arr;
};
/**
* 获取缓存key
*/
const getCacheKey = () => {
let cacheKey = props.cacheKey;
if (typeof props.api === 'string' && !cacheKey) {
cacheKey = props.api as string;
}
return cacheKey;
};
const cValue = computed(() => {
if (!props.value && props.value !== 0) {
return '';
}
const arr: Array<any> = getIncludeIds();
const cacheKey = getCacheKey();
const dictData = dictStore.getDictData(cacheKey) as DictItem[];
const res: Array<any> = [];
arr.forEach((item) => {
for (let i = 0; i < dictData.length; i++) {
if (dictData[i]?.value?.toString() === item?.toString()) {
res.push(dictData[i]?.label);
break;
}
if (i === dictData.length - 1) {
res.push(item);
}
}
});
return res.join(props.join);
});
const requestData = () => {
const api: (...arg: any) => Promise<any> =
typeof props.api === 'string'
? (params: any) => {
return (requestClient as any)[props.requestMethod](
props.api as any,
params,
);
}
: (props.api as (...arg: any) => Promise<any>);
const cacheKey = getCacheKey();
const params =
props.requestMethod === 'get'
? {
params: {
...props.params,
dictType: cacheKey,
includeType: 2,
includeIds: getIncludeIds(),
},
}
: {
data: {
...props.params,
dictType: cacheKey,
includeType: 2,
includeIds: getIncludeIds(),
},
};
dictStore.setDictCacheByApi(api, params);
};
onMounted(() => {
requestData();
});
watch(
() => props.value,
() => {
requestData();
},
);
</script>
<template>
<div>{{ cValue }}</div>
</template>
<style lang="less" scoped></style>

View File

@ -1,95 +0,0 @@
<script setup lang="ts">
import { computed, onMounted, type PropType, ref } from 'vue';
import { requestClient } from '#/api/request';
const props = defineProps({
data: {
type: Object,
default() {
return {};
},
},
value: {
//
type: [String, Number, Array],
default: undefined,
},
api: {
//
type: [Function, String] as PropType<
((...arg: any) => Promise<any>) | String
>,
default() {
return () => {
return new Promise((resolve) => {
resolve([]);
});
};
},
},
params: {
type: Object,
default() {
return {};
},
},
cacheKey: {
type: String,
default: '',
},
requestMethod: {
type: String,
default: 'post',
},
valueField: {
type: String,
default: 'id',
},
labelField: {
type: String,
default: 'name',
},
multiple: {
type: Boolean,
default: false,
},
});
const currentData = ref({});
const cValue = computed(() => {
return (currentData.value as any)[props.labelField] || props.value;
});
onMounted(() => {
const api: (...arg: any) => Promise<any> =
typeof props.api === 'string'
? (params: any) => {
return (requestClient as any)[props.requestMethod](
props.api as any,
params,
);
}
: (props.api as (...arg: any) => Promise<any>);
const searchType = props.multiple ? 'IN' : 'EQ';
const params =
props.requestMethod === 'get'
? {
params: {
...props.params,
[`m_${searchType}_${props.valueField}`]: props.value,
},
}
: {
...props.params,
[`m_${searchType}_${props.valueField}`]: props.value,
};
api(params).then((res) => {
if (res.length > 0) {
currentData.value = res[0];
}
});
});
</script>
<template>
<div>{{ cValue }}</div>
</template>
<style lang="less" scoped></style>

View File

@ -2,7 +2,7 @@ import type { VxeGridProps } from '#/adapter/vxe-table';
import type { CodegenApi } from '#/api/infra/codegen'; import type { CodegenApi } from '#/api/infra/codegen';
import { type VbenFormProps, z } from '@vben/common-ui'; import { type VbenFormProps, z } from '@vben/common-ui';
import { useUserStore } from '@vben/stores'; import { useDictStore, useUserStore } from '@vben/stores';
import { getDataSourceConfigList } from '#/api/infra/data-source-config'; import { getDataSourceConfigList } from '#/api/infra/data-source-config';
import { $t } from '#/locales'; import { $t } from '#/locales';
@ -88,21 +88,11 @@ export namespace CodegenImportTableModalData {
fieldName: 'dataSourceConfigId', fieldName: 'dataSourceConfigId',
component: 'ApiSelect', component: 'ApiSelect',
componentProps: { componentProps: {
defaultSelectedFirst: true,
allowClear: true,
placeholder: '请选择数据源', placeholder: '请选择数据源',
api: getDataSourceConfigList, api: getDataSourceConfigList,
labelField: 'name', labelField: 'name',
valueField: 'id', valueField: 'id',
}, },
componentEvents: (events, formApi) => {
return {
optionsChange: (value: any) => {
// 设置默认选中第一个
formApi.setFieldValue('dataSourceConfigId', value[0].id);
},
};
},
}, },
{ {
label: '表名称', label: '表名称',
@ -169,37 +159,47 @@ export namespace CodegenOptionsModalData {
{ {
label: '生成模版', label: '生成模版',
fieldName: 'template', fieldName: 'template',
component: 'ApiDict', component: 'ApiSelect',
componentProps: { componentProps: {
code: DICT_TYPE.INFRA_CODEGEN_TEMPLATE_TYPE,
class: 'w-full', class: 'w-full',
placeholder: '请选择生成模版', placeholder: '请选择生成模版',
api: () => {
return useDictStore().getDictOptions(
DICT_TYPE.INFRA_CODEGEN_TEMPLATE_TYPE,
);
}, },
rules: z.string().min(1, { message: '生成模版不能为空' }), },
rules: 'required',
defaultValue: '1', defaultValue: '1',
}, },
{ {
label: '前端类型', label: '前端类型',
fieldName: 'frontType', fieldName: 'frontType',
component: 'ApiDict', component: 'ApiSelect',
componentProps: { componentProps: {
code: DICT_TYPE.INFRA_CODEGEN_FRONT_TYPE,
class: 'w-full', class: 'w-full',
placeholder: '请选择前端类型', placeholder: '请选择前端类型',
api: () => {
return useDictStore().getDictOptions(
DICT_TYPE.INFRA_CODEGEN_FRONT_TYPE,
);
}, },
rules: z.string().min(1, { message: '前端类型不能为空' }), },
rules: 'required',
defaultValue: '31', defaultValue: '31',
}, },
{ {
label: '生成场景', label: '生成场景',
fieldName: 'scene', fieldName: 'scene',
component: 'ApiDict', component: 'ApiSelect',
componentProps: { componentProps: {
code: DICT_TYPE.INFRA_CODEGEN_SCENE,
class: 'w-full', class: 'w-full',
placeholder: '请选择生成场景', placeholder: '请选择生成场景',
api: () => {
return useDictStore().getDictOptions(DICT_TYPE.INFRA_CODEGEN_SCENE);
}, },
rules: z.string().min(1, { message: '生成场景不能为空' }), },
rules: 'required',
defaultValue: '1', defaultValue: '1',
}, },
{ {
@ -214,16 +214,17 @@ export namespace CodegenOptionsModalData {
}, },
labelField: 'name', labelField: 'name',
valueField: 'id', valueField: 'id',
childrenField: 'children',
placeholder: '请选择上级菜单', placeholder: '请选择上级菜单',
}, },
rules: z.number().min(1, { message: '上级菜单不能为空' }), rules: 'required',
defaultValue: null, defaultValue: null,
}, },
{ {
label: '模块名', label: '模块名',
fieldName: 'moduleName', fieldName: 'moduleName',
component: 'Input', component: 'Input',
rules: z.string().min(1, { message: '模块名不能为空' }), rules: 'required',
componentProps: { componentProps: {
placeholder: '请输入模块名', placeholder: '请输入模块名',
}, },
@ -232,22 +233,19 @@ export namespace CodegenOptionsModalData {
label: '业务名', label: '业务名',
fieldName: 'businessName', fieldName: 'businessName',
component: 'Input', component: 'Input',
rules: z.string().min(1, { message: '业务名不能为空' }), rules: 'required',
componentProps: { componentProps: {
placeholder: '请输入业务名', placeholder: '请输入业务名',
}, },
}, },
{
label: '类名',
fieldName: 'className',
component: 'Input',
rules: z.string().min(1, { message: '类名不能为空' }),
},
{ {
label: '类描述', label: '类描述',
fieldName: 'classComment', fieldName: 'classComment',
component: 'Input', component: 'Input',
rules: z.string().min(1, { message: '类描述不能为空' }), rules: 'required',
componentProps: {
placeholder: '请输入类描述',
},
}, },
]; ];
} }

View File

@ -85,9 +85,6 @@ const [Modal, modalApi] = useVbenModal({
confirmLoading: confirmLoading.value, confirmLoading: confirmLoading.value,
closeOnClickModal: false, closeOnClickModal: false,
closeOnPressEscape: false, closeOnPressEscape: false,
onOpened: async () => {
gridApi.reload(await gridApi.formApi.getValues());
},
onConfirm: async () => { onConfirm: async () => {
modalApi.setState({ confirmLoading: true }); modalApi.setState({ confirmLoading: true });
const formValues = await gridApi.formApi.getValues(); const formValues = await gridApi.formApi.getValues();

View File

@ -3,6 +3,8 @@ import type { FileConfigApi } from '#/api/infra/file-config';
import { h } from 'vue'; import { h } from 'vue';
import { useDictStore } from '@vben/stores';
import { Tag } from 'ant-design-vue'; import { Tag } from 'ant-design-vue';
import { type VbenFormProps, z } from '#/adapter/form'; import { type VbenFormProps, z } from '#/adapter/form';
@ -24,11 +26,13 @@ export const formSchema: VbenFormProps['schema'] = [
{ {
fieldName: 'storage', fieldName: 'storage',
label: '储存器', label: '储存器',
component: 'ApiDict', component: 'ApiSelect',
componentProps: { componentProps: {
code: DICT_TYPE.INFRA_FILE_STORAGE,
class: 'w-full', class: 'w-full',
placeholder: '请选择储存器', placeholder: '请选择储存器',
api: () => {
return useDictStore().getDictOptions(DICT_TYPE.INFRA_FILE_STORAGE);
},
}, },
}, },
{ {
@ -118,14 +122,15 @@ export const editFormSchema: VbenFormProps['schema'] = [
{ {
fieldName: 'storage', fieldName: 'storage',
label: '储存器', label: '储存器',
component: 'ApiDict', component: 'ApiSelect',
componentProps: { componentProps: {
code: DICT_TYPE.INFRA_FILE_STORAGE,
class: 'w-full', class: 'w-full',
placeholder: '请选择储存器', placeholder: '请选择储存器',
numberToString: true, api: () => {
return useDictStore().getDictOptions(DICT_TYPE.INFRA_FILE_STORAGE);
}, },
rules: z.string().min(1, '请选择储存器').or(z.number()), },
rules: 'required',
defaultValue: '1', defaultValue: '1',
}, },
{ {
@ -135,7 +140,7 @@ export const editFormSchema: VbenFormProps['schema'] = [
componentProps: { componentProps: {
placeholder: '请输入基础路径', placeholder: '请输入基础路径',
}, },
rules: z.string().min(1, '请输入基础路径'), rules: 'required',
dependencies: { dependencies: {
triggerFields: ['storage'], triggerFields: ['storage'],
if: (values: Record<string, any>) => { if: (values: Record<string, any>) => {
@ -150,7 +155,7 @@ export const editFormSchema: VbenFormProps['schema'] = [
componentProps: { componentProps: {
placeholder: '请输入主机地址', placeholder: '请输入主机地址',
}, },
rules: z.string().min(1, '请输入主机地址'), rules: 'required',
dependencies: { dependencies: {
triggerFields: ['storage'], triggerFields: ['storage'],
if: (values: Record<string, any>) => [11, 12].includes(values.storage), if: (values: Record<string, any>) => [11, 12].includes(values.storage),
@ -163,7 +168,7 @@ export const editFormSchema: VbenFormProps['schema'] = [
componentProps: { componentProps: {
placeholder: '请输入主机端口', placeholder: '请输入主机端口',
}, },
rules: z.string().min(1, '请输入主机端口'), rules: 'required',
dependencies: { dependencies: {
triggerFields: ['storage'], triggerFields: ['storage'],
if: (values: Record<string, any>) => [11, 12].includes(values.storage), if: (values: Record<string, any>) => [11, 12].includes(values.storage),

View File

@ -0,0 +1,22 @@
<script setup lang="ts">
import { useVbenModal } from '@vben/common-ui';
import { useVbenForm } from '#/adapter/form';
import { uploadFormSchema } from '../file.data';
const [Modal] = useVbenModal({
title: '上传文件',
});
const [Form] = useVbenForm({
schema: uploadFormSchema,
showDefaultActions: false,
handleSubmit: async (_values) => {},
});
</script>
<template>
<Modal>
<Form />
</Modal>
</template>

View File

@ -0,0 +1,52 @@
import type { VxeGridProps } from '@vben/plugins/vxe-table';
import type { VbenFormProps } from '#/adapter/form';
import { type FileApi, uploadFile } from '#/api/infra/file';
/**
*
*/
export const formSchema: VbenFormProps['schema'] = [];
/**
*
*/
export const tableColumns: VxeGridProps<FileApi.FilePageReqVO>['columns'] = [
{
fixed: 'left',
type: 'checkbox',
width: 50,
},
{
fixed: 'left',
type: 'seq',
width: 50,
},
{ field: 'name', title: '文件名称' },
{ field: 'path', title: '文件路径' },
{ field: 'url', title: '文件 URL' },
{ field: 'type', title: '文件类型' },
{ field: 'size', title: '文件大小' },
{ field: 'createTime', title: '创建时间' },
];
/**
*
*/
export const editFormSchema: VbenFormProps['schema'] = [];
/**
*
*/
export const uploadFormSchema: VbenFormProps['schema'] = [
{
fieldName: 'file',
label: '文件',
component: 'ApiUpload',
componentProps: {
api: uploadFile,
uploadMode: 'file',
},
},
];

View File

@ -0,0 +1,107 @@
<script setup lang="ts">
import { defineAsyncComponent, reactive } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { useVbenVxeGrid, type VxeGridProps } from '#/adapter/vxe-table';
import { getFilePage } from '#/api/infra/file';
import { ActionButtons } from '#/components/action-buttons';
import { tableColumns } from './file.data';
/**
* 表格配置
*/
const gridOptions = reactive<any>({
columns: tableColumns,
height: 'auto',
checkboxConfig: {
reserve: true,
highlight: true,
// labelField: 'id',
},
rowConfig: {
keyField: 'id',
},
proxyConfig: {
ajax: {
query: async (e, params) => {
const data = await getFilePage({
pageNo: e.page.currentPage,
pageSize: e.page.pageSize,
...params,
});
return data;
},
},
},
} as VxeGridProps);
/**
* 表格事件
*/
const gridEvents = reactive<any>({});
// 使
const [Grid] = useVbenVxeGrid({
gridOptions,
gridEvents,
});
/**
* 使用 上传表单组件
*/
const [UploadForm, uploadFormApi] = useVbenModal({
connectedComponent: defineAsyncComponent(
() => import('./components/upload-form.vue'),
),
});
const handleUpload = () => {
uploadFormApi.open();
};
const handleView = (_row: any) => {};
const handleEdit = (_row: any) => {};
const handleDelete = (_row: any) => {};
</script>
<template>
<Page auto-content-height>
<Grid>
<template #toolbar-actions>
<ActionButtons
:actions="[
{
type: 'primary',
label: '上传文件',
icon: 'ant-design:plus-outlined',
onClick: handleUpload,
},
]"
/>
</template>
<template #action="{ row }">
<ActionButtons
:actions="[
{
label: '查看',
icon: 'ant-design:eye-outlined',
onClick: () => handleView(row),
},
{
label: '编辑',
icon: 'ant-design:edit-outlined',
onClick: () => handleEdit(row),
},
{
label: '删除',
icon: 'ant-design:delete-outlined',
onClick: () => handleDelete(row),
},
]"
/>
</template>
</Grid>
<UploadForm />
</Page>
</template>

View File

@ -81,6 +81,7 @@ const emit = defineEmits<{
const modelValue = defineModel({ default: '' }); const modelValue = defineModel({ default: '' });
const attrs = useAttrs(); const attrs = useAttrs();
const { class: className, style }: Record<string, any> = attrs;
const refOptions = ref<OptionsItem[]>([]); const refOptions = ref<OptionsItem[]>([]);
const loading = ref(false); const loading = ref(false);
@ -188,7 +189,7 @@ function emitChange() {
} }
</script> </script>
<template> <template>
<div v-bind="{ ...$attrs }"> <div v-bind="{ class: className, style }">
<component <component
:is="component" :is="component"
v-bind="bindProps" v-bind="bindProps"

View File

@ -55,7 +55,7 @@ async function calcContentHeight() {
footerHeight.value = footerRef.value?.offsetHeight || 0; footerHeight.value = footerRef.value?.offsetHeight || 0;
setTimeout(() => { setTimeout(() => {
shouldAutoHeight.value = true; shouldAutoHeight.value = true;
}, 30); }, 100);
} }
onMounted(() => { onMounted(() => {