diff --git a/apps/vben5/apps/app-antd/package.json b/apps/vben5/apps/app-antd/package.json index a4bc65408..f12c4b965 100644 --- a/apps/vben5/apps/app-antd/package.json +++ b/apps/vben5/apps/app-antd/package.json @@ -30,6 +30,7 @@ "@abp/core": "workspace:*", "@abp/identity": "workspace:*", "@abp/request": "workspace:*", + "@abp/ui": "workspace:*", "@vben/access": "workspace:*", "@vben/common-ui": "workspace:*", "@vben/constants": "workspace:*", diff --git a/apps/vben5/apps/app-antd/src/adapter/component/index.ts b/apps/vben5/apps/app-antd/src/adapter/component/index.ts index 1afa62174..e237b52fc 100644 --- a/apps/vben5/apps/app-antd/src/adapter/component/index.ts +++ b/apps/vben5/apps/app-antd/src/adapter/component/index.ts @@ -21,6 +21,7 @@ import { Input, InputNumber, InputPassword, + InputSearch, Mentions, notification, Radio, @@ -57,6 +58,7 @@ export type ComponentType = | 'Input' | 'InputNumber' | 'InputPassword' + | 'InputSearch' | 'Mentions' | 'PrimaryButton' | 'Radio' @@ -90,6 +92,7 @@ async function initComponentAdapter() { Input: withDefaultPlaceholder(Input, 'input'), InputNumber: withDefaultPlaceholder(InputNumber, 'input'), InputPassword: withDefaultPlaceholder(InputPassword, 'input'), + InputSearch, Mentions: withDefaultPlaceholder(Mentions, 'input'), // 自定义主要按钮 PrimaryButton: (props, { attrs, slots }) => { diff --git a/apps/vben5/apps/app-antd/vite.config.mts b/apps/vben5/apps/app-antd/vite.config.mts index cc4b2bef1..4ccb102a9 100644 --- a/apps/vben5/apps/app-antd/vite.config.mts +++ b/apps/vben5/apps/app-antd/vite.config.mts @@ -10,14 +10,14 @@ export default defineConfig(async () => { changeOrigin: true, // rewrite: (path) => path.replace(/^\/api/, ''), // mock代理目标地址 - target: 'http://81.68.64.105:30001/', + target: 'http://127.0.0.1:30001/', ws: true, }, '/connect': { changeOrigin: true, // rewrite: (path) => path.replace(/^\/api/, ''), // mock代理目标地址 - target: 'http://81.68.64.105:30001/', + target: 'http://127.0.0.1:30001/', ws: true, }, }, diff --git a/apps/vben5/packages/@abp/core/package.json b/apps/vben5/packages/@abp/core/package.json index 7ffa6d8e4..8beec67f0 100644 --- a/apps/vben5/packages/@abp/core/package.json +++ b/apps/vben5/packages/@abp/core/package.json @@ -36,7 +36,12 @@ }, "dependencies": { "dayjs": "catalog:", + "lodash": "catalog:", "pinia": "catalog:", + "pinia-plugin-persistedstate": "catalog:", "vue": "catalog:" + }, + "devDependencies": { + "@types/lodash": "catalog:" } } diff --git a/apps/vben5/packages/@abp/core/src/constants/index.ts b/apps/vben5/packages/@abp/core/src/constants/index.ts new file mode 100644 index 000000000..4d5ffa36a --- /dev/null +++ b/apps/vben5/packages/@abp/core/src/constants/index.ts @@ -0,0 +1 @@ +export * from './validation'; diff --git a/apps/vben5/packages/@abp/core/src/constants/validation.ts b/apps/vben5/packages/@abp/core/src/constants/validation.ts new file mode 100644 index 000000000..27680e687 --- /dev/null +++ b/apps/vben5/packages/@abp/core/src/constants/validation.ts @@ -0,0 +1,46 @@ +export const ValidationEnum = { + DoNotMatch: "'{0}' and '{1}' do not match.", + FieldDoNotValidCreditCardNumber: + 'The {0} field is not a valid credit card number.', + FieldDoNotValidEmailAddress: 'The {0} field is not a valid e-mail address.', + FieldDoNotValidFullyQualifiedUrl: + 'The {0} field is not a valid fully-qualified http, https, or ftp URL.', + FieldDoNotValidPhoneNumber: 'The {0} field is not a valid phone number.', + FieldInvalid: 'The field {0} is invalid.', + FieldIsNotValid: '{0} is not valid.', + FieldMustBeetWeen: 'The field {0} must be between {1} and {2}.', + FieldMustBeStringOrArrayWithMaximumLength: + "The field {0} must be a string or array type with a maximum length of '{1}'.", + FieldMustBeStringOrArrayWithMinimumLength: + "The field {0} must be a string or array type with a minimum length of '{1}'.", + FieldMustBeStringWithMaximumLength: + 'The field {0} must be a string with a maximum length of {1}.', + FieldMustBeStringWithMinimumLengthAndMaximumLength: + 'The field {0} must be a string with a minimum length of {2} and a maximum length of {1}.', + FieldMustMatchRegularExpression: + "The field {0} must match the regular expression '{1}'.", + FieldOnlyAcceptsFilesExtensions: + 'The {0} field only accepts files with the following extensions: {1}', + FieldRequired: 'The {0} field is required.', + ThisFieldIsInvalid: 'ThisFieldIsInvalid.', + ThisFieldIsNotAValidCreditCardNumber: 'ThisFieldIsNotAValidCreditCardNumber.', + ThisFieldIsNotAValidEmailAddress: 'ThisFieldIsNotAValidEmailAddress.', + ThisFieldIsNotAValidFullyQualifiedHttpHttpsOrFtpUrl: + 'ThisFieldIsNotAValidFullyQualifiedHttpHttpsOrFtpUrl', + ThisFieldIsNotAValidPhoneNumber: 'ThisFieldIsNotAValidPhoneNumber.', + ThisFieldIsNotValid: 'ThisFieldIsNotValid.', + ThisFieldIsRequired: 'ThisFieldIsRequired.', + ThisFieldMustBeAStringOrArrayTypeWithAMaximumLength: + 'ThisFieldMustBeAStringOrArrayTypeWithAMaximumLengthOf{0}', + ThisFieldMustBeAStringOrArrayTypeWithAMinimumLength: + 'ThisFieldMustBeAStringOrArrayTypeWithAMinimumLengthOf{0}', + ThisFieldMustBeAStringWithAMaximumLength: + 'ThisFieldMustBeAStringWithAMaximumLengthOf{0}', + ThisFieldMustBeAStringWithAMinimumLengthAndAMaximumLength: + 'ThisFieldMustBeAStringWithAMinimumLengthOf{1}AndAMaximumLengthOf{0}', + ThisFieldMustBeBetween: 'ThisFieldMustBeBetween{0}And{1}', + ThisFieldMustMatchTheRegularExpression: + 'ThisFieldMustMatchTheRegularExpression{0}', + ThisFieldOnlyAcceptsFilesWithTheFollowingExtensions: + 'ThisFieldOnlyAcceptsFilesWithTheFollowingExtensions:{0}', +}; diff --git a/apps/vben5/packages/@abp/core/src/hooks/index.ts b/apps/vben5/packages/@abp/core/src/hooks/index.ts new file mode 100644 index 000000000..1582c45c5 --- /dev/null +++ b/apps/vben5/packages/@abp/core/src/hooks/index.ts @@ -0,0 +1,3 @@ +export * from './useLocalization'; +export * from './useSettings'; +export * from './useValidation'; diff --git a/apps/vben5/packages/@abp/core/src/hooks/useLocalization.ts b/apps/vben5/packages/@abp/core/src/hooks/useLocalization.ts new file mode 100644 index 000000000..4593a11e5 --- /dev/null +++ b/apps/vben5/packages/@abp/core/src/hooks/useLocalization.ts @@ -0,0 +1,70 @@ +import type { Dictionary, StringLocalizer } from '../types'; + +import { computed } from 'vue'; + +import { merge } from 'lodash'; + +import { useAbpStore } from '../store/abp'; +import { format } from '../utils/string'; + +export function useLocalization(resourceNames?: string | string[]) { + const abpStore = useAbpStore(); + const getResource = computed(() => { + if (!abpStore.application) { + return {}; + } + const { values } = abpStore.application.localization; + + let resource: { [key: string]: string } = {}; + if (resourceNames) { + if (Array.isArray(resourceNames)) { + resourceNames.forEach((name) => { + resource = merge(resource, values[name]); + }); + } else { + resource = merge(resource, values[resourceNames]); + } + } else { + Object.keys(values).forEach((rs) => { + resource = merge(resource, values[rs]); + }); + } + + return resource; + }); + const getResourceByName = computed(() => { + return (resource: string): Dictionary => { + if (!abpStore.application) { + return {}; + } + const { values } = abpStore.application.localization; + return values[resource] ?? {}; + }; + }); + + function L(key: string, args?: any[] | Record | undefined) { + if (!key) return ''; + if (!getResource.value) return key; + if (!getResource.value[key]) return key; + return format(getResource.value[key], args ?? []); + } + + function Lr( + resource: string, + key: string, + args?: any[] | Record | undefined, + ) { + if (!key) return ''; + const findResource = getResourceByName.value(resource); + if (!findResource) return key; + if (!findResource[key]) return key; + return format(findResource[key], args ?? []); + } + + const localizer: StringLocalizer = { + L, + Lr, + }; + + return { L, localizer, Lr }; +} diff --git a/apps/vben5/packages/@abp/core/src/hooks/useSettings.ts b/apps/vben5/packages/@abp/core/src/hooks/useSettings.ts new file mode 100644 index 000000000..a573d3c23 --- /dev/null +++ b/apps/vben5/packages/@abp/core/src/hooks/useSettings.ts @@ -0,0 +1,64 @@ +import type { ISettingProvider, SettingValue } from '../types/settings'; + +import { computed } from 'vue'; + +import { useAbpStore } from '../store'; + +export function useSettings(): ISettingProvider { + const getSettings = computed(() => { + const abpStore = useAbpStore(); + if (!abpStore.application) { + return []; + } + const { values: settings } = abpStore.application.setting; + const settingValues = Object.keys(settings).map((key): SettingValue => { + return { + name: key, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + value: settings[key]!, + }; + }); + return settingValues; + }); + + function get(name: string): SettingValue | undefined { + return getSettings.value.find((setting) => name === setting.name); + } + + function getAll(...names: string[]): SettingValue[] { + if (names) { + return getSettings.value.filter((setting) => + names.includes(setting.name), + ); + } + return getSettings.value; + } + + function getOrDefault(name: string, defaultValue: T): string | T { + const setting = get(name); + if (!setting) { + return defaultValue; + } + return setting.value; + } + + const settingProvider: ISettingProvider = { + getAll(...names: string[]) { + return getAll(...names); + }, + getNumber(name: string, defaultValue: number = 0) { + const value = getOrDefault(name, defaultValue); + const num = Number(value); + return Number.isNaN(num) ? defaultValue : num; + }, + getOrEmpty(name: string) { + return getOrDefault(name, ''); + }, + isTrue(name: string) { + const value = getOrDefault(name, 'false'); + return value.toLowerCase() === 'true'; + }, + }; + + return settingProvider; +} diff --git a/apps/vben5/packages/@abp/core/src/hooks/useValidation.ts b/apps/vben5/packages/@abp/core/src/hooks/useValidation.ts new file mode 100644 index 000000000..8a04e63e7 --- /dev/null +++ b/apps/vben5/packages/@abp/core/src/hooks/useValidation.ts @@ -0,0 +1,405 @@ +import type { RuleCreator } from '../types/rules'; +import type { + Field, + FieldBeetWeen, + FieldContains, + FieldDefineValidator, + FieldLength, + FieldMatch, + FieldRange, + FieldRegular, + FieldValidator, + Rule, + RuleType, +} from '../types/validations'; + +import { ValidationEnum } from '../constants'; +import { isEmail, isPhone } from '../utils/regex'; +import { useLocalization } from './useLocalization'; + +export function useValidation() { + const { L } = useLocalization(['AbpValidation']); + function _getFieldName(field: Field) { + return __getFieldName( + field.name ?? '', + field.resourceName, + field.prefix, + field.connector, + ); + } + + function __getFieldName( + fieldName: string, + resourceName?: string, + prefix?: string, + connector?: string, + ) { + if (fieldName && resourceName) { + fieldName = prefix + ? `${prefix}${connector ?? ':'}${fieldName}` + : fieldName; + const { L: l } = useLocalization(resourceName); + return l(fieldName); + } + return fieldName; + } + + function _createRule(options: { + len?: number; + max?: number; + message?: string; + min?: number; + required?: boolean; + trigger?: 'blur' | 'change' | ['change', 'blur']; + type?: 'array' | RuleType; + validator?: ( + rule: any, + value: any, + callback: any, + source?: any, + options?: any, + ) => Promise | void; + }): Rule[] { + return [ + { + len: options.len, + max: options.max, + message: options.message, + min: options.min, + required: options.required, + trigger: options.trigger, + type: options.type, + validator: options.validator, + }, + ]; + } + + function _createValidator( + field: Field, + useNameEnum: string, + notNameEnum: string, + required?: boolean, + ): Rule { + const message = field.name + ? L(useNameEnum, [_getFieldName(field)]) + : L(notNameEnum); + return { + message, + required, + trigger: field.trigger, + type: field.type, + }; + } + + function _createLengthValidator( + field: FieldLength, + checkMaximum: boolean, + useNameEnum: string, + notNameEnum: string, + required?: boolean, + ): Rule { + const message = field.name + ? L(useNameEnum, [_getFieldName(field), field.length]) + : L(notNameEnum, [field.length]); + + function checkLength(value: any[] | string) { + return checkMaximum + ? field.length > value.length + : value.length > field.length; + } + + return { + message, + required, + trigger: field.trigger, + type: field.type, + validator: (_: any, value: string) => { + if (!checkLength(value)) { + return Promise.reject(message); + } + return Promise.resolve(); + }, + }; + } + + function _createLengthRangValidator( + field: FieldRange, + useNameEnum: string, + notNameEnum: string, + required?: boolean, + ): Rule { + const message = field.name + ? L(useNameEnum, [_getFieldName(field), field.minimum, field.maximum]) + : L(notNameEnum, [field.minimum, field.maximum]); + return { + message, + required, + trigger: field.trigger, + type: field.type, + validator: (_: any, value: string) => { + if (value.length < field.minimum || value.length > field.maximum) { + return Promise.reject(message); + } + return Promise.resolve(); + }, + }; + } + + function _createBeetWeenValidator(field: FieldBeetWeen): Rule { + const message = field.name + ? L(ValidationEnum.FieldMustBeetWeen, [ + _getFieldName(field), + field.start, + field.end, + ]) + : L(ValidationEnum.ThisFieldMustBeBetween, [field.start, field.end]); + return { + message, + trigger: field.trigger, + validator: (_: any, value: number) => { + // beetween不在进行必输检查, 改为数字有效性检查 + if (Number.isNaN(value)) { + return Promise.reject(message); + } + return value < field.start || value > field.end + ? Promise.reject(message) + : Promise.resolve(); + }, + }; + } + + function _createRegularExpressionValidator( + field: FieldRegular, + required?: boolean, + ): Rule { + const message = field.name + ? L(ValidationEnum.FieldMustMatchRegularExpression, [ + _getFieldName(field), + field.expression, + ]) + : L(ValidationEnum.ThisFieldMustMatchTheRegularExpression, [ + field.expression, + ]); + return { + message, + pattern: new RegExp(field.expression), + required, + trigger: field.trigger, + type: field.type, + }; + } + + function _createEmailValidator(field: Field, required?: boolean): Rule { + const message = field.name + ? L(ValidationEnum.FieldDoNotValidEmailAddress, [_getFieldName(field)]) + : L(ValidationEnum.ThisFieldIsNotAValidEmailAddress); + return { + message, + required, + trigger: field.trigger, + type: field.type, + validator: (_: any, value: string) => { + if (!isEmail(value)) { + return Promise.reject(message); + } + return Promise.resolve(); + }, + }; + } + + function _createPhoneValidator(field: Field, required?: boolean): Rule { + const message = field.name + ? L(ValidationEnum.FieldDoNotValidPhoneNumber, [_getFieldName(field)]) + : L(ValidationEnum.ThisFieldIsNotAValidPhoneNumber); + return { + message, + required, + trigger: field.trigger, + type: field.type, + validator: (_: any, value: string) => { + if (!isPhone(value)) { + return Promise.reject(message); + } + return Promise.resolve(); + }, + }; + } + + const ruleCreator: RuleCreator = { + defineValidator(field: FieldDefineValidator) { + return _createRule(field); + }, + doNotMatch(field: FieldMatch) { + const message = L(ValidationEnum.DoNotMatch, [ + __getFieldName(field.name, field.resourceName, field.prefix), + __getFieldName(field.matchField, field.resourceName, field.prefix), + ]); + return _createRule({ + message, + required: field.required, + trigger: field.trigger, + type: field.type, + validator: (_, value: string) => { + if (value !== field.matchValue) { + return Promise.reject(message); + } + return Promise.resolve(); + }, + }); + }, + fieldDoNotValidCreditCardNumber(field: Field) { + if (field.name) { + return _createRule({ + message: L(ValidationEnum.FieldDoNotValidCreditCardNumber, [ + _getFieldName(field), + ]), + trigger: field.trigger, + type: field.type, + }); + } + return _createRule({ + message: L(ValidationEnum.ThisFieldIsNotAValidCreditCardNumber), + trigger: field.trigger, + type: field.type, + }); + }, + fieldDoNotValidEmailAddress(field: Field) { + return [_createEmailValidator(field)]; + }, + fieldDoNotValidFullyQualifiedUrl(field: Field) { + if (field.name) { + return _createRule({ + message: L(ValidationEnum.FieldDoNotValidFullyQualifiedUrl, [ + _getFieldName(field), + ]), + trigger: field.trigger, + type: field.type, + }); + } + return _createRule({ + message: L( + ValidationEnum.ThisFieldIsNotAValidFullyQualifiedHttpHttpsOrFtpUrl, + ), + trigger: field.trigger, + type: field.type, + }); + }, + fieldDoNotValidPhoneNumber(field: Field) { + return [_createPhoneValidator(field)]; + }, + fieldInvalid(field: FieldValidator) { + const message = field.name + ? L(ValidationEnum.FieldInvalid, [_getFieldName(field)]) + : L(ValidationEnum.ThisFieldIsInvalid); + return _createRule({ + message, + required: field.required, + trigger: field.trigger, + type: field.type, + validator: (_, value: any) => { + if (!field.validator(value)) { + return Promise.reject(message); + } + return Promise.resolve(); + }, + }); + }, + fieldIsNotValid(field: FieldValidator) { + const message = field.name + ? L(ValidationEnum.FieldIsNotValid, [_getFieldName(field)]) + : L(ValidationEnum.ThisFieldIsNotValid); + return _createRule({ + message, + required: field.required, + trigger: field.trigger, + type: field.type, + validator: (_, value: any) => { + if (field.validator(value)) { + return Promise.reject(message); + } + return Promise.resolve(); + }, + }); + }, + fieldMustBeetWeen(field: FieldBeetWeen) { + return [_createBeetWeenValidator(field)]; + }, + fieldMustBeStringOrArrayWithMaximumLength(field: FieldLength) { + return [ + _createLengthValidator( + field, + true, + ValidationEnum.FieldMustBeStringOrArrayWithMaximumLength, + ValidationEnum.ThisFieldMustBeAStringOrArrayTypeWithAMaximumLength, + ), + ]; + }, + fieldMustBeStringOrArrayWithMinimumLength(field: FieldLength) { + return [ + _createLengthValidator( + field, + false, + ValidationEnum.FieldMustBeStringOrArrayWithMinimumLength, + ValidationEnum.ThisFieldMustBeAStringOrArrayTypeWithAMinimumLength, + ), + ]; + }, + fieldMustBeStringWithMaximumLength(field: FieldLength) { + return [ + _createLengthValidator( + field, + true, + ValidationEnum.FieldMustBeStringWithMaximumLength, + ValidationEnum.ThisFieldMustBeAStringWithAMaximumLength, + ), + ]; + }, + fieldMustBeStringWithMinimumLengthAndMaximumLength(field: FieldRange) { + return [ + _createLengthRangValidator( + field, + ValidationEnum.FieldMustBeStringWithMinimumLengthAndMaximumLength, + ValidationEnum.ThisFieldMustBeAStringWithAMinimumLengthAndAMaximumLength, + ), + ]; + }, + fieldMustMatchRegularExpression(field: FieldRegular) { + return [_createRegularExpressionValidator(field)]; + }, + fieldOnlyAcceptsFilesExtensions(field: FieldContains) { + const message = field.name + ? L(ValidationEnum.FieldOnlyAcceptsFilesExtensions, [ + _getFieldName(field), + field.value, + ]) + : L(ValidationEnum.ThisFieldMustMatchTheRegularExpression, [ + field.value, + ]); + return _createRule({ + message, + trigger: field.trigger, + type: field.type, + validator: (_, value: string) => { + if (!field.value.includes(value)) { + return Promise.reject(message); + } + return Promise.resolve(); + }, + }); + }, + fieldRequired(field: Field) { + return [ + _createValidator( + field, + ValidationEnum.FieldRequired, + ValidationEnum.ThisFieldIsRequired, + true, + ), + ]; + }, + }; + + return { + ruleCreator, + }; +} diff --git a/apps/vben5/packages/@abp/core/src/index.ts b/apps/vben5/packages/@abp/core/src/index.ts index 48ebf8d9a..067745944 100644 --- a/apps/vben5/packages/@abp/core/src/index.ts +++ b/apps/vben5/packages/@abp/core/src/index.ts @@ -1,3 +1,5 @@ +export * from './constants'; +export * from './hooks'; export * from './store'; export * from './types'; export * from './utils'; diff --git a/apps/vben5/packages/@abp/core/src/store/abp.ts b/apps/vben5/packages/@abp/core/src/store/abp.ts index 4134845b2..8d035181d 100644 --- a/apps/vben5/packages/@abp/core/src/store/abp.ts +++ b/apps/vben5/packages/@abp/core/src/store/abp.ts @@ -7,54 +7,63 @@ import { ref } from 'vue'; import { acceptHMRUpdate, defineStore } from 'pinia'; -export const useAbpStore = defineStore('abp', () => { - const application = ref(); - const localization = ref(); - /** 获取 i18n 格式本地化文本 */ - function getI18nLocales() { - const abpLocales: Record = {}; - if (!localization.value) { +export const useAbpStore = defineStore( + 'abp', + () => { + const application = ref(); + const localization = ref(); + /** 获取 i18n 格式本地化文本 */ + function getI18nLocales() { + const abpLocales: Record = {}; + if (!localization.value) { + return abpLocales; + } + const resources = localization.value.resources; + // AbpValidation.The field {0} is invalid. + Object.keys(resources).forEach((resource) => { + // resource --> AbpValidation + const resourceLocales: Record = {}; + const resourcesByName = resources[resource]; + if (resourcesByName) { + Object.keys(resourcesByName.texts).forEach((key) => { + // The field {0} is invalid. --> The field {0} is invalid_ + let localeKey = key.replaceAll('.', '_'); + // The field {0} is invalid. --> The field {0} is invalid + localeKey.endsWith('_') && + (localeKey = localeKey.slice( + 0, + Math.max(0, localeKey.length - 1), + )); + // _The field {0} is invalid --> The field {0} is invalid + localeKey.startsWith('_') && + (localeKey = localeKey.slice(0, Math.max(1, localeKey.length))); + resourceLocales[localeKey] = resourcesByName.texts[key]; + }); + abpLocales[resource] = resourceLocales; + } + }); return abpLocales; } - const resources = localization.value.resources; - // AbpValidation.The field {0} is invalid. - Object.keys(resources).forEach((resource) => { - // resource --> AbpValidation - const resourceLocales: Record = {}; - const resourcesByName = resources[resource]; - if (resourcesByName) { - Object.keys(resourcesByName.texts).forEach((key) => { - // The field {0} is invalid. --> The field {0} is invalid_ - let localeKey = key.replaceAll('.', '_'); - // The field {0} is invalid. --> The field {0} is invalid - localeKey.endsWith('_') && - (localeKey = localeKey.slice(0, Math.max(0, localeKey.length - 1))); - // _The field {0} is invalid --> The field {0} is invalid - localeKey.startsWith('_') && - (localeKey = localeKey.slice(0, Math.max(1, localeKey.length))); - resourceLocales[localeKey] = resourcesByName.texts[key]; - }); - abpLocales[resource] = resourceLocales; - } - }); - return abpLocales; - } - function setApplication(val: ApplicationConfigurationDto) { - application.value = val; - } + function setApplication(val: ApplicationConfigurationDto) { + application.value = val; + } - function setLocalization(val: ApplicationLocalizationDto) { - localization.value = val; - } + function setLocalization(val: ApplicationLocalizationDto) { + localization.value = val; + } - return { - application, - getI18nLocales, - localization, - setApplication, - setLocalization, - }; -}); + return { + application, + getI18nLocales, + localization, + setApplication, + setLocalization, + }; + }, + { + persist: true, + }, +); // 解决热更新问题 const hot = import.meta.hot; diff --git a/apps/vben5/packages/@abp/core/src/types/index.ts b/apps/vben5/packages/@abp/core/src/types/index.ts index 56615902d..97fa56fd8 100644 --- a/apps/vben5/packages/@abp/core/src/types/index.ts +++ b/apps/vben5/packages/@abp/core/src/types/index.ts @@ -1,2 +1,6 @@ export * from './dto'; export * from './global'; +export * from './localization'; +export * from './rules'; +export * from './settings'; +export * from './validations'; diff --git a/apps/vben5/packages/@abp/core/src/types/localization.ts b/apps/vben5/packages/@abp/core/src/types/localization.ts new file mode 100644 index 000000000..19bebb7d9 --- /dev/null +++ b/apps/vben5/packages/@abp/core/src/types/localization.ts @@ -0,0 +1,10 @@ +interface StringLocalizer { + L(key: string, args?: any[] | Record | undefined): string; + Lr( + resource: string, + key: string, + args?: any[] | Record | undefined, + ): string; +} + +export type { StringLocalizer }; diff --git a/apps/vben5/packages/@abp/core/src/types/rules.ts b/apps/vben5/packages/@abp/core/src/types/rules.ts new file mode 100644 index 000000000..aef6ed573 --- /dev/null +++ b/apps/vben5/packages/@abp/core/src/types/rules.ts @@ -0,0 +1,50 @@ +import type { + Field, + FieldBeetWeen, + FieldContains, + FieldDefineValidator, + FieldLength, + FieldMatch, + FieldRange, + FieldRegular, + FieldValidator, + Rule, +} from './validations'; + +/** 规则创建器 */ +interface RuleCreator { + /** 自定义验证器 */ + defineValidator(field: FieldDefineValidator): Rule[]; + /** input 与 value 是否匹配 */ + doNotMatch(field: FieldMatch): Rule[]; + /** 字段{0}不是有效的信用卡号码 */ + fieldDoNotValidCreditCardNumber(field: Field): Rule[]; + /** 字段{0}不是有效的邮箱地址 */ + fieldDoNotValidEmailAddress(field: Field): Rule[]; + /** 字段{0}不是有效的完全限定的http,https或ftp URL. */ + fieldDoNotValidFullyQualifiedUrl(field: Field): Rule[]; + /** 字段{0}不是有效的手机号码 */ + fieldDoNotValidPhoneNumber(field: Field): Rule[]; + /** 字段是无效值 */ + fieldInvalid(field: FieldValidator): Rule[]; + /** 验证未通过 */ + fieldIsNotValid(field: FieldValidator): Rule[]; + /** 字段{0}值必须在{1}和{2}范围内 */ + fieldMustBeetWeen(field: FieldBeetWeen): Rule[]; + /** 字段{0}必须是最大长度为'{1}'的字符串或数组 */ + fieldMustBeStringOrArrayWithMaximumLength(field: FieldLength): Rule[]; + /** 字段{0}必须是最小长度为'{1}'的字符串或数组 */ + fieldMustBeStringOrArrayWithMinimumLength(field: FieldLength): Rule[]; + /** 字段{0}必须是最大长度为{1}的字符串 */ + fieldMustBeStringWithMaximumLength(field: FieldLength): Rule[]; + /** 字段{0}必须是最小长度为{2}并且最大长度{1}的字符串 */ + fieldMustBeStringWithMinimumLengthAndMaximumLength(field: FieldRange): Rule[]; + /** 字段{0}与正则表达式不匹配 */ + fieldMustMatchRegularExpression(field: FieldRegular): Rule[]; + /** {0}字段只允许以下扩展名的文件: {1} */ + fieldOnlyAcceptsFilesExtensions(field: FieldContains): Rule[]; + /** 字段{0}不可为空 */ + fieldRequired(field: Field): Rule[]; +} + +export type { RuleCreator }; diff --git a/apps/vben5/packages/@abp/core/src/types/settings.ts b/apps/vben5/packages/@abp/core/src/types/settings.ts new file mode 100644 index 000000000..c00bdfc97 --- /dev/null +++ b/apps/vben5/packages/@abp/core/src/types/settings.ts @@ -0,0 +1,31 @@ +import type { NameValue } from './global'; + +type SettingValue = NameValue; +/** + * 设置接口 + */ +interface ISettingProvider { + /** + * 获取设定值结合 + * @param names 过滤的设置名称 + */ + getAll(...names: string[]): SettingValue[]; + /** + * 查询 number 类型设定值 + * @param name 设置名称 + * @returns 返回类型为 number 的设定值, 默认0 + */ + getNumber(name: string): number; + /** + * 获取设定值,如果为空返回空字符串 + * @param name 设置名称 + */ + getOrEmpty(name: string): string; + /** + * 查询 boolean 类型设定值 + * @param name 设置名称 + */ + isTrue(name: string): boolean; +} + +export type { ISettingProvider, SettingValue }; diff --git a/apps/vben5/packages/@abp/core/src/types/validations.ts b/apps/vben5/packages/@abp/core/src/types/validations.ts new file mode 100644 index 000000000..4f4a10657 --- /dev/null +++ b/apps/vben5/packages/@abp/core/src/types/validations.ts @@ -0,0 +1,112 @@ +type RuleType = + | 'boolean' + | 'date' + | 'email' + | 'enum' + | 'float' + | 'hex' + | 'integer' + | 'method' + | 'number' + | 'object' + | 'regexp' + | 'string' + | 'url'; + +interface Rule { + [key: string]: any; + trigger?: 'blur' | 'change' | ['change', 'blur']; + type?: 'array' | RuleType; +} + +interface Field extends Rule { + /** 连接符 + * @description 用于本地化字段名称时的连接字符 + * @example . => L('ResourceName.DisplayName.Field') + * @example : => L('ResourceName.DisplayName:Field') + */ + connector?: string; + /** 字段名称 */ + name?: string; + /** 字段前缀 + * @description 用于本地化字段名称 + * @example DisplayName => L('ResourceName.DisplayName:Field') + */ + prefix?: string; + /** 本地化资源 */ + resourceName?: string; +} + +interface FieldRequired extends Field { + /** 是否必须 */ + required?: boolean; +} + +interface FieldBeetWeen extends Field { + /** 结束值 */ + end: number; + /** 起始值 */ + start: number; +} + +interface FieldLength extends Field { + /** 长度 */ + length: number; +} + +interface FieldRange extends Field { + /** 最大数值 */ + maximum: number; + /** 最小数值 */ + minimum: number; +} + +interface FieldRegular extends Field { + /** 正则表达式 */ + expression: string; +} + +interface FieldMatch extends FieldRequired { + /** 对比字段 */ + matchField: string; + /** 对比字段值 */ + matchValue: string; + /** 字段名称 */ + name: string; +} + +interface FieldContains extends Field { + /** 验证的值中是否包含在定义的值中 */ + value: string; +} + +interface FieldValidator extends FieldRequired { + /** 值是否有效验证器 */ + validator: (value: any) => boolean; +} + +interface FieldDefineValidator extends FieldRequired { + message?: string; + validator: ( + rule: any, + value: any, + callback: any, + source?: any, + options?: any, + ) => Promise | void; +} + +export type { + Field, + FieldBeetWeen, + FieldContains, + FieldDefineValidator, + FieldLength, + FieldMatch, + FieldRange, + FieldRegular, + FieldRequired, + FieldValidator, + Rule, + RuleType, +}; diff --git a/apps/vben5/packages/@abp/core/src/utils/index.ts b/apps/vben5/packages/@abp/core/src/utils/index.ts index 61cf91f96..202185507 100644 --- a/apps/vben5/packages/@abp/core/src/utils/index.ts +++ b/apps/vben5/packages/@abp/core/src/utils/index.ts @@ -1,2 +1,4 @@ export * from './date'; +export * from './regex'; +export * from './string'; export * from './tree'; diff --git a/apps/vben5/packages/@abp/core/src/utils/regex.ts b/apps/vben5/packages/@abp/core/src/utils/regex.ts new file mode 100644 index 000000000..fb047e06d --- /dev/null +++ b/apps/vben5/packages/@abp/core/src/utils/regex.ts @@ -0,0 +1,47 @@ +/* eslint-disable regexp/no-unused-capturing-group */ +export function isRegMatch(reg: RegExp, val: string) { + return reg.test(val); +} + +export function isEmail(val: string) { + const reg = /^\w+((-\w+)|(\.\w+))*@[A-Z0-9]+((\.|-)[A-Z0-9]+)*\.[A-Z0-9]+$/i; + return isRegMatch(reg, val); +} + +export function isPhone(val: string) { + const reg = /^(13\d|14[5|7]|15\d|17\d|18\d|19\d)\d{8}$/; + return isRegMatch(reg, val); +} + +export function isDigit(val: string) { + return [...val].some((element) => _isDigit(element)); +} + +export function isLower(val: string) { + return [...val].some((element) => _isLower(element)); +} + +export function isUpper(val: string) { + return [...val].some((element) => _isUpper(element)); +} + +export function isLetterOrDigit(val: string) { + const arr = [...val]; + return !arr.some((element) => _isLetterOrDigit(element)); +} + +function _isDigit(char: string) { + return isRegMatch(/\d/g, char); +} + +function _isLower(char: string) { + return isRegMatch(/[a-z]/g, char); +} + +function _isUpper(char: string) { + return isRegMatch(/[A-Z]/g, char); +} + +function _isLetterOrDigit(char: string) { + return isRegMatch(/[^ \w]/g, char); +} diff --git a/apps/vben5/packages/@abp/core/src/utils/string.ts b/apps/vben5/packages/@abp/core/src/utils/string.ts new file mode 100644 index 000000000..cca6eb5aa --- /dev/null +++ b/apps/vben5/packages/@abp/core/src/utils/string.ts @@ -0,0 +1,37 @@ +/** + * + * @param str 字符串是否为空或空格 + */ +export function isNullOrWhiteSpace(str?: string) { + return str === undefined || str === null || str === '' || str === ' '; +} + +/** + * 格式化字符串 + * @param formatted 需要处理的字符串 + * @param args 参数列表,可以是数组,也可以是对象 + * @returns 返回格式化的字符串 + * @example format('Hello, {0}!', ['World']) + * @example format('Hello, {name}!', {name: 'World'}) + */ +export function format(formatted: string, args: any[] | object) { + if (Array.isArray(args)) { + for (const [i, arg] of args.entries()) { + const regexp = new RegExp(String.raw`\{` + i + String.raw`\}`, 'gi'); + formatted = formatted.replace(regexp, arg); + } + } else if (typeof args === 'object') { + Object.keys(args).forEach((key) => { + const regexp = new RegExp(String.raw`\{` + key + String.raw`\}`, 'gi'); + const param = (args as any)[key]; + formatted = formatted.replace(regexp, param); + }); + } + return formatted; +} + +export function getUnique(val: string) { + const arr = [...val]; + const newArr = [...new Set(arr)]; + return newArr.join(''); +} diff --git a/apps/vben5/packages/@abp/identity/src/api/roles.ts b/apps/vben5/packages/@abp/identity/src/api/roles.ts index 8292bd7ae..06da49391 100644 --- a/apps/vben5/packages/@abp/identity/src/api/roles.ts +++ b/apps/vben5/packages/@abp/identity/src/api/roles.ts @@ -70,7 +70,7 @@ export function getPagedListApi( * @param id 角色id * @param ouId 组织机构id */ -export function removeOrganizationUnit( +export function removeOrganizationUnitApi( id: string, ouId: string, ): Promise { diff --git a/apps/vben5/packages/@abp/identity/src/api/users.ts b/apps/vben5/packages/@abp/identity/src/api/users.ts index dd51b9028..c4cea8535 100644 --- a/apps/vben5/packages/@abp/identity/src/api/users.ts +++ b/apps/vben5/packages/@abp/identity/src/api/users.ts @@ -1,6 +1,8 @@ -import type { PagedResultDto } from '@abp/core'; +import type { ListResultDto, PagedResultDto } from '@abp/core'; +import type { IdentityRoleDto, OrganizationUnitDto } from '../types'; import type { + ChangeUserPasswordInput, GetUserPagedListInput, IdentityUserCreateDto, IdentityUserDto, @@ -70,7 +72,7 @@ export function getPagedListApi( * @param id 用户id * @param ouId 组织机构id */ -export function removeOrganizationUnit( +export function removeOrganizationUnitApi( id: string, ouId: string, ): Promise { @@ -79,6 +81,18 @@ export function removeOrganizationUnit( ); } +/** + * 获取用户组织机构列表 + * @param id 用户id + */ +export function getOrganizationUnitsApi( + id: string, +): Promise> { + return requestClient.get>( + `/api/identity/users/${id}/organization-units`, + ); +} + /** * 锁定用户 * @param id 用户id @@ -95,3 +109,41 @@ export function lockApi(id: string, seconds: number): Promise { export function unLockApi(id: string): Promise { return requestClient.put(`/api/identity/users/${id}/unlock`); } + +/** + * 更改用户密码 + * @param id 用户id + * @param input 密码变更dto + */ +export function changePasswordApi( + id: string, + input: ChangeUserPasswordInput, +): Promise { + return requestClient.put( + `/api/identity/users/change-password?id=${id}`, + input, + ); +} + +/** + * 获取可用的角色列表 + */ +export function getAssignableRolesApi(): Promise< + ListResultDto +> { + return requestClient.get>( + `/api/identity/users/assignable-roles`, + ); +} + +/** + * 获取用户角色列表 + * @param id 用户id + */ +export function getRolesApi( + id: string, +): Promise> { + return requestClient.get>( + `/api/identity/users/${id}/roles`, + ); +} diff --git a/apps/vben5/packages/@abp/identity/src/components/organization-units/OrganizationUnitRoleTable.vue b/apps/vben5/packages/@abp/identity/src/components/organization-units/OrganizationUnitRoleTable.vue index 426a754a9..3f50eb0bc 100644 --- a/apps/vben5/packages/@abp/identity/src/components/organization-units/OrganizationUnitRoleTable.vue +++ b/apps/vben5/packages/@abp/identity/src/components/organization-units/OrganizationUnitRoleTable.vue @@ -16,7 +16,7 @@ import { DeleteOutlined, PlusOutlined } from '@ant-design/icons-vue'; import { Button, Modal } from 'ant-design-vue'; import { addRoles, getRoleListApi } from '../../api/organization-units'; -import { removeOrganizationUnit } from '../../api/roles'; +import { removeOrganizationUnitApi } from '../../api/roles'; import { OrganizationUnitPermissions } from '../../constants/permissions'; defineOptions({ @@ -111,7 +111,7 @@ const onDelete = (row: IdentityRoleDto) => { ]), onOk: () => { setLoading(true); - return removeOrganizationUnit(row.id, props.selectedKey!) + return removeOrganizationUnitApi(row.id, props.selectedKey!) .then(onRefresh) .finally(() => setLoading(false)); }, diff --git a/apps/vben5/packages/@abp/identity/src/components/organization-units/OrganizationUnitUserTable.vue b/apps/vben5/packages/@abp/identity/src/components/organization-units/OrganizationUnitUserTable.vue index 275a8498a..250105026 100644 --- a/apps/vben5/packages/@abp/identity/src/components/organization-units/OrganizationUnitUserTable.vue +++ b/apps/vben5/packages/@abp/identity/src/components/organization-units/OrganizationUnitUserTable.vue @@ -14,7 +14,7 @@ import { DeleteOutlined, PlusOutlined } from '@ant-design/icons-vue'; import { Button, Modal } from 'ant-design-vue'; import { addMembers, getUserListApi } from '../../api/organization-units'; -import { removeOrganizationUnit } from '../../api/users'; +import { removeOrganizationUnitApi } from '../../api/users'; import { OrganizationUnitPermissions } from '../../constants/permissions'; defineOptions({ @@ -113,7 +113,7 @@ const onDelete = (row: IdentityUserDto) => { ]), onOk: () => { setLoading(true); - return removeOrganizationUnit(row.id, props.selectedKey!) + return removeOrganizationUnitApi(row.id, props.selectedKey!) .then(onRefresh) .finally(() => setLoading(false)); }, diff --git a/apps/vben5/packages/@abp/identity/src/components/users/UserModal.vue b/apps/vben5/packages/@abp/identity/src/components/users/UserModal.vue index 3029b621a..b57aab912 100644 --- a/apps/vben5/packages/@abp/identity/src/components/users/UserModal.vue +++ b/apps/vben5/packages/@abp/identity/src/components/users/UserModal.vue @@ -1,7 +1,8 @@ diff --git a/apps/vben5/packages/@abp/identity/src/hooks/index.ts b/apps/vben5/packages/@abp/identity/src/hooks/index.ts new file mode 100644 index 000000000..cf20b7cbc --- /dev/null +++ b/apps/vben5/packages/@abp/identity/src/hooks/index.ts @@ -0,0 +1,2 @@ +export * from './usePasswordValidator'; +export * from './useRandomPassword'; diff --git a/apps/vben5/packages/@abp/identity/src/hooks/usePasswordValidator.ts b/apps/vben5/packages/@abp/identity/src/hooks/usePasswordValidator.ts new file mode 100644 index 000000000..93cf39b1f --- /dev/null +++ b/apps/vben5/packages/@abp/identity/src/hooks/usePasswordValidator.ts @@ -0,0 +1,80 @@ +import { computed, unref } from 'vue'; + +import { + getUnique, + isDigit, + isLetterOrDigit, + isLower, + isNullOrWhiteSpace, + isUpper, + useLocalization, + useSettings, + ValidationEnum, +} from '@abp/core'; + +export function usePasswordValidator() { + const { settingProvider } = useSettings(); + const { getNumber, isTrue } = settingProvider; + const { L } = useLocalization(['AbpIdentity', 'AbpUi']); + + const passwordSetting = computed(() => { + return { + requiredDigit: isTrue('Abp.Identity.Password.RequireDigit'), + requiredLength: getNumber('Abp.Identity.Password.RequiredLength'), + requiredLowercase: isTrue('Abp.Identity.Password.RequireLowercase'), + requiredUniqueChars: getNumber( + 'Abp.Identity.Password.RequiredUniqueChars', + ), + requireNonAlphanumeric: isTrue( + 'Abp.Identity.Password.RequireNonAlphanumeric', + ), + requireUppercase: isTrue('Abp.Identity.Password.RequireUppercase'), + }; + }); + + function validate(password: string): Promise { + return new Promise((resolve, reject) => { + if (isNullOrWhiteSpace(password)) { + return reject( + L(ValidationEnum.FieldRequired, [L('DisplayName:Password')]), + ); + } + const setting = unref(passwordSetting); + if ( + setting.requiredLength > 0 && + password.length < setting.requiredLength + ) { + return reject( + L('Volo.Abp.Identity:PasswordTooShort', [setting.requiredLength]), + ); + } + if (setting.requireNonAlphanumeric && isLetterOrDigit(password)) { + return reject(L('Volo.Abp.Identity:PasswordRequiresNonAlphanumeric')); + } + if (setting.requiredDigit && !isDigit(password)) { + return reject(L('Volo.Abp.Identity:PasswordRequiresDigit')); + } + if (setting.requiredLowercase && !isLower(password)) { + return reject(L('Volo.Abp.Identity:PasswordRequiresLower')); + } + if (setting.requireUppercase && !isUpper(password)) { + return reject(L('Volo.Abp.Identity:PasswordRequiresUpper')); + } + if ( + setting.requiredUniqueChars >= 1 && + getUnique(password).length < setting.requiredUniqueChars + ) { + return reject( + L('Volo.Abp.Identity:PasswordRequiredUniqueChars', [ + setting.requiredUniqueChars, + ]), + ); + } + return resolve(); + }); + } + + return { + validate, + }; +} diff --git a/apps/vben5/packages/@abp/identity/src/hooks/useRandomPassword.ts b/apps/vben5/packages/@abp/identity/src/hooks/useRandomPassword.ts new file mode 100644 index 000000000..4d0a21e5c --- /dev/null +++ b/apps/vben5/packages/@abp/identity/src/hooks/useRandomPassword.ts @@ -0,0 +1,77 @@ +import { useSettings } from '@abp/core'; + +/** + * 摘自 https://www.html5tricks.com/demo/js-passwd-generator/index.html + * 侵权请联系删除 + */ +export function useRandomPassword() { + const randomFunc: { [key: string]: () => string } = { + defaultNumber: getRandomNumber, + lower: getRandomLower, + number: getRandomNumber, + symbol: getRandomSymbol, + upper: getRandomUpper, + }; + + function getRandomLower() { + return String.fromCodePoint(Math.floor(Math.random() * 26) + 97); + } + + function getRandomUpper() { + return String.fromCodePoint(Math.floor(Math.random() * 26) + 65); + } + + function getRandomNumber() { + return String.fromCodePoint(Math.floor(Math.random() * 10) + 48); + } + + function getRandomSymbol() { + const symbols = '~!@#$%^&*()_+{}":?><;.,'; + return symbols[Math.floor(Math.random() * symbols.length)] ?? ''; + } + + function generatePassword() { + const { settingProvider } = useSettings(); + // 根据配置项生成随机密码 + // 密码长度 + const length = settingProvider.getNumber( + 'Abp.Identity.Password.RequiredLength', + ); + // 需要小写字母 + const lower = settingProvider.isTrue( + 'Abp.Identity.Password.RequireLowercase', + ); + // 需要大写字母 + const upper = settingProvider.isTrue( + 'Abp.Identity.Password.RequireUppercase', + ); + // 需要数字 + const number = settingProvider.isTrue('Abp.Identity.Password.RequireDigit'); + // 需要符号 + const symbol = settingProvider.isTrue( + 'Abp.Identity.Password.RequireNonAlphanumeric', + ); + // 默认生成数字 + const defaultNumber = !lower && !upper && !number && !symbol; + + let generatedPassword = ''; + const typesArr = [ + { lower }, + { upper }, + { number }, + { symbol }, + { defaultNumber }, + ].filter((item) => Object.values(item)[0]); + for (let i = 0; i < length; i++) { + typesArr.forEach((type) => { + const funcName = Object.keys(type)[0]; + if (funcName && randomFunc[funcName]) { + generatedPassword += randomFunc[funcName](); + } + }); + } + return generatedPassword.slice(0, length); + } + + return { generatePassword }; +} diff --git a/apps/vben5/packages/@abp/identity/src/types/users.ts b/apps/vben5/packages/@abp/identity/src/types/users.ts index 0b5cd0bf0..9c718cd88 100644 --- a/apps/vben5/packages/@abp/identity/src/types/users.ts +++ b/apps/vben5/packages/@abp/identity/src/types/users.ts @@ -25,13 +25,18 @@ interface IUser { } /** 更改密码数据传输对象 */ -interface ChangePasswordInput { +interface ChangeMyPasswordInput { /** 当前密码 */ currentPassword?: string; /** 新密码 */ newPassword: string; } +interface ChangeUserPasswordInput { + /** 新密码 */ + password: string; +} + /** 用户组织机构数据传输对象 */ interface IdentityUserOrganizationUnitUpdateDto { /** 组织机构标识列表 */ @@ -86,7 +91,8 @@ type IdentityUserCreateDto = IdentityUserCreateOrUpdateDto; type IdentityUserUpdateDto = IdentityUserCreateOrUpdateDto; export type { - ChangePasswordInput, + ChangeMyPasswordInput, + ChangeUserPasswordInput, GetUserPagedListInput, IdentityUserCreateDto, IdentityUserDto, diff --git a/apps/vben5/packages/@abp/permission/src/components/permissions/PermissionModal.vue b/apps/vben5/packages/@abp/permission/src/components/permissions/PermissionModal.vue index 7fe29479b..fea114430 100644 --- a/apps/vben5/packages/@abp/permission/src/components/permissions/PermissionModal.vue +++ b/apps/vben5/packages/@abp/permission/src/components/permissions/PermissionModal.vue @@ -276,7 +276,7 @@ function getChildren(permissions: PermissionTree[]): PermissionTree[] { {{ $t('AbpPermissionManagement.SelectAllInThisTab') }} @@ -293,7 +293,10 @@ function getChildren(permissions: PermissionTree[]): PermissionTree[] { children: 'children', }" :tree-data="permission.children" - @check="(keys, info) => onCheckNode(permission, keys, info)" + @check=" + (keys: any, info: CheckInfo) => + onCheckNode(permission, keys, info) + " @expand="onExpandNode" @select="onSelectNode" /> diff --git a/apps/vben5/packages/@abp/ui/src/adapter/component/index.ts b/apps/vben5/packages/@abp/ui/src/adapter/component/index.ts index 1afa62174..e237b52fc 100644 --- a/apps/vben5/packages/@abp/ui/src/adapter/component/index.ts +++ b/apps/vben5/packages/@abp/ui/src/adapter/component/index.ts @@ -21,6 +21,7 @@ import { Input, InputNumber, InputPassword, + InputSearch, Mentions, notification, Radio, @@ -57,6 +58,7 @@ export type ComponentType = | 'Input' | 'InputNumber' | 'InputPassword' + | 'InputSearch' | 'Mentions' | 'PrimaryButton' | 'Radio' @@ -90,6 +92,7 @@ async function initComponentAdapter() { Input: withDefaultPlaceholder(Input, 'input'), InputNumber: withDefaultPlaceholder(InputNumber, 'input'), InputPassword: withDefaultPlaceholder(InputPassword, 'input'), + InputSearch, Mentions: withDefaultPlaceholder(Mentions, 'input'), // 自定义主要按钮 PrimaryButton: (props, { attrs, slots }) => { diff --git a/apps/vben5/pnpm-workspace.yaml b/apps/vben5/pnpm-workspace.yaml index cfbfa7f21..f0564f794 100644 --- a/apps/vben5/pnpm-workspace.yaml +++ b/apps/vben5/pnpm-workspace.yaml @@ -44,6 +44,7 @@ catalog: '@types/eslint': ^9.6.1 '@types/html-minifier-terser': ^7.0.2 '@types/jsonwebtoken': ^9.0.7 + '@types/lodash': ^4.17.13 '@types/lodash.clonedeep': ^4.5.9 '@types/node': ^22.10.0 '@types/nprogress': ^0.2.3 @@ -113,6 +114,7 @@ catalog: jsonc-eslint-parser: ^2.4.0 jsonwebtoken: ^9.0.2 lint-staged: ^15.2.10 + lodash: ^4.17.13 lodash.clonedeep: ^4.5.0 lucide-vue-next: ^0.461.0 medium-zoom: ^1.1.0