Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev-v5
commit
5a658d6419
|
@ -4,6 +4,7 @@ export interface UserInfo {
|
||||||
realName: string;
|
realName: string;
|
||||||
roles: string[];
|
roles: string[];
|
||||||
username: string;
|
username: string;
|
||||||
|
homePath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MOCK_USERS: UserInfo[] = [
|
export const MOCK_USERS: UserInfo[] = [
|
||||||
|
@ -20,6 +21,7 @@ export const MOCK_USERS: UserInfo[] = [
|
||||||
realName: 'Admin',
|
realName: 'Admin',
|
||||||
roles: ['admin'],
|
roles: ['admin'],
|
||||||
username: 'admin',
|
username: 'admin',
|
||||||
|
homePath: '/workspace',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
|
@ -27,6 +29,7 @@ export const MOCK_USERS: UserInfo[] = [
|
||||||
realName: 'Jack',
|
realName: 'Jack',
|
||||||
roles: ['user'],
|
roles: ['user'],
|
||||||
username: 'jack',
|
username: 'jack',
|
||||||
|
homePath: '/analytics',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -54,7 +54,9 @@ function setupAccessGuard(router: Router) {
|
||||||
if (coreRouteNames.includes(to.name as string)) {
|
if (coreRouteNames.includes(to.name as string)) {
|
||||||
if (to.path === LOGIN_PATH && accessStore.accessToken) {
|
if (to.path === LOGIN_PATH && accessStore.accessToken) {
|
||||||
return decodeURIComponent(
|
return decodeURIComponent(
|
||||||
(to.query?.redirect as string) || DEFAULT_HOME_PATH,
|
(to.query?.redirect as string) ||
|
||||||
|
userStore.userInfo?.homePath ||
|
||||||
|
DEFAULT_HOME_PATH,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
@ -72,7 +74,10 @@ function setupAccessGuard(router: Router) {
|
||||||
return {
|
return {
|
||||||
path: LOGIN_PATH,
|
path: LOGIN_PATH,
|
||||||
// 如不需要,直接删除 query
|
// 如不需要,直接删除 query
|
||||||
query: { redirect: encodeURIComponent(to.fullPath) },
|
query:
|
||||||
|
to.fullPath === DEFAULT_HOME_PATH
|
||||||
|
? {}
|
||||||
|
: { redirect: encodeURIComponent(to.fullPath) },
|
||||||
// 携带当前跳转的页面,登录后重新跳转该页面
|
// 携带当前跳转的页面,登录后重新跳转该页面
|
||||||
replace: true,
|
replace: true,
|
||||||
};
|
};
|
||||||
|
@ -104,7 +109,10 @@ function setupAccessGuard(router: Router) {
|
||||||
accessStore.setAccessMenus(accessibleMenus);
|
accessStore.setAccessMenus(accessibleMenus);
|
||||||
accessStore.setAccessRoutes(accessibleRoutes);
|
accessStore.setAccessRoutes(accessibleRoutes);
|
||||||
accessStore.setIsAccessChecked(true);
|
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 {
|
return {
|
||||||
...router.resolve(decodeURIComponent(redirectPath)),
|
...router.resolve(decodeURIComponent(redirectPath)),
|
||||||
|
|
|
@ -54,7 +54,9 @@ function setupAccessGuard(router: Router) {
|
||||||
if (coreRouteNames.includes(to.name as string)) {
|
if (coreRouteNames.includes(to.name as string)) {
|
||||||
if (to.path === LOGIN_PATH && accessStore.accessToken) {
|
if (to.path === LOGIN_PATH && accessStore.accessToken) {
|
||||||
return decodeURIComponent(
|
return decodeURIComponent(
|
||||||
(to.query?.redirect as string) || DEFAULT_HOME_PATH,
|
(to.query?.redirect as string) ||
|
||||||
|
userStore.userInfo?.homePath ||
|
||||||
|
DEFAULT_HOME_PATH,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
@ -72,7 +74,10 @@ function setupAccessGuard(router: Router) {
|
||||||
return {
|
return {
|
||||||
path: LOGIN_PATH,
|
path: LOGIN_PATH,
|
||||||
// 如不需要,直接删除 query
|
// 如不需要,直接删除 query
|
||||||
query: { redirect: encodeURIComponent(to.fullPath) },
|
query:
|
||||||
|
to.fullPath === DEFAULT_HOME_PATH
|
||||||
|
? {}
|
||||||
|
: { redirect: encodeURIComponent(to.fullPath) },
|
||||||
// 携带当前跳转的页面,登录后重新跳转该页面
|
// 携带当前跳转的页面,登录后重新跳转该页面
|
||||||
replace: true,
|
replace: true,
|
||||||
};
|
};
|
||||||
|
@ -102,7 +107,10 @@ function setupAccessGuard(router: Router) {
|
||||||
accessStore.setAccessMenus(accessibleMenus);
|
accessStore.setAccessMenus(accessibleMenus);
|
||||||
accessStore.setAccessRoutes(accessibleRoutes);
|
accessStore.setAccessRoutes(accessibleRoutes);
|
||||||
accessStore.setIsAccessChecked(true);
|
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 {
|
return {
|
||||||
...router.resolve(decodeURIComponent(redirectPath)),
|
...router.resolve(decodeURIComponent(redirectPath)),
|
||||||
|
|
|
@ -54,7 +54,9 @@ function setupAccessGuard(router: Router) {
|
||||||
if (coreRouteNames.includes(to.name as string)) {
|
if (coreRouteNames.includes(to.name as string)) {
|
||||||
if (to.path === LOGIN_PATH && accessStore.accessToken) {
|
if (to.path === LOGIN_PATH && accessStore.accessToken) {
|
||||||
return decodeURIComponent(
|
return decodeURIComponent(
|
||||||
(to.query?.redirect as string) || DEFAULT_HOME_PATH,
|
(to.query?.redirect as string) ||
|
||||||
|
userStore.userInfo?.homePath ||
|
||||||
|
DEFAULT_HOME_PATH,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
@ -72,7 +74,10 @@ function setupAccessGuard(router: Router) {
|
||||||
return {
|
return {
|
||||||
path: LOGIN_PATH,
|
path: LOGIN_PATH,
|
||||||
// 如不需要,直接删除 query
|
// 如不需要,直接删除 query
|
||||||
query: { redirect: encodeURIComponent(to.fullPath) },
|
query:
|
||||||
|
to.fullPath === DEFAULT_HOME_PATH
|
||||||
|
? {}
|
||||||
|
: { redirect: encodeURIComponent(to.fullPath) },
|
||||||
// 携带当前跳转的页面,登录后重新跳转该页面
|
// 携带当前跳转的页面,登录后重新跳转该页面
|
||||||
replace: true,
|
replace: true,
|
||||||
};
|
};
|
||||||
|
@ -101,7 +106,10 @@ function setupAccessGuard(router: Router) {
|
||||||
accessStore.setAccessMenus(accessibleMenus);
|
accessStore.setAccessMenus(accessibleMenus);
|
||||||
accessStore.setAccessRoutes(accessibleRoutes);
|
accessStore.setAccessRoutes(accessibleRoutes);
|
||||||
accessStore.setIsAccessChecked(true);
|
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 {
|
return {
|
||||||
...router.resolve(decodeURIComponent(redirectPath)),
|
...router.resolve(decodeURIComponent(redirectPath)),
|
||||||
|
|
|
@ -186,6 +186,10 @@ function sidebarComponents(): DefaultTheme.SidebarItem[] {
|
||||||
link: 'common-ui/vben-count-to-animator',
|
link: 'common-ui/vben-count-to-animator',
|
||||||
text: 'CountToAnimator 数字动画',
|
text: 'CountToAnimator 数字动画',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
link: 'common-ui/vben-ellipsis-text',
|
||||||
|
text: 'EllipsisText 省略文本',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -123,6 +123,8 @@ function fetchApi(): Promise<Record<string, any>> {
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
### Props
|
### Props
|
||||||
|
|
||||||
| 属性名 | 描述 | 类型 | 默认值 |
|
| 属性名 | 描述 | 类型 | 默认值 |
|
||||||
|
|
|
@ -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 | 启用文本提示时,用来定制提示内容 |
|
|
@ -287,6 +287,8 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
|
||||||
| setValues | 设置表单值, 默认会过滤不在schema中定义的field, 可通过filterFields形参关闭过滤 | `(fields: Record<string, any>, filterFields?: boolean, shouldValidate?: boolean) => Promise<void>` |
|
| setValues | 设置表单值, 默认会过滤不在schema中定义的field, 可通过filterFields形参关闭过滤 | `(fields: Record<string, any>, filterFields?: boolean, shouldValidate?: boolean) => Promise<void>` |
|
||||||
| getValues | 获取表单值 | `(fields:Record<string, any>,shouldValidate: boolean = false)=>Promise<void>` |
|
| getValues | 获取表单值 | `(fields:Record<string, any>,shouldValidate: boolean = false)=>Promise<void>` |
|
||||||
| validate | 表单校验 | `()=>Promise<void>` |
|
| validate | 表单校验 | `()=>Promise<void>` |
|
||||||
|
| validateField | 校验指定字段 | `(fieldName: string)=>Promise<ValidationResult<unknown>>` |
|
||||||
|
| isFieldValid | 检查某个字段是否已通过校验 | `(fieldName: string)=>Promise<boolean>` |
|
||||||
| resetValidate | 重置表单校验 | `()=>Promise<void>` |
|
| resetValidate | 重置表单校验 | `()=>Promise<void>` |
|
||||||
| updateSchema | 更新formSchema | `(schema:FormSchema[])=>void` |
|
| updateSchema | 更新formSchema | `(schema:FormSchema[])=>void` |
|
||||||
| setFieldValue | 设置字段值 | `(field: string, value: any, shouldValidate?: boolean)=>Promise<void>` |
|
| setFieldValue | 设置字段值 | `(field: string, value: any, shouldValidate?: boolean)=>Promise<void>` |
|
||||||
|
@ -311,14 +313,14 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
|
||||||
| resetButtonOptions | 重置按钮组件参数 | `ActionButtonOptions` | - |
|
| resetButtonOptions | 重置按钮组件参数 | `ActionButtonOptions` | - |
|
||||||
| submitButtonOptions | 提交按钮组件参数 | `ActionButtonOptions` | - |
|
| submitButtonOptions | 提交按钮组件参数 | `ActionButtonOptions` | - |
|
||||||
| showDefaultActions | 是否显示默认操作按钮 | `boolean` | `true` |
|
| showDefaultActions | 是否显示默认操作按钮 | `boolean` | `true` |
|
||||||
| collapsed | 是否折叠,在`是否展开,在showCollapseButton=true`时生效 | `boolean` | `false` |
|
| collapsed | 是否折叠,在`showCollapseButton`为`true`时生效 | `boolean` | `false` |
|
||||||
| collapseTriggerResize | 折叠时,触发`resize`事件 | `boolean` | `false` |
|
| collapseTriggerResize | 折叠时,触发`resize`事件 | `boolean` | `false` |
|
||||||
| collapsedRows | 折叠时保持的行数 | `number` | `1` |
|
| collapsedRows | 折叠时保持的行数 | `number` | `1` |
|
||||||
| fieldMappingTime | 用于将表单内时间区域的应设成 2 个字段 | `[string, [string, string], string?][]` | - |
|
| fieldMappingTime | 用于将表单内时间区域组件的数组值映射成 2 个字段 | `[string, [string, string], string?][]` | - |
|
||||||
| commonConfig | 表单项的通用配置,每个配置都会传递到每个表单项,表单项可覆盖 | `FormCommonConfig` | - |
|
| commonConfig | 表单项的通用配置,每个配置都会传递到每个表单项,表单项可覆盖 | `FormCommonConfig` | - |
|
||||||
| schema | 表单项的每一项配置 | `FormSchema` | - |
|
| schema | 表单项的每一项配置 | `FormSchema[]` | - |
|
||||||
| submitOnEnter | 按下回车健时提交表单 | `boolean` | false |
|
| submitOnEnter | 按下回车健时提交表单 | `boolean` | false |
|
||||||
| submitOnChange | 字段值改变时提交表单 | `boolean` | false |
|
| submitOnChange | 字段值改变时提交表单(内部防抖,这个属性一般用于表格的搜索表单) | `boolean` | false |
|
||||||
|
|
||||||
### TS 类型说明
|
### TS 类型说明
|
||||||
|
|
||||||
|
@ -355,10 +357,21 @@ export interface FormCommonConfig {
|
||||||
* 所有表单项的props
|
* 所有表单项的props
|
||||||
*/
|
*/
|
||||||
componentProps?: ComponentProps;
|
componentProps?: ComponentProps;
|
||||||
|
/**
|
||||||
|
* 是否紧凑模式(移除表单底部为显示校验错误信息所预留的空间)。
|
||||||
|
* 在有设置校验规则的场景下,建议不要将其设置为true
|
||||||
|
* 默认为false。但用作表格的搜索表单时,默认为true
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
compact?: boolean;
|
||||||
/**
|
/**
|
||||||
* 所有表单项的控件样式
|
* 所有表单项的控件样式
|
||||||
*/
|
*/
|
||||||
controlClass?: string;
|
controlClass?: string;
|
||||||
|
/**
|
||||||
|
* 在表单项的Label后显示一个冒号
|
||||||
|
*/
|
||||||
|
colon?: boolean;
|
||||||
/**
|
/**
|
||||||
* 所有表单项的禁用状态
|
* 所有表单项的禁用状态
|
||||||
* @default false
|
* @default false
|
||||||
|
@ -418,7 +431,7 @@ export interface FormSchema<
|
||||||
dependencies?: FormItemDependencies;
|
dependencies?: FormItemDependencies;
|
||||||
/** 描述 */
|
/** 描述 */
|
||||||
description?: string;
|
description?: string;
|
||||||
/** 字段名 */
|
/** 字段名,也作为自定义插槽的名称 */
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
/** 帮助信息 */
|
/** 帮助信息 */
|
||||||
help?: string;
|
help?: string;
|
||||||
|
@ -441,7 +454,7 @@ export interface FormSchema<
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
dependencies: {
|
dependencies: {
|
||||||
// 只有当 name 字段的值变化时,才会触发联动
|
// 触发字段。只有这些字段值变动时,联动才会触发
|
||||||
triggerFields: ['name'],
|
triggerFields: ['name'],
|
||||||
// 动态判断当前字段是否需要显示,不显示则直接销毁
|
// 动态判断当前字段是否需要显示,不显示则直接销毁
|
||||||
if(values,formApi){},
|
if(values,formApi){},
|
||||||
|
@ -462,11 +475,11 @@ dependencies: {
|
||||||
|
|
||||||
### 表单校验
|
### 表单校验
|
||||||
|
|
||||||
表单联动需要通过 schema 内的 `rules` 属性进行配置。
|
表单校验需要通过 schema 内的 `rules` 属性进行配置。
|
||||||
|
|
||||||
rules的值可以是一个字符串,也可以是一个zod的schema。
|
rules的值可以是字符串(预定义的校验规则名称),也可以是一个zod的schema。
|
||||||
|
|
||||||
#### 字符串
|
#### 预定义的校验规则
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
// 表示字段必填,默认会根据适配器的required进行国际化
|
// 表示字段必填,默认会根据适配器的required进行国际化
|
||||||
|
@ -492,11 +505,16 @@ import { z } from '#/adapter/form';
|
||||||
rules: z.string().min(1, { message: '请输入字符串' });
|
rules: z.string().min(1, { message: '请输入字符串' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 可选,并且携带默认值
|
// 可选(可以是undefined),并且携带默认值。注意zod的optional不包括空字符串''
|
||||||
{
|
{
|
||||||
rules: z.string().default('默认值').optional(),
|
rules: z.string().default('默认值').optional(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 可以是空字符串、undefined或者一个邮箱地址
|
||||||
|
{
|
||||||
|
rules: z.union(z.string().email().optional(), z.literal(""))
|
||||||
|
}
|
||||||
|
|
||||||
// 复杂校验
|
// 复杂校验
|
||||||
{
|
{
|
||||||
z.string().min(1, { message: "请输入" })
|
z.string().min(1, { message: "请输入" })
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { EllipsisText } from '@vben/common-ui';
|
||||||
|
|
||||||
|
const text = `
|
||||||
|
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 等主流技术。该项目会持续跟进最新技术,并将其应用在项目中。
|
||||||
|
`;
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<EllipsisText :line="3" expand>{{ text }}</EllipsisText>
|
||||||
|
</template>
|
|
@ -0,0 +1,10 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { EllipsisText } from '@vben/common-ui';
|
||||||
|
|
||||||
|
const text = `
|
||||||
|
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 等主流技术。该项目会持续跟进最新技术,并将其应用在项目中。
|
||||||
|
`;
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<EllipsisText :max-width="500">{{ text }}</EllipsisText>
|
||||||
|
</template>
|
|
@ -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>
|
|
@ -138,7 +138,11 @@ defineExpose({
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
:class="
|
: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"
|
:style="queryFormStyle"
|
||||||
>
|
>
|
||||||
|
|
|
@ -130,6 +130,11 @@ export class FormApi {
|
||||||
return form.values;
|
return form.values;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async isFieldValid(fieldName: string) {
|
||||||
|
const form = await this.getForm();
|
||||||
|
return form.isFieldValid(fieldName);
|
||||||
|
}
|
||||||
|
|
||||||
merge(formApi: FormApi) {
|
merge(formApi: FormApi) {
|
||||||
const chain = [this, formApi];
|
const chain = [this, formApi];
|
||||||
const proxy = new Proxy(formApi, {
|
const proxy = new Proxy(formApi, {
|
||||||
|
@ -348,4 +353,14 @@ export class FormApi {
|
||||||
}
|
}
|
||||||
return await this.submitForm();
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ import { isEventObjectLike } from './helper';
|
||||||
interface Props extends FormSchema {}
|
interface Props extends FormSchema {}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
colon,
|
||||||
commonComponentProps,
|
commonComponentProps,
|
||||||
component,
|
component,
|
||||||
componentEvents,
|
componentEvents,
|
||||||
|
@ -55,7 +56,7 @@ const values = useFormValues();
|
||||||
const errors = useFieldError(fieldName);
|
const errors = useFieldError(fieldName);
|
||||||
const fieldComponentRef = useTemplateRef<HTMLInputElement>('fieldComponentRef');
|
const fieldComponentRef = useTemplateRef<HTMLInputElement>('fieldComponentRef');
|
||||||
const formApi = formRenderProps.form;
|
const formApi = formRenderProps.form;
|
||||||
|
const compact = formRenderProps.compact;
|
||||||
const isInValid = computed(() => errors.value?.length > 0);
|
const isInValid = computed(() => errors.value?.length > 0);
|
||||||
|
|
||||||
const FieldComponent = computed(() => {
|
const FieldComponent = computed(() => {
|
||||||
|
@ -289,8 +290,10 @@ function autofocus() {
|
||||||
'form-valid-error': isInValid,
|
'form-valid-error': isInValid,
|
||||||
'flex-col': isVertical,
|
'flex-col': isVertical,
|
||||||
'flex-row items-center': !isVertical,
|
'flex-row items-center': !isVertical,
|
||||||
|
'pb-6': !compact,
|
||||||
|
'pb-2': compact,
|
||||||
}"
|
}"
|
||||||
class="flex pb-6"
|
class="flex"
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
>
|
>
|
||||||
<FormLabel
|
<FormLabel
|
||||||
|
@ -309,7 +312,10 @@ function autofocus() {
|
||||||
:required="shouldRequired && !hideRequiredMark"
|
:required="shouldRequired && !hideRequiredMark"
|
||||||
:style="labelStyle"
|
:style="labelStyle"
|
||||||
>
|
>
|
||||||
{{ label }}
|
<template v-if="label">
|
||||||
|
<span>{{ label }}</span>
|
||||||
|
<span v-if="colon" class="ml-[2px]">:</span>
|
||||||
|
</template>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<div :class="cn('relative flex w-full items-center', wrapperClass)">
|
<div :class="cn('relative flex w-full items-center', wrapperClass)">
|
||||||
<FormControl :class="cn(controlClass)">
|
<FormControl :class="cn(controlClass)">
|
||||||
|
|
|
@ -86,6 +86,7 @@ const computedSchema = computed(
|
||||||
formFieldProps: Record<string, any>;
|
formFieldProps: Record<string, any>;
|
||||||
} & Omit<FormSchema, 'formFieldProps'>)[] => {
|
} & Omit<FormSchema, 'formFieldProps'>)[] => {
|
||||||
const {
|
const {
|
||||||
|
colon = false,
|
||||||
componentProps = {},
|
componentProps = {},
|
||||||
controlClass = '',
|
controlClass = '',
|
||||||
disabled,
|
disabled,
|
||||||
|
@ -110,6 +111,7 @@ const computedSchema = computed(
|
||||||
: false;
|
: false;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
colon,
|
||||||
disabled,
|
disabled,
|
||||||
disabledOnChangeListener,
|
disabledOnChangeListener,
|
||||||
disabledOnInputListener,
|
disabledOnInputListener,
|
||||||
|
|
|
@ -145,6 +145,10 @@ type ComponentProps =
|
||||||
| MaybeComponentProps;
|
| MaybeComponentProps;
|
||||||
|
|
||||||
export interface FormCommonConfig {
|
export interface FormCommonConfig {
|
||||||
|
/**
|
||||||
|
* 在Label后显示一个冒号
|
||||||
|
*/
|
||||||
|
colon?: boolean;
|
||||||
/**
|
/**
|
||||||
* 所有表单项的props
|
* 所有表单项的props
|
||||||
*/
|
*/
|
||||||
|
@ -280,6 +284,10 @@ export interface FormRenderProps<
|
||||||
* 表单项通用后备配置,当子项目没配置时使用这里的配置,子项目配置优先级高于此配置
|
* 表单项通用后备配置,当子项目没配置时使用这里的配置,子项目配置优先级高于此配置
|
||||||
*/
|
*/
|
||||||
commonConfig?: FormCommonConfig;
|
commonConfig?: FormCommonConfig;
|
||||||
|
/**
|
||||||
|
* 紧凑模式(移除表单每一项底部为校验信息预留的空间)
|
||||||
|
*/
|
||||||
|
compact?: boolean;
|
||||||
/**
|
/**
|
||||||
* 组件v-model事件绑定
|
* 组件v-model事件绑定
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -6,7 +6,7 @@ import type { ExtendedFormApi, VbenFormProps } from './types';
|
||||||
import { useForwardPriorityValues } from '@vben-core/composables';
|
import { useForwardPriorityValues } from '@vben-core/composables';
|
||||||
// import { isFunction } from '@vben-core/shared/utils';
|
// 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';
|
import { cloneDeep } from '@vben-core/shared/utils';
|
||||||
|
|
||||||
|
@ -27,8 +27,6 @@ interface Props extends VbenFormProps {
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
const formActionsRef = useTemplateRef<typeof FormActions>('formActionsRef');
|
|
||||||
|
|
||||||
const state = props.formApi?.useStore?.();
|
const state = props.formApi?.useStore?.();
|
||||||
|
|
||||||
const forward = useForwardPriorityValues(props, state);
|
const forward = useForwardPriorityValues(props, state);
|
||||||
|
@ -44,11 +42,7 @@ const handleUpdateCollapsed = (value: boolean) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
function handleKeyDownEnter(event: KeyboardEvent) {
|
function handleKeyDownEnter(event: KeyboardEvent) {
|
||||||
if (
|
if (!state.value.submitOnEnter || !forward.value.formApi?.isMounted) {
|
||||||
!state.value.submitOnEnter ||
|
|
||||||
!formActionsRef.value ||
|
|
||||||
!formActionsRef.value.handleSubmit
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 如果是 textarea 不阻止默认行为,否则会导致无法换行。
|
// 如果是 textarea 不阻止默认行为,否则会导致无法换行。
|
||||||
|
@ -58,12 +52,12 @@ function handleKeyDownEnter(event: KeyboardEvent) {
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
formActionsRef.value?.handleSubmit?.();
|
forward.value.formApi.validateAndSubmitForm();
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleValuesChangeDebounced = useDebounceFn((newVal) => {
|
const handleValuesChangeDebounced = useDebounceFn((newVal) => {
|
||||||
forward.value.handleValuesChange?.(cloneDeep(newVal));
|
forward.value.handleValuesChange?.(cloneDeep(newVal));
|
||||||
state.value.submitOnChange && formActionsRef.value?.handleSubmit?.();
|
state.value.submitOnChange && forward.value.formApi?.validateAndSubmitForm();
|
||||||
}, 300);
|
}, 300);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
@ -94,7 +88,6 @@ onMounted(async () => {
|
||||||
<slot v-bind="slotProps">
|
<slot v-bind="slotProps">
|
||||||
<FormActions
|
<FormActions
|
||||||
v-if="forward.showDefaultActions"
|
v-if="forward.showDefaultActions"
|
||||||
ref="formActionsRef"
|
|
||||||
:model-value="state.collapsed"
|
:model-value="state.collapsed"
|
||||||
@update:model-value="handleUpdateCollapsed"
|
@update:model-value="handleUpdateCollapsed"
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
export { default as MenuBadge } from './components/menu-badge.vue';
|
||||||
export * from './components/normal-menu';
|
export * from './components/normal-menu';
|
||||||
export { default as Menu } from './menu.vue';
|
export { default as Menu } from './menu.vue';
|
||||||
export type * from './types';
|
export type * from './types';
|
||||||
|
|
|
@ -11,8 +11,8 @@ export const buttonVariants = cva(
|
||||||
size: {
|
size: {
|
||||||
default: 'h-9 px-4 py-2',
|
default: 'h-9 px-4 py-2',
|
||||||
icon: 'h-8 w-8 rounded-sm px-1 text-lg',
|
icon: 'h-8 w-8 rounded-sm px-1 text-lg',
|
||||||
lg: 'h-10 rounded-md px-8',
|
lg: 'h-10 rounded-md px-4',
|
||||||
sm: 'h-8 rounded-md px-3 text-xs',
|
sm: 'h-8 rounded-md px-2 text-xs',
|
||||||
xs: 'h-8 w-8 rounded-sm px-1 text-xs',
|
xs: 'h-8 w-8 rounded-sm px-1 text-xs',
|
||||||
},
|
},
|
||||||
variant: {
|
variant: {
|
||||||
|
|
|
@ -24,7 +24,7 @@ const forwardedProps = useForwardProps(delegatedProps);
|
||||||
v-bind="forwardedProps"
|
v-bind="forwardedProps"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
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,
|
props.class,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
|
|
@ -68,6 +68,7 @@ const { isMobile } = usePreferences();
|
||||||
const slots = useSlots();
|
const slots = useSlots();
|
||||||
|
|
||||||
const [Form, formApi] = useTableForm({
|
const [Form, formApi] = useTableForm({
|
||||||
|
compact: true,
|
||||||
handleSubmit: async () => {
|
handleSubmit: async () => {
|
||||||
const formValues = formApi.form.values;
|
const formValues = formApi.form.values;
|
||||||
formApi.setLatestSubmissionValues(toRaw(formValues));
|
formApi.setLatestSubmissionValues(toRaw(formValues));
|
||||||
|
@ -284,6 +285,10 @@ watch(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isCompactForm = computed(() => {
|
||||||
|
return formApi.getState()?.compact;
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
props.api?.mount?.(gridRef.value, formApi);
|
props.api?.mount?.(gridRef.value, formApi);
|
||||||
init();
|
init();
|
||||||
|
@ -338,7 +343,7 @@ onUnmounted(() => {
|
||||||
<div
|
<div
|
||||||
v-if="formOptions"
|
v-if="formOptions"
|
||||||
v-show="showSearchForm !== false"
|
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">
|
<slot name="form">
|
||||||
<Form>
|
<Form>
|
||||||
|
|
|
@ -41,6 +41,25 @@ interface AccessState {
|
||||||
*/
|
*/
|
||||||
export const useAccessStore = defineStore('core-access', {
|
export const useAccessStore = defineStore('core-access', {
|
||||||
actions: {
|
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[]) {
|
setAccessCodes(codes: string[]) {
|
||||||
this.accessCodes = codes;
|
this.accessCodes = codes;
|
||||||
},
|
},
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/vue-query": "catalog:",
|
"@tanstack/vue-query": "catalog:",
|
||||||
|
"@vben-core/menu-ui": "workspace:*",
|
||||||
"@vben/access": "workspace:*",
|
"@vben/access": "workspace:*",
|
||||||
"@vben/common-ui": "workspace:*",
|
"@vben/common-ui": "workspace:*",
|
||||||
"@vben/constants": "workspace:*",
|
"@vben/constants": "workspace:*",
|
||||||
|
|
|
@ -4,7 +4,9 @@
|
||||||
"register": "Register",
|
"register": "Register",
|
||||||
"codeLogin": "Code Login",
|
"codeLogin": "Code Login",
|
||||||
"qrcodeLogin": "Qr 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": {
|
"dashboard": {
|
||||||
"title": "Dashboard",
|
"title": "Dashboard",
|
||||||
|
|
|
@ -4,7 +4,9 @@
|
||||||
"register": "注册",
|
"register": "注册",
|
||||||
"codeLogin": "验证码登录",
|
"codeLogin": "验证码登录",
|
||||||
"qrcodeLogin": "二维码登录",
|
"qrcodeLogin": "二维码登录",
|
||||||
"forgetPassword": "忘记密码"
|
"forgetPassword": "忘记密码",
|
||||||
|
"sendingCode": "正在发送验证码",
|
||||||
|
"codeSentTo": "验证码已发送至{0}"
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"title": "概览",
|
"title": "概览",
|
||||||
|
|
|
@ -52,7 +52,9 @@ function setupAccessGuard(router: Router) {
|
||||||
if (coreRouteNames.includes(to.name as string)) {
|
if (coreRouteNames.includes(to.name as string)) {
|
||||||
if (to.path === LOGIN_PATH && accessStore.accessToken) {
|
if (to.path === LOGIN_PATH && accessStore.accessToken) {
|
||||||
return decodeURIComponent(
|
return decodeURIComponent(
|
||||||
(to.query?.redirect as string) || DEFAULT_HOME_PATH,
|
(to.query?.redirect as string) ||
|
||||||
|
userStore.userInfo?.homePath ||
|
||||||
|
DEFAULT_HOME_PATH,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
@ -70,7 +72,10 @@ function setupAccessGuard(router: Router) {
|
||||||
return {
|
return {
|
||||||
path: LOGIN_PATH,
|
path: LOGIN_PATH,
|
||||||
// 如不需要,直接删除 query
|
// 如不需要,直接删除 query
|
||||||
query: { redirect: encodeURIComponent(to.fullPath) },
|
query:
|
||||||
|
to.fullPath === DEFAULT_HOME_PATH
|
||||||
|
? {}
|
||||||
|
: { redirect: encodeURIComponent(to.fullPath) },
|
||||||
// 携带当前跳转的页面,登录后重新跳转该页面
|
// 携带当前跳转的页面,登录后重新跳转该页面
|
||||||
replace: true,
|
replace: true,
|
||||||
};
|
};
|
||||||
|
@ -100,7 +105,10 @@ function setupAccessGuard(router: Router) {
|
||||||
accessStore.setAccessMenus(accessibleMenus);
|
accessStore.setAccessMenus(accessibleMenus);
|
||||||
accessStore.setAccessRoutes(accessibleRoutes);
|
accessStore.setAccessRoutes(accessibleRoutes);
|
||||||
accessStore.setIsAccessChecked(true);
|
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 {
|
return {
|
||||||
...router.resolve(decodeURIComponent(redirectPath)),
|
...router.resolve(decodeURIComponent(redirectPath)),
|
||||||
|
|
|
@ -2,16 +2,36 @@
|
||||||
import type { VbenFormSchema } from '@vben/common-ui';
|
import type { VbenFormSchema } from '@vben/common-ui';
|
||||||
import type { Recordable } from '@vben/types';
|
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 { AuthenticationCodeLogin, z } from '@vben/common-ui';
|
||||||
import { $t } from '@vben/locales';
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
defineOptions({ name: 'CodeLogin' });
|
defineOptions({ name: 'CodeLogin' });
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const CODE_LENGTH = 6;
|
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[] => {
|
const formSchema = computed((): VbenFormSchema[] => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
@ -39,6 +59,25 @@ const formSchema = computed((): VbenFormSchema[] => {
|
||||||
: $t('authentication.sendCode');
|
: $t('authentication.sendCode');
|
||||||
return text;
|
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'),
|
placeholder: $t('authentication.code'),
|
||||||
},
|
},
|
||||||
fieldName: 'code',
|
fieldName: 'code',
|
||||||
|
@ -62,6 +101,7 @@ async function handleLogin(values: Recordable<any>) {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AuthenticationCodeLogin
|
<AuthenticationCodeLogin
|
||||||
|
ref="loginRef"
|
||||||
:form-schema="formSchema"
|
:form-schema="formSchema"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
@submit="handleLogin"
|
@submit="handleLogin"
|
||||||
|
|
|
@ -1,7 +1,116 @@
|
||||||
<script lang="ts" setup>
|
<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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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>
|
</template>
|
||||||
|
|
|
@ -7,7 +7,13 @@ import { Button } from 'ant-design-vue';
|
||||||
const props = defineProps<{ path: string }>();
|
const props = defineProps<{ path: string }>();
|
||||||
|
|
||||||
function handleClick() {
|
function handleClick() {
|
||||||
openWindow(VBEN_DOC_URL + props.path);
|
// 如果没有.html,打开页面时可能会出现404
|
||||||
|
const path =
|
||||||
|
VBEN_DOC_URL +
|
||||||
|
(props.path.toLowerCase().endsWith('.html')
|
||||||
|
? props.path
|
||||||
|
: `${props.path}.html`);
|
||||||
|
openWindow(path);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,8 @@ import { EllipsisText, Page } from '@vben/common-ui';
|
||||||
|
|
||||||
import { Card } from 'ant-design-vue';
|
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 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);
|
const text = ref(longText);
|
||||||
|
@ -15,6 +17,9 @@ const text = ref(longText);
|
||||||
description="用于多行文本省略,支持点击展开和自定义内容。"
|
description="用于多行文本省略,支持点击展开和自定义内容。"
|
||||||
title="文本省略组件示例"
|
title="文本省略组件示例"
|
||||||
>
|
>
|
||||||
|
<template #extra>
|
||||||
|
<DocButton class="mb-2" path="/components/common-ui/vben-ellipsis-text" />
|
||||||
|
</template>
|
||||||
<Card class="mb-4" title="基本使用">
|
<Card class="mb-4" title="基本使用">
|
||||||
<EllipsisText :max-width="500">{{ text }}</EllipsisText>
|
<EllipsisText :max-width="500">{{ text }}</EllipsisText>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
@ -16,6 +16,8 @@ const activeTab = ref('basic');
|
||||||
const [BaseForm, baseFormApi] = useVbenForm({
|
const [BaseForm, baseFormApi] = useVbenForm({
|
||||||
// 所有表单项共用,可单独在表单内覆盖
|
// 所有表单项共用,可单独在表单内覆盖
|
||||||
commonConfig: {
|
commonConfig: {
|
||||||
|
// 在label后显示一个冒号
|
||||||
|
colon: true,
|
||||||
// 所有表单项
|
// 所有表单项
|
||||||
componentProps: {
|
componentProps: {
|
||||||
class: 'w-full',
|
class: 'w-full',
|
||||||
|
@ -40,6 +42,7 @@ const [BaseForm, baseFormApi] = useVbenForm({
|
||||||
fieldName: 'username',
|
fieldName: 'username',
|
||||||
// 界面显示的label
|
// 界面显示的label
|
||||||
label: '字符串',
|
label: '字符串',
|
||||||
|
rules: 'required',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// 组件需要在 #/adapter.ts内注册,并加上类型
|
// 组件需要在 #/adapter.ts内注册,并加上类型
|
||||||
|
@ -392,7 +395,7 @@ function handleSetFormValue() {
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</template>
|
</template>
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<DocButton path="/components/common-ui/vben-form" />
|
<DocButton class="mb-2" path="/components/common-ui/vben-form" />
|
||||||
</template>
|
</template>
|
||||||
<Card v-show="activeTab === 'basic'" title="基础示例">
|
<Card v-show="activeTab === 'basic'" title="基础示例">
|
||||||
<template #extra>
|
<template #extra>
|
||||||
|
|
|
@ -1804,6 +1804,9 @@ importers:
|
||||||
'@tanstack/vue-query':
|
'@tanstack/vue-query':
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 5.62.7(vue@3.5.13(typescript@5.7.2))
|
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':
|
'@vben/access':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../packages/effects/access
|
version: link:../packages/effects/access
|
||||||
|
|
Loading…
Reference in New Issue