dev-v5
xingyu4j 2024-12-18 13:58:47 +08:00
commit 5a658d6419
32 changed files with 422 additions and 49 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -186,6 +186,10 @@ function sidebarComponents(): DefaultTheme.SidebarItem[] {
link: 'common-ui/vben-count-to-animator',
text: 'CountToAnimator 数字动画',
},
{
link: 'common-ui/vben-ellipsis-text',
text: 'EllipsisText 省略文本',
},
],
},
];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -138,7 +138,11 @@ defineExpose({
<template>
<div
:class="
cn('col-span-full w-full pb-6 text-right', rootProps.actionWrapperClass)
cn(
'col-span-full w-full text-right',
rootProps.compact ? 'pb-2' : 'pb-6',
rootProps.actionWrapperClass,
)
"
:style="queryFormStyle"
>

View File

@ -130,6 +130,11 @@ export class FormApi {
return form.values;
}
async isFieldValid(fieldName: string) {
const form = await this.getForm();
return form.isFieldValid(fieldName);
}
merge(formApi: FormApi) {
const chain = [this, formApi];
const proxy = new Proxy(formApi, {
@ -348,4 +353,14 @@ export class FormApi {
}
return await this.submitForm();
}
async validateField(fieldName: string, opts?: Partial<ValidationOptions>) {
const form = await this.getForm();
const validateResult = await form.validateField(fieldName, opts);
if (Object.keys(validateResult?.errors ?? {}).length > 0) {
console.error('validate error', validateResult?.errors);
}
return validateResult;
}
}

View File

@ -26,6 +26,7 @@ import { isEventObjectLike } from './helper';
interface Props extends FormSchema {}
const {
colon,
commonComponentProps,
component,
componentEvents,
@ -55,7 +56,7 @@ const values = useFormValues();
const errors = useFieldError(fieldName);
const fieldComponentRef = useTemplateRef<HTMLInputElement>('fieldComponentRef');
const formApi = formRenderProps.form;
const compact = formRenderProps.compact;
const isInValid = computed(() => errors.value?.length > 0);
const FieldComponent = computed(() => {
@ -289,8 +290,10 @@ function autofocus() {
'form-valid-error': isInValid,
'flex-col': isVertical,
'flex-row items-center': !isVertical,
'pb-6': !compact,
'pb-2': compact,
}"
class="flex pb-6"
class="flex"
v-bind="$attrs"
>
<FormLabel
@ -309,7 +312,10 @@ function autofocus() {
:required="shouldRequired && !hideRequiredMark"
:style="labelStyle"
>
{{ label }}
<template v-if="label">
<span>{{ label }}</span>
<span v-if="colon" class="ml-[2px]">:</span>
</template>
</FormLabel>
<div :class="cn('relative flex w-full items-center', wrapperClass)">
<FormControl :class="cn(controlClass)">

View File

@ -86,6 +86,7 @@ const computedSchema = computed(
formFieldProps: Record<string, any>;
} & Omit<FormSchema, 'formFieldProps'>)[] => {
const {
colon = false,
componentProps = {},
controlClass = '',
disabled,
@ -110,6 +111,7 @@ const computedSchema = computed(
: false;
return {
colon,
disabled,
disabledOnChangeListener,
disabledOnInputListener,

View File

@ -145,6 +145,10 @@ type ComponentProps =
| MaybeComponentProps;
export interface FormCommonConfig {
/**
* Label
*/
colon?: boolean;
/**
* props
*/
@ -280,6 +284,10 @@ export interface FormRenderProps<
* 使
*/
commonConfig?: FormCommonConfig;
/**
*
*/
compact?: boolean;
/**
* v-model
*/

View File

@ -6,7 +6,7 @@ import type { ExtendedFormApi, VbenFormProps } from './types';
import { useForwardPriorityValues } from '@vben-core/composables';
// import { isFunction } from '@vben-core/shared/utils';
import { nextTick, onMounted, useTemplateRef, watch } from 'vue';
import { nextTick, onMounted, watch } from 'vue';
import { cloneDeep } from '@vben-core/shared/utils';
@ -27,8 +27,6 @@ interface Props extends VbenFormProps {
const props = defineProps<Props>();
const formActionsRef = useTemplateRef<typeof FormActions>('formActionsRef');
const state = props.formApi?.useStore?.();
const forward = useForwardPriorityValues(props, state);
@ -44,11 +42,7 @@ const handleUpdateCollapsed = (value: boolean) => {
};
function handleKeyDownEnter(event: KeyboardEvent) {
if (
!state.value.submitOnEnter ||
!formActionsRef.value ||
!formActionsRef.value.handleSubmit
) {
if (!state.value.submitOnEnter || !forward.value.formApi?.isMounted) {
return;
}
// textarea
@ -58,12 +52,12 @@ function handleKeyDownEnter(event: KeyboardEvent) {
}
event.preventDefault();
formActionsRef.value?.handleSubmit?.();
forward.value.formApi.validateAndSubmitForm();
}
const handleValuesChangeDebounced = useDebounceFn((newVal) => {
forward.value.handleValuesChange?.(cloneDeep(newVal));
state.value.submitOnChange && formActionsRef.value?.handleSubmit?.();
state.value.submitOnChange && forward.value.formApi?.validateAndSubmitForm();
}, 300);
onMounted(async () => {
@ -94,7 +88,6 @@ onMounted(async () => {
<slot v-bind="slotProps">
<FormActions
v-if="forward.showDefaultActions"
ref="formActionsRef"
:model-value="state.collapsed"
@update:model-value="handleUpdateCollapsed"
>

View File

@ -1,3 +1,4 @@
export { default as MenuBadge } from './components/menu-badge.vue';
export * from './components/normal-menu';
export { default as Menu } from './menu.vue';
export type * from './types';

View File

@ -11,8 +11,8 @@ export const buttonVariants = cva(
size: {
default: 'h-9 px-4 py-2',
icon: 'h-8 w-8 rounded-sm px-1 text-lg',
lg: 'h-10 rounded-md px-8',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-4',
sm: 'h-8 rounded-md px-2 text-xs',
xs: 'h-8 w-8 rounded-sm px-1 text-xs',
},
variant: {

View File

@ -24,7 +24,7 @@ const forwardedProps = useForwardProps(delegatedProps);
v-bind="forwardedProps"
:class="
cn(
'border-input bg-background relative flex h-10 w-10 items-center justify-center border-y border-r text-center text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md focus:relative focus:z-10 focus:outline-none focus:ring-2',
'border-input bg-background relative flex h-10 w-8 items-center justify-center border-y border-r text-center text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md focus:relative focus:z-10 focus:outline-none focus:ring-2 md:w-10',
props.class,
)
"

View File

@ -68,6 +68,7 @@ const { isMobile } = usePreferences();
const slots = useSlots();
const [Form, formApi] = useTableForm({
compact: true,
handleSubmit: async () => {
const formValues = formApi.form.values;
formApi.setLatestSubmissionValues(toRaw(formValues));
@ -284,6 +285,10 @@ watch(
},
);
const isCompactForm = computed(() => {
return formApi.getState()?.compact;
});
onMounted(() => {
props.api?.mount?.(gridRef.value, formApi);
init();
@ -338,7 +343,7 @@ onUnmounted(() => {
<div
v-if="formOptions"
v-show="showSearchForm !== false"
class="relative rounded py-3 pb-4"
:class="cn('relative rounded py-3', isCompactForm ? 'pb-6' : 'pb-4')"
>
<slot name="form">
<Form>

View File

@ -41,6 +41,25 @@ interface AccessState {
*/
export const useAccessStore = defineStore('core-access', {
actions: {
getMenuByPath(path: string) {
function findMenu(
menus: MenuRecordRaw[],
path: string,
): MenuRecordRaw | undefined {
for (const menu of menus) {
if (menu.path === path) {
return menu;
}
if (menu.children) {
const matched = findMenu(menu.children, path);
if (matched) {
return matched;
}
}
}
}
return findMenu(this.accessMenus, path);
},
setAccessCodes(codes: string[]) {
this.accessCodes = codes;
},

View File

@ -30,6 +30,7 @@
},
"dependencies": {
"@tanstack/vue-query": "catalog:",
"@vben-core/menu-ui": "workspace:*",
"@vben/access": "workspace:*",
"@vben/common-ui": "workspace:*",
"@vben/constants": "workspace:*",

View File

@ -4,7 +4,9 @@
"register": "Register",
"codeLogin": "Code Login",
"qrcodeLogin": "Qr Code Login",
"forgetPassword": "Forget Password"
"forgetPassword": "Forget Password",
"sendingCode": "SMS Code is sending...",
"codeSentTo": "Code has been sent to {0}"
},
"dashboard": {
"title": "Dashboard",

View File

@ -4,7 +4,9 @@
"register": "注册",
"codeLogin": "验证码登录",
"qrcodeLogin": "二维码登录",
"forgetPassword": "忘记密码"
"forgetPassword": "忘记密码",
"sendingCode": "正在发送验证码",
"codeSentTo": "验证码已发送至{0}"
},
"dashboard": {
"title": "概览",

View File

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

View File

@ -2,16 +2,36 @@
import type { VbenFormSchema } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import { computed, ref } from 'vue';
import { computed, ref, useTemplateRef } from 'vue';
import { AuthenticationCodeLogin, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { message } from 'ant-design-vue';
defineOptions({ name: 'CodeLogin' });
const loading = ref(false);
const CODE_LENGTH = 6;
const loginRef =
useTemplateRef<InstanceType<typeof AuthenticationCodeLogin>>('loginRef');
function sendCodeApi(phoneNumber: string) {
message.loading({
content: $t('page.auth.sendingCode'),
duration: 0,
key: 'sending-code',
});
return new Promise((resolve) => {
setTimeout(() => {
message.success({
content: $t('page.auth.codeSentTo', [phoneNumber]),
duration: 3,
key: 'sending-code',
});
resolve({ code: '123456', phoneNumber });
}, 3000);
});
}
const formSchema = computed((): VbenFormSchema[] => {
return [
{
@ -39,6 +59,25 @@ const formSchema = computed((): VbenFormSchema[] => {
: $t('authentication.sendCode');
return text;
},
handleSendCode: async () => {
//
// Simulate sending verification code
loading.value = true;
const formApi = loginRef.value?.getFormApi();
if (!formApi) {
loading.value = false;
throw new Error('formApi is not ready');
}
await formApi.validateField('phoneNumber');
const isPhoneReady = await formApi.isFieldValid('phoneNumber');
if (!isPhoneReady) {
loading.value = false;
throw new Error('Phone number is not Ready');
}
const { phoneNumber } = await formApi.getValues();
await sendCodeApi(phoneNumber);
loading.value = false;
},
placeholder: $t('authentication.code'),
},
fieldName: 'code',
@ -62,6 +101,7 @@ async function handleLogin(values: Recordable<any>) {
<template>
<AuthenticationCodeLogin
ref="loginRef"
:form-schema="formSchema"
:loading="loading"
@submit="handleLogin"

View File

@ -1,7 +1,116 @@
<script lang="ts" setup>
import { Fallback } from '@vben/common-ui';
import { reactive } from 'vue';
import { useRoute } from 'vue-router';
import { Page } from '@vben/common-ui';
import { useAccessStore } from '@vben/stores';
import { MenuBadge } from '@vben-core/menu-ui';
import { Button, Card, Radio, RadioGroup } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
const colors = [
{ label: '预设:默认', value: 'default' },
{ label: '预设:关键', value: 'destructive' },
{ label: '预设:主要', value: 'primary' },
{ label: '预设:成功', value: 'success' },
{ label: '自定义', value: 'bg-gray-200 text-black' },
];
const route = useRoute();
const accessStore = useAccessStore();
const menu = accessStore.getMenuByPath(route.path);
const badgeProps = reactive({
badge: menu?.badge as string,
badgeType: menu?.badge ? 'normal' : (menu?.badgeType as 'dot' | 'normal'),
badgeVariants: menu?.badgeVariants as string,
});
const [Form] = useVbenForm({
handleValuesChange(values) {
badgeProps.badge = values.badge;
badgeProps.badgeType = values.badgeType;
badgeProps.badgeVariants = values.badgeVariants;
},
schema: [
{
component: 'RadioGroup',
componentProps: {
buttonStyle: 'solid',
options: [
{ label: '点徽标', value: 'dot' },
{ label: '文字徽标', value: 'normal' },
],
optionType: 'button',
},
defaultValue: badgeProps.badgeType,
fieldName: 'badgeType',
label: '类型',
},
{
component: 'Input',
componentProps: {
maxLength: 4,
placeholder: '请输入徽标内容',
style: { width: '200px' },
},
defaultValue: badgeProps.badge,
fieldName: 'badge',
label: '徽标内容',
},
{
component: 'RadioGroup',
defaultValue: badgeProps.badgeVariants,
fieldName: 'badgeVariants',
label: '颜色',
},
{
component: 'Input',
fieldName: 'action',
},
],
showDefaultActions: false,
});
function updateMenuBadge() {
if (menu) {
menu.badge = badgeProps.badge;
menu.badgeType = badgeProps.badgeType;
menu.badgeVariants = badgeProps.badgeVariants;
}
}
</script>
<template>
<Fallback description="用于徽标示例" status="coming-soon" title="徽标示例" />
<Page
description="菜单项上可以显示徽标,这些徽标可以主动更新"
title="菜单徽标"
>
<Card title="徽标更新">
<Form>
<template #badgeVariants="slotProps">
<RadioGroup v-bind="slotProps">
<Radio
v-for="color in colors"
:key="color.value"
:value="color.value"
>
<div
:title="color.label"
class="flex h-[14px] w-[50px] items-center justify-start"
>
<MenuBadge
v-bind="{ ...badgeProps, badgeVariants: color.value }"
/>
</div>
</Radio>
</RadioGroup>
</template>
<template #action>
<Button type="primary" @click="updateMenuBadge"></Button>
</template>
</Form>
</Card>
</Page>
</template>

View File

@ -7,7 +7,13 @@ import { Button } from 'ant-design-vue';
const props = defineProps<{ path: string }>();
function handleClick() {
openWindow(VBEN_DOC_URL + props.path);
// .html404
const path =
VBEN_DOC_URL +
(props.path.toLowerCase().endsWith('.html')
? props.path
: `${props.path}.html`);
openWindow(path);
}
</script>

View File

@ -5,6 +5,8 @@ import { EllipsisText, Page } from '@vben/common-ui';
import { Card } from 'ant-design-vue';
import DocButton from '../doc-button.vue';
const longText = `Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、多主题配置、按钮级别权限控制等功能。项目会使用前端较新的技术栈可以作为项目的启动模版以帮助你快速搭建企业级中后台产品原型。也可以作为一个示例用于学习 vue3、vite、ts 等主流技术。该项目会持续跟进最新技术并将其应用在项目中。Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、多主题配置、按钮级别权限控制等功能。项目会使用前端较新的技术栈可以作为项目的启动模版以帮助你快速搭建企业级中后台产品原型。也可以作为一个示例用于学习 vue3、vite、ts 等主流技术。该项目会持续跟进最新技术并将其应用在项目中。Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、多主题配置、按钮级别权限控制等功能。项目会使用前端较新的技术栈可以作为项目的启动模版以帮助你快速搭建企业级中后台产品原型。也可以作为一个示例用于学习 vue3、vite、ts 等主流技术。该项目会持续跟进最新技术并将其应用在项目中。Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、多主题配置、按钮级别权限控制等功能。项目会使用前端较新的技术栈可以作为项目的启动模版以帮助你快速搭建企业级中后台产品原型。也可以作为一个示例用于学习 vue3、vite、ts 等主流技术。该项目会持续跟进最新技术,并将其应用在项目中。`;
const text = ref(longText);
@ -15,6 +17,9 @@ const text = ref(longText);
description="用于多行文本省略,支持点击展开和自定义内容。"
title="文本省略组件示例"
>
<template #extra>
<DocButton class="mb-2" path="/components/common-ui/vben-ellipsis-text" />
</template>
<Card class="mb-4" title="基本使用">
<EllipsisText :max-width="500">{{ text }}</EllipsisText>
</Card>

View File

@ -16,6 +16,8 @@ const activeTab = ref('basic');
const [BaseForm, baseFormApi] = useVbenForm({
//
commonConfig: {
// label
colon: true,
//
componentProps: {
class: 'w-full',
@ -40,6 +42,7 @@ const [BaseForm, baseFormApi] = useVbenForm({
fieldName: 'username',
// label
label: '字符串',
rules: 'required',
},
{
// #/adapter.ts
@ -392,7 +395,7 @@ function handleSetFormValue() {
</Tabs>
</template>
<template #extra>
<DocButton path="/components/common-ui/vben-form" />
<DocButton class="mb-2" path="/components/common-ui/vben-form" />
</template>
<Card v-show="activeTab === 'basic'" title="基础示例">
<template #extra>

View File

@ -1804,6 +1804,9 @@ importers:
'@tanstack/vue-query':
specifier: 'catalog:'
version: 5.62.7(vue@3.5.13(typescript@5.7.2))
'@vben-core/menu-ui':
specifier: workspace:*
version: link:../packages/@core/ui-kit/menu-ui
'@vben/access':
specifier: workspace:*
version: link:../packages/effects/access