Compare commits
94 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f2692d4a85 | ||
![]() |
63db6ee4d7 | ||
![]() |
95d8522b9f | ||
![]() |
56d77021c4 | ||
![]() |
f4a1cc72d9 | ||
![]() |
d8d884ecc1 | ||
![]() |
a70ae0f603 | ||
![]() |
41b69b5f93 | ||
![]() |
c8e01a70b6 | ||
![]() |
4b98a143c7 | ||
![]() |
593ed62585 | ||
![]() |
afdf186fa4 | ||
![]() |
40448b9365 | ||
![]() |
f85badf482 | ||
![]() |
12f25cf3a2 | ||
![]() |
90c7bf625a | ||
![]() |
af622d852f | ||
![]() |
046476f8bd | ||
![]() |
34a11ce64d | ||
![]() |
24e889adbe | ||
![]() |
d214da3303 | ||
![]() |
da0ddfc8c7 | ||
![]() |
eb782a9080 | ||
![]() |
c8dd9bbf0b | ||
![]() |
3587ec54eb | ||
![]() |
733f43bc47 | ||
![]() |
0428b5447e | ||
![]() |
28f8ee440d | ||
![]() |
53e1a6fdf7 | ||
![]() |
012808a1da | ||
![]() |
f32a949482 | ||
![]() |
883e6c5033 | ||
![]() |
41093a39b0 | ||
![]() |
ab988cf82e | ||
![]() |
40bb93afcd | ||
![]() |
dbcb7138f2 | ||
![]() |
fe58af2e78 | ||
![]() |
94c68c966e | ||
![]() |
77083abcc5 | ||
![]() |
1302092798 | ||
![]() |
ec53bf8084 | ||
![]() |
5fa31aa97e | ||
![]() |
924eeca280 | ||
![]() |
d6c0ed6429 | ||
![]() |
b87d41bada | ||
![]() |
2f37942961 | ||
![]() |
acd305bd9d | ||
![]() |
788a29a8cb | ||
![]() |
3bd5ef4523 | ||
![]() |
9e8e92a8e0 | ||
![]() |
979bf1b431 | ||
![]() |
1e74e7c0fb | ||
![]() |
240573e354 | ||
![]() |
36c6a65d22 | ||
![]() |
a814bed79e | ||
![]() |
86e52ce58a | ||
![]() |
9ddaba5333 | ||
![]() |
5b079471b9 | ||
![]() |
8cc73cf59c | ||
![]() |
fc28f4ec7e | ||
![]() |
995e9d6fdc | ||
![]() |
a89711610d | ||
![]() |
67c2b13713 | ||
![]() |
1ff1e4a8d7 | ||
![]() |
ea8af98324 | ||
![]() |
43a0ce6d76 | ||
![]() |
3ba0e169b2 | ||
![]() |
dc15accd04 | ||
![]() |
12d53db740 | ||
![]() |
10b6fd26dc | ||
![]() |
06e56338d8 | ||
![]() |
16622e62de | ||
![]() |
75779da35c | ||
![]() |
94efcec7da | ||
![]() |
a3d0d2ed34 | ||
![]() |
45024f5dcb | ||
![]() |
90dc00b168 | ||
![]() |
ba36ce8836 | ||
![]() |
db5c1487cb | ||
![]() |
57d5a919d2 | ||
![]() |
546c0928fb | ||
![]() |
4c487fe85f | ||
![]() |
5e44aa9283 | ||
![]() |
c6b3ca9815 | ||
![]() |
d69618e491 | ||
![]() |
26bec4222f | ||
![]() |
4005023fd4 | ||
![]() |
8617d3dd1e | ||
![]() |
632081e828 | ||
![]() |
6b9acf09dc | ||
![]() |
2c6edafeb2 | ||
![]() |
9cf0573921 | ||
![]() |
da7d61b160 | ||
![]() |
f11523953f |
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -227,5 +227,6 @@
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"editor.linkedEditing": true, // 自动同步更改html标签,
|
||||
"vscodeCustomCodeColor.highlightValue": "v-access", // v-access显示的颜色
|
||||
"vscodeCustomCodeColor.highlightValueColor": "#CCFFFF"
|
||||
"vscodeCustomCodeColor.highlightValueColor": "#CCFFFF",
|
||||
"oxc.enable": false
|
||||
}
|
||||
|
32
CHANGELOG.md
32
CHANGELOG.md
@@ -1,3 +1,35 @@
|
||||
# 1.1.2
|
||||
|
||||
**Features**
|
||||
|
||||
- Options转Enum工具函数
|
||||
|
||||
**OTHERS**
|
||||
|
||||
- 菜单管理 改为虚拟滚动
|
||||
- 移除requestClient的一些冗余参数
|
||||
- 主动退出登录(右上角个人选项)不需要带跳转地址
|
||||
|
||||
**BUG FIXES**
|
||||
|
||||
- 语言 漏加Content-Language请求头
|
||||
- 用户管理/岗位管理 左边部门树错误emit导致会调用两次列表api
|
||||
|
||||
# 1.1.1
|
||||
|
||||
**REFACTOR**
|
||||
|
||||
- 使用VxeTable重构OAuth账号绑定列表(替代antdv的Table)
|
||||
- commonDownloadExcel方法 支持处理区间选择器字段导出excel
|
||||
|
||||
**BUG FIXES**
|
||||
|
||||
- 修复在Modal/Drawer中使用VxeTable时, 第二次打开表单参数依旧为第一次提交的参数
|
||||
|
||||
**OTHERS**
|
||||
|
||||
- 废弃downloadExcel方法 统一使用commonDownloadExcel方法
|
||||
|
||||
# 1.1.0
|
||||
|
||||
**FEATURES**
|
||||
|
@@ -24,7 +24,7 @@ V1.1.0版本已支持离线图标
|
||||
|
||||
| 组件/框架 | 版本 |
|
||||
| :------------- | :----- |
|
||||
| vben | 5.4.1 |
|
||||
| vben | 5.4.5 |
|
||||
| ant-design-vue | 4.2.5 |
|
||||
| vue | 3.5.11 |
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/web-antd",
|
||||
"version": "1.1.0",
|
||||
"version": "1.1.2",
|
||||
"homepage": "https://vben.pro",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
|
@@ -1,75 +1,5 @@
|
||||
import { isObject, isString } from '@vben/utils';
|
||||
|
||||
import { requestClient } from './request';
|
||||
|
||||
const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss';
|
||||
|
||||
export function joinTimestamp<T extends boolean>(
|
||||
join: boolean,
|
||||
restful: T,
|
||||
): T extends true ? string : object;
|
||||
|
||||
export function joinTimestamp(join: boolean, restful = false): object | string {
|
||||
if (!join) {
|
||||
return restful ? '' : {};
|
||||
}
|
||||
const now = Date.now();
|
||||
if (restful) {
|
||||
return `?_t=${now}`;
|
||||
}
|
||||
return { _t: now };
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: Format request parameter time
|
||||
*/
|
||||
export function formatRequestDate(params: Record<string, any>) {
|
||||
if (Object.prototype.toString.call(params) !== '[object Object]') {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const key in params) {
|
||||
const format = params[key]?.format ?? null;
|
||||
if (format && typeof format === 'function') {
|
||||
params[key] = params[key].format(DATE_TIME_FORMAT);
|
||||
}
|
||||
if (isString(key)) {
|
||||
const value = params[key];
|
||||
if (value) {
|
||||
try {
|
||||
params[key] = isString(value) ? value.trim() : value;
|
||||
} catch (error: any) {
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isObject(params[key])) {
|
||||
formatRequestDate(params[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the object as a parameter to the URL
|
||||
* @param baseUrl url
|
||||
* @param obj
|
||||
* @returns {string}
|
||||
* eg:
|
||||
* let obj = {a: '3', b: '4'}
|
||||
* setObjToUrlParams('www.baidu.com', obj)
|
||||
* ==>www.baidu.com?a=3&b=4
|
||||
*/
|
||||
export function setObjToUrlParams(baseUrl: string, obj: any): string {
|
||||
let parameters = '';
|
||||
for (const key in obj) {
|
||||
parameters += `${key}=${encodeURIComponent(obj[key])}&`;
|
||||
}
|
||||
parameters = parameters.replace(/&$/, '');
|
||||
return /\?$/.test(baseUrl)
|
||||
? baseUrl + parameters
|
||||
: baseUrl.replace(/\/?$/, '?') + parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: contentType
|
||||
*/
|
||||
|
@@ -12,7 +12,6 @@ import {
|
||||
RequestClient,
|
||||
} from '@vben/request';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
import { isString } from '@vben/utils';
|
||||
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import { isEmpty, isNull } from 'lodash-es';
|
||||
@@ -27,8 +26,6 @@ import {
|
||||
} from '#/utils/encryption/crypto';
|
||||
import * as encryptUtil from '#/utils/encryption/jsencrypt';
|
||||
|
||||
import { formatRequestDate, joinTimestamp, setObjToUrlParams } from './helper';
|
||||
|
||||
const { apiURL, clientId, enableEncrypt } = useAppConfig(
|
||||
import.meta.env,
|
||||
import.meta.env.PROD,
|
||||
@@ -46,16 +43,10 @@ function createRequestClient(baseURL: string) {
|
||||
baseURL,
|
||||
// 消息提示类型
|
||||
errorMessageMode: 'message',
|
||||
// 格式化提交参数时间
|
||||
formatDate: true,
|
||||
// 是否返回原生响应 比如:需要获取响应头时使用该属性
|
||||
isReturnNativeResponse: false,
|
||||
// 需要对返回数据进行处理
|
||||
isTransformResponse: true,
|
||||
// post请求的时候添加参数到url
|
||||
joinParamsToUrl: false,
|
||||
// 是否加入时间戳
|
||||
joinTime: false,
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -99,54 +90,11 @@ function createRequestClient(baseURL: string) {
|
||||
*/
|
||||
const language = preferences.app.locale.replace('-', '_');
|
||||
config.headers['Accept-Language'] = language;
|
||||
config.headers['Content-Language'] = language;
|
||||
// 添加全局clientId
|
||||
config.headers.clientId = clientId;
|
||||
|
||||
const { encrypt, formatDate, joinParamsToUrl, joinTime = true } = config;
|
||||
const params = config.params || {};
|
||||
const data = config.data || false;
|
||||
// TODO: 这块要重构 复杂度太高了
|
||||
formatDate && data && !isString(data) && formatRequestDate(data);
|
||||
if (config.method?.toUpperCase() === 'GET') {
|
||||
if (isString(params)) {
|
||||
// 兼容restful风格
|
||||
config.url = `${config.url + params}${joinTimestamp(joinTime, true)}`;
|
||||
config.params = undefined;
|
||||
} else {
|
||||
// 给 get 请求加上时间戳参数,避免从缓存中拿数据。
|
||||
config.params = Object.assign(
|
||||
params || {},
|
||||
joinTimestamp(joinTime, false),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (isString(params)) {
|
||||
// 兼容restful风格
|
||||
config.url = config.url + params;
|
||||
config.params = undefined;
|
||||
} else {
|
||||
formatDate && formatRequestDate(params);
|
||||
if (
|
||||
Reflect.has(config, 'data') &&
|
||||
config.data &&
|
||||
(Object.keys(config.data).length > 0 ||
|
||||
config.data instanceof FormData)
|
||||
) {
|
||||
config.data = data;
|
||||
config.params = params;
|
||||
} else {
|
||||
// 非GET请求如果没有提供data,则将params视为data
|
||||
config.data = params;
|
||||
config.params = undefined;
|
||||
}
|
||||
if (joinParamsToUrl) {
|
||||
config.url = setObjToUrlParams(
|
||||
config.url as string,
|
||||
Object.assign({}, config.params, config.data),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
const { encrypt } = config;
|
||||
// 全局开启请求加密功能 && 该请求开启 && 是post/put请求
|
||||
if (
|
||||
enableEncrypt &&
|
||||
|
@@ -48,14 +48,6 @@ function handleChange(info: Record<string, any>) {
|
||||
const name = file?.name;
|
||||
|
||||
switch (status) {
|
||||
case 'uploading': {
|
||||
if (!uploading) {
|
||||
emit('uploading', name);
|
||||
uploading = true;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'done': {
|
||||
// http 200会走到这里 需要再次判断
|
||||
const { response } = file;
|
||||
@@ -77,6 +69,14 @@ function handleChange(info: Record<string, any>) {
|
||||
|
||||
break;
|
||||
}
|
||||
case 'uploading': {
|
||||
if (!uploading) {
|
||||
emit('uploading', name);
|
||||
uploading = true;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
// No default
|
||||
}
|
||||
}
|
||||
|
@@ -88,7 +88,10 @@ const avatar = computed(() => {
|
||||
});
|
||||
|
||||
async function handleLogout() {
|
||||
await authStore.logout();
|
||||
/**
|
||||
* 主动登出不需要带跳转地址
|
||||
*/
|
||||
await authStore.logout(false);
|
||||
resetRoutes();
|
||||
}
|
||||
|
||||
|
@@ -47,4 +47,18 @@ export const overridesPreferences = defineOverridesPreferences({
|
||||
*/
|
||||
semiDarkSidebar: false,
|
||||
},
|
||||
/**
|
||||
* !!! 更改配置后请清空浏览器缓存
|
||||
* 在这里更换logo
|
||||
* source可选值:
|
||||
* 1. 本地public目录下的图片 需要加上/ 比如:/logo.png
|
||||
* 2. 网络图片链接
|
||||
* 3. vite导入的图片 import xxx from 'xxx.png'
|
||||
*
|
||||
* !!! 更改配置后请清空浏览器缓存
|
||||
*/
|
||||
// logo: {
|
||||
// enable: true,
|
||||
// source: '',
|
||||
// },
|
||||
});
|
||||
|
@@ -17,12 +17,12 @@ const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
|
||||
|
||||
/** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统(不会显示在菜单中) */
|
||||
// const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
|
||||
/** 不需要权限的菜单列表(会显示在菜单中) */
|
||||
// const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
|
||||
const staticRoutes: RouteRecordRaw[] = [];
|
||||
const externalRoutes: RouteRecordRaw[] = [];
|
||||
|
||||
/** 路由列表,由基本路由+静态路由组成 */
|
||||
/** 路由列表,由基本路由、外部路由和404兜底路由组成
|
||||
* 无需走权限验证(会一直显示在菜单中) */
|
||||
const routes: RouteRecordRaw[] = [
|
||||
...coreRoutes,
|
||||
...externalRoutes,
|
||||
@@ -32,5 +32,6 @@ const routes: RouteRecordRaw[] = [
|
||||
/** 基本路由列表,这些路由不需要进入权限拦截 */
|
||||
const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
|
||||
|
||||
/** 有权限校验的路由列表,包含动态路由和静态路由 */
|
||||
const accessRoutes = [...dynamicRoutes, ...staticRoutes];
|
||||
export { accessRoutes, coreRouteNames, routes };
|
||||
|
@@ -1,10 +1,16 @@
|
||||
import type { VbenFormProps } from '#/adapter/form';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
import { cloneDeep, formatDate } from '@vben/utils';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { dataURLtoBlob, urlToBase64 } from './base64Conver';
|
||||
|
||||
/**
|
||||
*
|
||||
* @deprecated 无法处理区间选择器数据 请使用commonDownloadExcel
|
||||
*
|
||||
* 下载excel文件
|
||||
* @param [func] axios函数
|
||||
* @param [fileName] 文件名称 不需要带xlsx后缀
|
||||
@@ -30,6 +36,91 @@ export async function downloadExcel(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 源码同packages\@core\ui-kit\form-ui\src\components\form-actions.vue
|
||||
* @param values 表单值
|
||||
* @param fieldMappingTime 区间选择器 字段映射
|
||||
* @returns 格式化后的值
|
||||
*/
|
||||
function handleRangeTimeValue(
|
||||
values: Record<string, any>,
|
||||
fieldMappingTime: VbenFormProps['fieldMappingTime'],
|
||||
) {
|
||||
// 需要深拷贝 可能是readonly的
|
||||
values = cloneDeep(values);
|
||||
if (!fieldMappingTime || !Array.isArray(fieldMappingTime)) {
|
||||
return values;
|
||||
}
|
||||
|
||||
fieldMappingTime.forEach(
|
||||
([field, [startTimeKey, endTimeKey], format = 'YYYY-MM-DD']) => {
|
||||
if (
|
||||
values[field] === null &&
|
||||
values[startTimeKey] &&
|
||||
values[endTimeKey]
|
||||
) {
|
||||
Reflect.deleteProperty(values, startTimeKey);
|
||||
Reflect.deleteProperty(values, endTimeKey);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!values[field]) {
|
||||
Reflect.deleteProperty(values, field);
|
||||
return;
|
||||
}
|
||||
|
||||
const [startTime, endTime] = values[field];
|
||||
const [startTimeFormat, endTimeFormat] = Array.isArray(format)
|
||||
? format
|
||||
: [format, format];
|
||||
|
||||
values[startTimeKey] = startTime
|
||||
? formatDate(startTime, startTimeFormat)
|
||||
: undefined;
|
||||
values[endTimeKey] = endTime
|
||||
? formatDate(endTime, endTimeFormat)
|
||||
: undefined;
|
||||
|
||||
Reflect.deleteProperty(values, field);
|
||||
},
|
||||
);
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
export interface DownloadExcelOptions {
|
||||
// 是否随机文件名(带时间戳)
|
||||
withRandomName?: boolean;
|
||||
// 区间选择器 字段映射
|
||||
fieldMappingTime?: VbenFormProps['fieldMappingTime'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用下载excel方法
|
||||
* @param api 后端下载接口
|
||||
* @param fileName 文件名 不带拓展名
|
||||
* @param requestData 请求参数
|
||||
* @param options 下载选项
|
||||
*/
|
||||
export async function commonDownloadExcel(
|
||||
api: (data?: any) => Promise<Blob>,
|
||||
fileName: string,
|
||||
requestData: any = {},
|
||||
options: DownloadExcelOptions = {},
|
||||
) {
|
||||
const hideLoading = message.loading($t('pages.common.downloadLoading'), 0);
|
||||
try {
|
||||
const { withRandomName = true, fieldMappingTime } = options;
|
||||
// 需要处理时间字段映射
|
||||
const data = await api(handleRangeTimeValue(requestData, fieldMappingTime));
|
||||
downloadExcelFile(data, fileName, withRandomName);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
export function downloadExcelFile(
|
||||
data: BlobPart,
|
||||
filename: string,
|
||||
|
@@ -152,8 +152,10 @@ async function handleAccountLogin(values: LoginAndRegisterParams) {
|
||||
:form-schema="formSchema"
|
||||
:loading="authStore.loginLoading"
|
||||
:show-register="false"
|
||||
:show-third-party-login="true"
|
||||
@submit="handleAccountLogin"
|
||||
>
|
||||
<!-- 可通过show-third-party-login控制是否显示第三方登录 -->
|
||||
<template #third-party-login>
|
||||
<OAuthLogin />
|
||||
</template>
|
||||
|
@@ -1,9 +1,7 @@
|
||||
<script setup lang="tsx">
|
||||
import type { ColumnsType } from 'ant-design-vue/es/table';
|
||||
import { computed, ref, unref } from 'vue';
|
||||
|
||||
import type { SocialInfo } from '#/api/system/social/model';
|
||||
|
||||
import { computed, onMounted, ref, unref } from 'vue';
|
||||
import { useVbenVxeGrid, type VxeGridProps } from '@vben/plugins/vxe-table';
|
||||
|
||||
import {
|
||||
Alert,
|
||||
@@ -13,7 +11,6 @@ import {
|
||||
ListItem,
|
||||
message,
|
||||
Modal,
|
||||
Table,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { authUnbinding } from '#/api';
|
||||
@@ -21,47 +18,6 @@ import { socialList } from '#/api/system/social';
|
||||
|
||||
import { accountBindList, type BindItem } from '../../oauth-common';
|
||||
|
||||
const columns: ColumnsType = [
|
||||
{
|
||||
align: 'center',
|
||||
dataIndex: 'source',
|
||||
title: '绑定平台',
|
||||
},
|
||||
{
|
||||
align: 'center',
|
||||
customRender: ({ value }) => {
|
||||
return <Avatar src={value} />;
|
||||
},
|
||||
dataIndex: 'avatar',
|
||||
title: '头像',
|
||||
},
|
||||
{
|
||||
align: 'center',
|
||||
dataIndex: 'userName',
|
||||
title: '账号',
|
||||
},
|
||||
{
|
||||
align: 'center',
|
||||
dataIndex: 'action',
|
||||
title: '操作',
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* 解绑账号
|
||||
*/
|
||||
function handleUnbind(record: Record<string, any>) {
|
||||
Modal.confirm({
|
||||
content: `确定解绑[${record.source}]平台的[${record.userName}]账号吗?`,
|
||||
async onOk() {
|
||||
await authUnbinding(record.id);
|
||||
await reload();
|
||||
},
|
||||
title: '提示',
|
||||
type: 'warning',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 没有传递action事件则不支持绑定 弹出默认提示
|
||||
*/
|
||||
@@ -85,35 +41,102 @@ const bindList = computed<BindItem[]>(() => {
|
||||
return list;
|
||||
});
|
||||
|
||||
const tableData = ref<SocialInfo[]>([]);
|
||||
const gridOptions: VxeGridProps = {
|
||||
checkboxConfig: {
|
||||
// 高亮
|
||||
highlight: true,
|
||||
// 翻页时保留选中状态
|
||||
reserve: true,
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
field: 'source',
|
||||
title: '绑定平台',
|
||||
},
|
||||
{
|
||||
slots: {
|
||||
default: ({ row }) => {
|
||||
return <Avatar src={row.avatar} />;
|
||||
},
|
||||
},
|
||||
field: 'avatar',
|
||||
title: '头像',
|
||||
},
|
||||
{
|
||||
align: 'center',
|
||||
field: 'userName',
|
||||
title: '账号',
|
||||
},
|
||||
{
|
||||
align: 'center',
|
||||
slots: {
|
||||
default: 'action',
|
||||
},
|
||||
title: '操作',
|
||||
},
|
||||
],
|
||||
height: 220,
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
toolbarConfig: {
|
||||
custom: false,
|
||||
zoom: false,
|
||||
refresh: false,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async () => {
|
||||
const resp = await socialList();
|
||||
/**
|
||||
* 平台转小写
|
||||
* 已经绑定的平台
|
||||
*/
|
||||
boundPlatformsList.value = resp.map((item) =>
|
||||
item.source.toLowerCase(),
|
||||
);
|
||||
return {
|
||||
rows: resp,
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
isHover: true,
|
||||
isCurrent: false,
|
||||
keyField: 'id',
|
||||
},
|
||||
id: 'profile-bind-table',
|
||||
};
|
||||
|
||||
async function reload() {
|
||||
const resp = await socialList();
|
||||
/**
|
||||
* 平台转小写
|
||||
* 已经绑定的平台
|
||||
*/
|
||||
boundPlatformsList.value = resp.map((item) => item.source.toLowerCase());
|
||||
tableData.value = resp;
|
||||
const [BasicTable, tableApi] = useVbenVxeGrid({
|
||||
gridOptions,
|
||||
});
|
||||
|
||||
/**
|
||||
* 解绑账号
|
||||
*/
|
||||
function handleUnbind(record: Record<string, any>) {
|
||||
Modal.confirm({
|
||||
content: `确定解绑[${record.source}]平台的[${record.userName}]账号吗?`,
|
||||
async onOk() {
|
||||
await authUnbinding(record.id);
|
||||
await tableApi.reload();
|
||||
},
|
||||
title: '提示',
|
||||
type: 'warning',
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(reload);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-[16px]">
|
||||
<Table
|
||||
:columns="columns"
|
||||
:data-source="tableData"
|
||||
:pagination="false"
|
||||
size="middle"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'action'">
|
||||
<a-button type="link" @click="handleUnbind(record)">解绑</a-button>
|
||||
</template>
|
||||
<BasicTable>
|
||||
<template #action="{ row }">
|
||||
<a-button type="link" @click="handleUnbind(row)">解绑</a-button>
|
||||
</template>
|
||||
</Table>
|
||||
</BasicTable>
|
||||
<div class="pb-3">
|
||||
<List
|
||||
:data-source="bindList"
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { UserProfile } from '#/api/system/profile/model';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { useUserStore } from '@vben/stores';
|
||||
@@ -34,7 +34,8 @@ async function handleUploadFinish() {
|
||||
userStore.setUserInfo(userInfo);
|
||||
}
|
||||
|
||||
emitter.on('updateProfile', loadProfile);
|
||||
onMounted(() => emitter.on('updateProfile', loadProfile));
|
||||
onUnmounted(() => emitter.off('updateProfile'));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@@ -7,6 +7,7 @@ import type {
|
||||
} from '@vben/common-ui';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import {
|
||||
AnalysisChartCard,
|
||||
@@ -18,11 +19,15 @@ import {
|
||||
} from '@vben/common-ui';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { useUserStore } from '@vben/stores';
|
||||
import { openWindow } from '@vben/utils';
|
||||
|
||||
import AnalyticsVisitsSource from '../analytics/analytics-visits-source.vue';
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
// 这是一个示例数据,实际项目中需要根据实际情况进行调整
|
||||
// url 也可以是内部路由,在 navTo 方法中识别处理,进行内部跳转
|
||||
// 例如:url: /dashboard/workspace
|
||||
const projectItems: WorkbenchProjectItem[] = [
|
||||
{
|
||||
color: '',
|
||||
@@ -31,6 +36,7 @@ const projectItems: WorkbenchProjectItem[] = [
|
||||
group: '开源组',
|
||||
icon: 'carbon:logo-github',
|
||||
title: 'Github',
|
||||
url: 'https://github.com',
|
||||
},
|
||||
{
|
||||
color: '#3fb27f',
|
||||
@@ -39,6 +45,7 @@ const projectItems: WorkbenchProjectItem[] = [
|
||||
group: '算法组',
|
||||
icon: 'ion:logo-vue',
|
||||
title: 'Vue',
|
||||
url: 'https://vuejs.org',
|
||||
},
|
||||
{
|
||||
color: '#e18525',
|
||||
@@ -47,6 +54,7 @@ const projectItems: WorkbenchProjectItem[] = [
|
||||
group: '上班摸鱼',
|
||||
icon: 'ion:logo-html5',
|
||||
title: 'Html5',
|
||||
url: 'https://developer.mozilla.org/zh-CN/docs/Web/HTML',
|
||||
},
|
||||
{
|
||||
color: '#bf0c2c',
|
||||
@@ -55,6 +63,7 @@ const projectItems: WorkbenchProjectItem[] = [
|
||||
group: 'UI',
|
||||
icon: 'ion:logo-angular',
|
||||
title: 'Angular',
|
||||
url: 'https://angular.io',
|
||||
},
|
||||
{
|
||||
color: '#00d8ff',
|
||||
@@ -63,6 +72,7 @@ const projectItems: WorkbenchProjectItem[] = [
|
||||
group: '技术牛',
|
||||
icon: 'bx:bxl-react',
|
||||
title: 'React',
|
||||
url: 'https://reactjs.org',
|
||||
},
|
||||
{
|
||||
color: '#EBD94E',
|
||||
@@ -71,39 +81,47 @@ const projectItems: WorkbenchProjectItem[] = [
|
||||
group: '架构组',
|
||||
icon: 'ion:logo-javascript',
|
||||
title: 'Js',
|
||||
url: 'https://developer.mozilla.org/zh-CN/docs/Web/JavaScript',
|
||||
},
|
||||
];
|
||||
|
||||
// 同样,这里的 url 也可以使用以 http 开头的外部链接
|
||||
const quickNavItems: WorkbenchQuickNavItem[] = [
|
||||
{
|
||||
color: '#1fdaca',
|
||||
icon: 'ion:home-outline',
|
||||
title: '首页',
|
||||
url: '/',
|
||||
},
|
||||
{
|
||||
color: '#bf0c2c',
|
||||
icon: 'ion:grid-outline',
|
||||
title: '仪表盘',
|
||||
url: '/dashboard',
|
||||
},
|
||||
{
|
||||
color: '#e18525',
|
||||
icon: 'ion:layers-outline',
|
||||
title: '组件',
|
||||
url: '/demos/features/icons',
|
||||
},
|
||||
{
|
||||
color: '#3fb27f',
|
||||
icon: 'ion:settings-outline',
|
||||
title: '系统管理',
|
||||
url: '/demos/features/login-expired', // 这里的 URL 是示例,实际项目中需要根据实际情况进行调整
|
||||
},
|
||||
{
|
||||
color: '#4daf1bc9',
|
||||
icon: 'ion:key-outline',
|
||||
title: '权限管理',
|
||||
url: '/demos/access/page-control',
|
||||
},
|
||||
{
|
||||
color: '#00d8ff',
|
||||
icon: 'ion:bar-chart-outline',
|
||||
title: '图表',
|
||||
url: '/analytics',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -195,6 +213,24 @@ const trendItems: WorkbenchTrendItem[] = [
|
||||
title: 'Vben',
|
||||
},
|
||||
];
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// 这是一个示例方法,实际项目中需要根据实际情况进行调整
|
||||
// This is a sample method, adjust according to the actual project requirements
|
||||
function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
|
||||
if (nav.url?.startsWith('http')) {
|
||||
openWindow(nav.url);
|
||||
return;
|
||||
}
|
||||
if (nav.url?.startsWith('/')) {
|
||||
router.push(nav.url).catch((error) => {
|
||||
console.error('Navigation failed:', error);
|
||||
});
|
||||
} else {
|
||||
console.warn(`Unknown URL for navigation item: ${nav.title} -> ${nav.url}`);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -210,7 +246,7 @@ const trendItems: WorkbenchTrendItem[] = [
|
||||
|
||||
<div class="mt-5 flex flex-col lg:flex-row">
|
||||
<div class="mr-4 w-full lg:w-3/5">
|
||||
<WorkbenchProject :items="projectItems" title="项目" />
|
||||
<WorkbenchProject :items="projectItems" title="项目" @click="navTo" />
|
||||
<WorkbenchTrends :items="trendItems" class="mt-5" title="最新动态" />
|
||||
</div>
|
||||
<div class="w-full lg:w-2/5">
|
||||
@@ -218,6 +254,7 @@ const trendItems: WorkbenchTrendItem[] = [
|
||||
:items="quickNavItems"
|
||||
class="mt-5 lg:mt-0"
|
||||
title="快捷导航"
|
||||
@click="navTo"
|
||||
/>
|
||||
<WorkbenchTodo :items="todoItems" class="mt-5" title="待办事项" />
|
||||
<AnalysisChartCard class="mt-5" title="访问来源">
|
||||
|
@@ -7,7 +7,6 @@ import { Page, useVbenModal, type VbenFormProps } from '@vben/common-ui';
|
||||
import { getPopupContainer } from '@vben/utils';
|
||||
|
||||
import { Modal, Popconfirm, Space } from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import {
|
||||
tableCheckboxEvent,
|
||||
@@ -44,21 +43,6 @@ const gridOptions: VxeGridProps = {
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues = {}) => {
|
||||
// 区间选择器处理
|
||||
if (formValues?.createTime) {
|
||||
formValues.params = {
|
||||
beginTime: dayjs(formValues.createTime[0]).format(
|
||||
'YYYY-MM-DD 00:00:00',
|
||||
),
|
||||
endTime: dayjs(formValues.createTime[1]).format(
|
||||
'YYYY-MM-DD 23:59:59',
|
||||
),
|
||||
};
|
||||
Reflect.deleteProperty(formValues, 'createTime');
|
||||
} else {
|
||||
Reflect.deleteProperty(formValues, 'params');
|
||||
}
|
||||
|
||||
return await demoList({
|
||||
pageNum: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
|
3
apps/web-antd/src/views/monitor/cache/components/index.ts
vendored
Normal file
3
apps/web-antd/src/views/monitor/cache/components/index.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as CommandChart } from './command-chart.vue';
|
||||
export { default as MemoryChart } from './memory-chart.vue';
|
||||
export { default as RedisDescription } from './redis-description.vue';
|
@@ -8,9 +8,7 @@ import { Button, Card, Col, Row } from 'ant-design-vue';
|
||||
|
||||
import { redisCacheInfo, type RedisInfo } from '#/api/monitor/cache';
|
||||
|
||||
import CommandChart from './components/CommandChart.vue';
|
||||
import MemoryChart from './components/MemoryChart.vue';
|
||||
import RedisDescription from './components/RedisDescription.vue';
|
||||
import { CommandChart, MemoryChart, RedisDescription } from './components';
|
||||
|
||||
const baseSpan = { lg: 12, md: 24, sm: 24, xl: 12, xs: 24 };
|
||||
|
||||
|
@@ -7,7 +7,6 @@ import { Page, useVbenModal, type VbenFormProps } from '@vben/common-ui';
|
||||
import { getVxePopupContainer } from '@vben/utils';
|
||||
|
||||
import { Modal, Popconfirm, Space } from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import {
|
||||
tableCheckboxEvent,
|
||||
@@ -22,7 +21,7 @@ import {
|
||||
loginInfoRemove,
|
||||
userUnlock,
|
||||
} from '#/api/monitor/logininfo';
|
||||
import { downloadExcel } from '#/utils/file/download';
|
||||
import { commonDownloadExcel } from '#/utils/file/download';
|
||||
import { confirmDeleteModal } from '#/utils/modal';
|
||||
|
||||
import { columns, querySchema } from './data';
|
||||
@@ -37,6 +36,14 @@ const formOptions: VbenFormProps = {
|
||||
},
|
||||
schema: querySchema(),
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
|
||||
// 日期选择格式化
|
||||
fieldMappingTime: [
|
||||
[
|
||||
'dateTime',
|
||||
['params[beginTime]', 'params[endTime]'],
|
||||
['YYYY-MM-DD 00:00:00', 'YYYY-MM-DD 23:59:59'],
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
const gridOptions: VxeGridProps = {
|
||||
@@ -55,20 +62,6 @@ const gridOptions: VxeGridProps = {
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues = {}) => {
|
||||
// 区间选择器处理
|
||||
if (formValues?.dateTime) {
|
||||
formValues.params = {
|
||||
beginTime: dayjs(formValues.dateTime[0]).format(
|
||||
'YYYY-MM-DD 00:00:00',
|
||||
),
|
||||
endTime: dayjs(formValues.dateTime[1]).format(
|
||||
'YYYY-MM-DD 23:59:59',
|
||||
),
|
||||
};
|
||||
Reflect.deleteProperty(formValues, 'dateTime');
|
||||
} else {
|
||||
Reflect.deleteProperty(formValues, 'params');
|
||||
}
|
||||
return await loginInfoList({
|
||||
pageNum: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
@@ -149,6 +142,17 @@ async function handleUnlock() {
|
||||
canUnlock.value = false;
|
||||
tableApi.grid.clearCheckboxRow();
|
||||
}
|
||||
|
||||
function handleDownloadExcel() {
|
||||
commonDownloadExcel(
|
||||
loginInfoExport,
|
||||
'登录日志',
|
||||
tableApi.formApi.form.values,
|
||||
{
|
||||
fieldMappingTime: formOptions.fieldMappingTime,
|
||||
},
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -164,13 +168,7 @@ async function handleUnlock() {
|
||||
</a-button>
|
||||
<a-button
|
||||
v-access:code="['monitor:logininfor:export']"
|
||||
@click="
|
||||
downloadExcel(
|
||||
loginInfoExport,
|
||||
'登录日志',
|
||||
tableApi.formApi.form.values,
|
||||
)
|
||||
"
|
||||
@click="handleDownloadExcel"
|
||||
>
|
||||
{{ $t('pages.common.export') }}
|
||||
</a-button>
|
||||
|
@@ -9,7 +9,6 @@ import { Page, useVbenDrawer, type VbenFormProps } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { Modal, Space } from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
|
||||
import {
|
||||
@@ -23,7 +22,7 @@ import {
|
||||
operLogExport,
|
||||
operLogList,
|
||||
} from '#/api/monitor/operlog';
|
||||
import { downloadExcel } from '#/utils/file/download';
|
||||
import { commonDownloadExcel } from '#/utils/file/download';
|
||||
import { confirmDeleteModal } from '#/utils/modal';
|
||||
|
||||
import { columns, querySchema } from './data';
|
||||
@@ -38,6 +37,14 @@ const formOptions: VbenFormProps = {
|
||||
},
|
||||
schema: querySchema(),
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
|
||||
// 日期选择格式化
|
||||
fieldMappingTime: [
|
||||
[
|
||||
'createTime',
|
||||
['params[beginTime]', 'params[endTime]'],
|
||||
['YYYY-MM-DD 00:00:00', 'YYYY-MM-DD 23:59:59'],
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
const gridOptions: VxeGridProps<OperationLog> = {
|
||||
@@ -56,21 +63,6 @@ const gridOptions: VxeGridProps<OperationLog> = {
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page, sort }, formValues = {}) => {
|
||||
// 区间选择器处理
|
||||
if (formValues?.createTime) {
|
||||
formValues.params = {
|
||||
beginTime: dayjs(formValues.createTime[0]).format(
|
||||
'YYYY-MM-DD 00:00:00',
|
||||
),
|
||||
endTime: dayjs(formValues.createTime[1]).format(
|
||||
'YYYY-MM-DD 23:59:59',
|
||||
),
|
||||
};
|
||||
Reflect.deleteProperty(formValues, 'createTime');
|
||||
} else {
|
||||
Reflect.deleteProperty(formValues, 'params');
|
||||
}
|
||||
|
||||
const params: any = {
|
||||
pageNum: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
@@ -81,6 +73,7 @@ const gridOptions: VxeGridProps<OperationLog> = {
|
||||
params.orderByColumn = sort.field;
|
||||
params.isAsc = sort.order;
|
||||
}
|
||||
|
||||
return await operLogList(params);
|
||||
},
|
||||
},
|
||||
@@ -149,6 +142,12 @@ async function handleDelete() {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function handleDownloadExcel() {
|
||||
commonDownloadExcel(operLogExport, '操作日志', tableApi.formApi.form.values, {
|
||||
fieldMappingTime: formOptions.fieldMappingTime,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -164,13 +163,7 @@ async function handleDelete() {
|
||||
</a-button>
|
||||
<a-button
|
||||
v-access:code="['monitor:operlog:export']"
|
||||
@click="
|
||||
downloadExcel(
|
||||
operLogExport,
|
||||
'操作日志',
|
||||
tableApi.formApi.form.values,
|
||||
)
|
||||
"
|
||||
@click="handleDownloadExcel"
|
||||
>
|
||||
{{ $t('pages.common.export') }}
|
||||
</a-button>
|
||||
|
@@ -21,7 +21,7 @@ import {
|
||||
clientRemove,
|
||||
} from '#/api/system/client';
|
||||
import { TableSwitch } from '#/components/table';
|
||||
import { downloadExcel } from '#/utils/file/download';
|
||||
import { commonDownloadExcel } from '#/utils/file/download';
|
||||
|
||||
import clientDrawer from './client-drawer.vue';
|
||||
import { columns, querySchema } from './data';
|
||||
@@ -114,23 +114,21 @@ function handleMultiDelete() {
|
||||
});
|
||||
}
|
||||
|
||||
function handleDownloadExcel() {
|
||||
commonDownloadExcel(clientExport, '客户端数据', tableApi.formApi.form.values);
|
||||
}
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page :auto-content-height="true">
|
||||
<BasicTable table-title="系统授权列表">
|
||||
<BasicTable table-title="客户端列表">
|
||||
<template #toolbar-tools>
|
||||
<Space>
|
||||
<a-button
|
||||
v-access:code="['system:client:export']"
|
||||
@click="
|
||||
downloadExcel(
|
||||
clientExport,
|
||||
'客户端数据',
|
||||
tableApi.formApi.form.values,
|
||||
)
|
||||
"
|
||||
@click="handleDownloadExcel"
|
||||
>
|
||||
{{ $t('pages.common.export') }}
|
||||
</a-button>
|
||||
|
@@ -98,7 +98,8 @@ export const modalSchema: FormSchemaGetter = () => [
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
component: 'Textarea',
|
||||
formItemClass: 'items-baseline',
|
||||
fieldName: 'configValue',
|
||||
label: '参数键值',
|
||||
rules: 'required',
|
||||
|
@@ -7,7 +7,6 @@ import { Page, useVbenModal, type VbenFormProps } from '@vben/common-ui';
|
||||
import { getVxePopupContainer } from '@vben/utils';
|
||||
|
||||
import { Modal, Popconfirm, Space } from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import {
|
||||
tableCheckboxEvent,
|
||||
@@ -20,7 +19,7 @@ import {
|
||||
configRefreshCache,
|
||||
configRemove,
|
||||
} from '#/api/system/config';
|
||||
import { downloadExcel } from '#/utils/file/download';
|
||||
import { commonDownloadExcel } from '#/utils/file/download';
|
||||
|
||||
import configModal from './config-modal.vue';
|
||||
import { columns, querySchema } from './data';
|
||||
@@ -34,6 +33,14 @@ const formOptions: VbenFormProps = {
|
||||
},
|
||||
schema: querySchema(),
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
|
||||
// 日期选择格式化
|
||||
fieldMappingTime: [
|
||||
[
|
||||
'createTime',
|
||||
['params[beginTime]', 'params[endTime]'],
|
||||
['YYYY-MM-DD 00:00:00', 'YYYY-MM-DD 23:59:59'],
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
const gridOptions: VxeGridProps = {
|
||||
@@ -50,21 +57,6 @@ const gridOptions: VxeGridProps = {
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues = {}) => {
|
||||
// 区间选择器处理
|
||||
if (formValues?.createTime) {
|
||||
formValues.params = {
|
||||
beginTime: dayjs(formValues.createTime[0]).format(
|
||||
'YYYY-MM-DD 00:00:00',
|
||||
),
|
||||
endTime: dayjs(formValues.createTime[1]).format(
|
||||
'YYYY-MM-DD 23:59:59',
|
||||
),
|
||||
};
|
||||
Reflect.deleteProperty(formValues, 'createTime');
|
||||
} else {
|
||||
Reflect.deleteProperty(formValues, 'params');
|
||||
}
|
||||
|
||||
return await configList({
|
||||
pageNum: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
@@ -123,6 +115,12 @@ function handleMultiDelete() {
|
||||
});
|
||||
}
|
||||
|
||||
function handleDownloadExcel() {
|
||||
commonDownloadExcel(configExport, '参数配置', tableApi.formApi.form.values, {
|
||||
fieldMappingTime: formOptions.fieldMappingTime,
|
||||
});
|
||||
}
|
||||
|
||||
async function handleRefreshCache() {
|
||||
await configRefreshCache();
|
||||
await tableApi.query();
|
||||
@@ -137,13 +135,7 @@ async function handleRefreshCache() {
|
||||
<a-button @click="handleRefreshCache"> 刷新缓存 </a-button>
|
||||
<a-button
|
||||
v-access:code="['system:config:export']"
|
||||
@click="
|
||||
downloadExcel(
|
||||
configExport,
|
||||
'参数配置',
|
||||
tableApi.formApi.form.values,
|
||||
)
|
||||
"
|
||||
@click="handleDownloadExcel"
|
||||
>
|
||||
{{ $t('pages.common.export') }}
|
||||
</a-button>
|
||||
|
@@ -4,12 +4,7 @@ import type { Recordable } from '@vben/types';
|
||||
import { nextTick } from 'vue';
|
||||
|
||||
import { Page, useVbenDrawer, type VbenFormProps } from '@vben/common-ui';
|
||||
import {
|
||||
eachTree,
|
||||
getVxePopupContainer,
|
||||
listToTree,
|
||||
removeEmptyChildren,
|
||||
} from '@vben/utils';
|
||||
import { eachTree, getVxePopupContainer } from '@vben/utils';
|
||||
|
||||
import { Popconfirm, Space } from 'ant-design-vue';
|
||||
|
||||
@@ -43,13 +38,7 @@ const gridOptions: VxeGridProps = {
|
||||
const resp = await deptList({
|
||||
...formValues,
|
||||
});
|
||||
const treeData = listToTree(resp, {
|
||||
id: 'deptId',
|
||||
pid: 'parentId',
|
||||
children: 'children',
|
||||
});
|
||||
removeEmptyChildren(treeData);
|
||||
return { rows: treeData };
|
||||
return { rows: resp };
|
||||
},
|
||||
// 默认请求接口后展开全部 不需要可以删除这段
|
||||
querySuccess: () => {
|
||||
@@ -62,15 +51,21 @@ const gridOptions: VxeGridProps = {
|
||||
},
|
||||
},
|
||||
},
|
||||
/**
|
||||
* 虚拟滚动 默认关闭
|
||||
*/
|
||||
scrollY: {
|
||||
enabled: false,
|
||||
gt: 0,
|
||||
},
|
||||
rowConfig: {
|
||||
isHover: true,
|
||||
keyField: 'deptId',
|
||||
},
|
||||
|
||||
treeConfig: {
|
||||
parentField: 'parentId',
|
||||
rowField: 'deptId',
|
||||
transform: false,
|
||||
transform: true,
|
||||
},
|
||||
id: 'system-dept-index',
|
||||
};
|
||||
|
@@ -7,7 +7,6 @@ import { useVbenDrawer, type VbenFormProps } from '@vben/common-ui';
|
||||
import { getVxePopupContainer } from '@vben/utils';
|
||||
|
||||
import { Modal, Popconfirm, Space } from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import {
|
||||
tableCheckboxEvent,
|
||||
@@ -19,7 +18,7 @@ import {
|
||||
dictDataList,
|
||||
dictDataRemove,
|
||||
} from '#/api/system/dict/dict-data';
|
||||
import { downloadExcel } from '#/utils/file/download';
|
||||
import { commonDownloadExcel } from '#/utils/file/download';
|
||||
|
||||
import { emitter } from '../mitt';
|
||||
import { columns, querySchema } from './data';
|
||||
@@ -54,21 +53,6 @@ const gridOptions: VxeGridProps = {
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues = {}) => {
|
||||
// 区间选择器处理
|
||||
if (formValues?.createTime) {
|
||||
formValues.params = {
|
||||
beginTime: dayjs(formValues.createTime[0]).format(
|
||||
'YYYY-MM-DD 00:00:00',
|
||||
),
|
||||
endTime: dayjs(formValues.createTime[1]).format(
|
||||
'YYYY-MM-DD 23:59:59',
|
||||
),
|
||||
};
|
||||
Reflect.deleteProperty(formValues, 'createTime');
|
||||
} else {
|
||||
Reflect.deleteProperty(formValues, 'params');
|
||||
}
|
||||
|
||||
const params: any = {
|
||||
pageNum: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
@@ -136,6 +120,10 @@ function handleMultiDelete() {
|
||||
});
|
||||
}
|
||||
|
||||
function handleDownloadExcel() {
|
||||
commonDownloadExcel(dictDataExport, '字典数据', tableApi.formApi.form.values);
|
||||
}
|
||||
|
||||
emitter.on('rowClick', async (value) => {
|
||||
dictType.value = value;
|
||||
await tableApi.query();
|
||||
@@ -149,13 +137,7 @@ emitter.on('rowClick', async (value) => {
|
||||
<Space>
|
||||
<a-button
|
||||
v-access:code="['system:dict:export']"
|
||||
@click="
|
||||
downloadExcel(
|
||||
dictDataExport,
|
||||
'字典数据',
|
||||
tableApi.formApi.form.values,
|
||||
)
|
||||
"
|
||||
@click="handleDownloadExcel"
|
||||
>
|
||||
{{ $t('pages.common.export') }}
|
||||
</a-button>
|
||||
|
@@ -1,8 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { onUnmounted } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import DictDataPanel from './data/index.vue';
|
||||
import { emitter } from './mitt';
|
||||
import DictTypePanel from './type/index.vue';
|
||||
|
||||
onUnmounted(() => emitter.off('rowClick'));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@@ -1,22 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
import { useVbenModal, type VbenFormProps } from '@vben/common-ui';
|
||||
import { getVxePopupContainer } from '@vben/utils';
|
||||
|
||||
import {
|
||||
Dropdown,
|
||||
Menu,
|
||||
MenuItem,
|
||||
type MenuProps,
|
||||
Modal,
|
||||
Popconfirm,
|
||||
Space,
|
||||
} from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { Modal, Popconfirm, Space } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
tableCheckboxEvent,
|
||||
@@ -29,9 +19,7 @@ import {
|
||||
dictTypeRemove,
|
||||
refreshDictTypeCache,
|
||||
} from '#/api/system/dict/dict-type';
|
||||
import { dictSyncTenant } from '#/api/system/tenant';
|
||||
import { useTenantStore } from '#/store/tenant';
|
||||
import { downloadExcel } from '#/utils/file/download';
|
||||
import { commonDownloadExcel } from '#/utils/file/download';
|
||||
|
||||
import { emitter } from '../mitt';
|
||||
import { columns, querySchema } from './data';
|
||||
@@ -64,21 +52,6 @@ const gridOptions: VxeGridProps = {
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues = {}) => {
|
||||
// 区间选择器处理
|
||||
if (formValues?.createTime) {
|
||||
formValues.params = {
|
||||
beginTime: dayjs(formValues.createTime[0]).format(
|
||||
'YYYY-MM-DD 00:00:00',
|
||||
),
|
||||
endTime: dayjs(formValues.createTime[1]).format(
|
||||
'YYYY-MM-DD 23:59:59',
|
||||
),
|
||||
};
|
||||
Reflect.deleteProperty(formValues, 'createTime');
|
||||
} else {
|
||||
Reflect.deleteProperty(formValues, 'params');
|
||||
}
|
||||
|
||||
return await dictTypeList({
|
||||
pageNum: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
@@ -147,43 +120,18 @@ function handleMultiDelete() {
|
||||
});
|
||||
}
|
||||
|
||||
const handleMenuClick: MenuProps['onClick'] = (e) => {
|
||||
switch (e.key) {
|
||||
case '1': {
|
||||
handleRefreshCache();
|
||||
break;
|
||||
}
|
||||
case '2': {
|
||||
handleSyncTenantDict();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
async function handleRefreshCache() {
|
||||
await refreshDictTypeCache();
|
||||
await tableApi.query();
|
||||
}
|
||||
|
||||
function handleSyncTenantDict() {
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
iconType: 'warning',
|
||||
content: '确认同步租户字典?',
|
||||
onOk: async () => {
|
||||
await dictSyncTenant();
|
||||
await tableApi.query();
|
||||
},
|
||||
});
|
||||
function handleDownloadExcel() {
|
||||
commonDownloadExcel(
|
||||
dictTypeExport,
|
||||
'字典类型数据',
|
||||
tableApi.formApi.form.values,
|
||||
);
|
||||
}
|
||||
|
||||
const { hasAccessByRoles } = useAccess();
|
||||
const tenantStore = useTenantStore();
|
||||
/**
|
||||
* 开启租户 & 超级管理员才可以同步租户字典
|
||||
*/
|
||||
const couldSyncTenantDict = computed(() => {
|
||||
return tenantStore.tenantEnable && hasAccessByRoles(['superadmin']);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -191,28 +139,15 @@ const couldSyncTenantDict = computed(() => {
|
||||
<BasicTable id="dict-type" table-title="字典类型列表">
|
||||
<template #toolbar-tools>
|
||||
<Space>
|
||||
<Dropdown>
|
||||
<template #overlay>
|
||||
<Menu @click="handleMenuClick">
|
||||
<span v-access:code="['system:dict:edit']">
|
||||
<MenuItem key="1">刷新字典缓存</MenuItem>
|
||||
</span>
|
||||
<span v-if="couldSyncTenantDict">
|
||||
<MenuItem key="2"> 同步租户字典 </MenuItem>
|
||||
</span>
|
||||
</Menu>
|
||||
</template>
|
||||
<a-button> 更多 </a-button>
|
||||
</Dropdown>
|
||||
<a-button
|
||||
v-access:code="['system:dict:edit']"
|
||||
@click="handleRefreshCache"
|
||||
>
|
||||
刷新缓存
|
||||
</a-button>
|
||||
<a-button
|
||||
v-access:code="['system:dict:export']"
|
||||
@click="
|
||||
downloadExcel(
|
||||
dictTypeExport,
|
||||
'字典类型数据',
|
||||
tableApi.formApi.form.values,
|
||||
)
|
||||
"
|
||||
@click="handleDownloadExcel"
|
||||
>
|
||||
{{ $t('pages.common.export') }}
|
||||
</a-button>
|
||||
|
@@ -6,12 +6,7 @@ import { computed } from 'vue';
|
||||
import { useAccess } from '@vben/access';
|
||||
import { Page, useVbenDrawer, type VbenFormProps } from '@vben/common-ui';
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
import {
|
||||
eachTree,
|
||||
getVxePopupContainer,
|
||||
listToTree,
|
||||
removeEmptyChildren,
|
||||
} from '@vben/utils';
|
||||
import { eachTree, getVxePopupContainer } from '@vben/utils';
|
||||
|
||||
import { Popconfirm, Space } from 'ant-design-vue';
|
||||
|
||||
@@ -49,13 +44,7 @@ const gridOptions: VxeGridProps = {
|
||||
const resp = await menuList({
|
||||
...formValues,
|
||||
});
|
||||
const treeData = listToTree(resp, {
|
||||
id: 'menuId',
|
||||
pid: 'parentId',
|
||||
children: 'children',
|
||||
});
|
||||
removeEmptyChildren(treeData);
|
||||
return { rows: treeData };
|
||||
return { rows: resp };
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -63,11 +52,20 @@ const gridOptions: VxeGridProps = {
|
||||
isHover: true,
|
||||
keyField: 'menuId',
|
||||
},
|
||||
|
||||
/**
|
||||
* 开启虚拟滚动
|
||||
* 数据量小可以选择关闭
|
||||
* 如果遇到样式问题(空白、错位 滚动等)可以选择关闭虚拟滚动
|
||||
*/
|
||||
scrollY: {
|
||||
enabled: true,
|
||||
gt: 0,
|
||||
},
|
||||
treeConfig: {
|
||||
parentField: 'parentId',
|
||||
rowField: 'menuId',
|
||||
transform: false,
|
||||
// 自动转换为tree 由vxe处理 无需手动转换
|
||||
transform: true,
|
||||
},
|
||||
id: 'system-menu-index',
|
||||
};
|
||||
|
@@ -7,7 +7,6 @@ import { Page, useVbenModal, type VbenFormProps } from '@vben/common-ui';
|
||||
import { getVxePopupContainer } from '@vben/utils';
|
||||
|
||||
import { Modal, Popconfirm, Space } from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import {
|
||||
tableCheckboxEvent,
|
||||
@@ -46,21 +45,6 @@ const gridOptions: VxeGridProps = {
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues = {}) => {
|
||||
// 区间选择器处理
|
||||
if (formValues?.createTime) {
|
||||
formValues.params = {
|
||||
beginTime: dayjs(formValues.createTime[0]).format(
|
||||
'YYYY-MM-DD 00:00:00',
|
||||
),
|
||||
endTime: dayjs(formValues.createTime[1]).format(
|
||||
'YYYY-MM-DD 23:59:59',
|
||||
),
|
||||
};
|
||||
Reflect.deleteProperty(formValues, 'createTime');
|
||||
} else {
|
||||
Reflect.deleteProperty(formValues, 'params');
|
||||
}
|
||||
|
||||
return await noticeList({
|
||||
pageNum: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
|
@@ -8,7 +8,6 @@ import { Page, useVbenDrawer, type VbenFormProps } from '@vben/common-ui';
|
||||
import { getVxePopupContainer } from '@vben/utils';
|
||||
|
||||
import { Modal, Popconfirm, Space } from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import {
|
||||
tableCheckboxEvent,
|
||||
@@ -52,21 +51,6 @@ const gridOptions: VxeGridProps = {
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues = {}) => {
|
||||
// 区间选择器处理
|
||||
if (formValues?.createTime) {
|
||||
formValues.params = {
|
||||
beginTime: dayjs(formValues.createTime[0]).format(
|
||||
'YYYY-MM-DD 00:00:00',
|
||||
),
|
||||
endTime: dayjs(formValues.createTime[1]).format(
|
||||
'YYYY-MM-DD 23:59:59',
|
||||
),
|
||||
};
|
||||
Reflect.deleteProperty(formValues, 'createTime');
|
||||
} else {
|
||||
Reflect.deleteProperty(formValues, 'params');
|
||||
}
|
||||
|
||||
return await ossConfigList({
|
||||
pageNum: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
|
@@ -17,7 +17,6 @@ import {
|
||||
Switch,
|
||||
Tooltip,
|
||||
} from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
|
||||
import {
|
||||
@@ -42,6 +41,14 @@ const formOptions: VbenFormProps = {
|
||||
},
|
||||
schema: querySchema(),
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
|
||||
// 日期选择格式化
|
||||
fieldMappingTime: [
|
||||
[
|
||||
'createTime',
|
||||
['params[beginCreateTime]', 'params[endCreateTime]'],
|
||||
['YYYY-MM-DD 00:00:00', 'YYYY-MM-DD 23:59:59'],
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
const gridOptions: VxeGridProps = {
|
||||
@@ -60,21 +67,6 @@ const gridOptions: VxeGridProps = {
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page, sort }, formValues = {}) => {
|
||||
// 区间选择器处理
|
||||
if (formValues?.createTime) {
|
||||
formValues.params = {
|
||||
beginTime: dayjs(formValues.createTime[0]).format(
|
||||
'YYYY-MM-DD 00:00:00',
|
||||
),
|
||||
endTime: dayjs(formValues.createTime[1]).format(
|
||||
'YYYY-MM-DD 23:59:59',
|
||||
),
|
||||
};
|
||||
Reflect.deleteProperty(formValues, 'createTime');
|
||||
} else {
|
||||
Reflect.deleteProperty(formValues, 'params');
|
||||
}
|
||||
|
||||
const params: any = {
|
||||
pageNum: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
|
@@ -7,7 +7,6 @@ import { Page, useVbenDrawer, type VbenFormProps } from '@vben/common-ui';
|
||||
import { getVxePopupContainer } from '@vben/utils';
|
||||
|
||||
import { Modal, Popconfirm, Space } from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import {
|
||||
tableCheckboxEvent,
|
||||
@@ -15,7 +14,7 @@ import {
|
||||
type VxeGridProps,
|
||||
} from '#/adapter/vxe-table';
|
||||
import { postExport, postList, postRemove } from '#/api/system/post';
|
||||
import { downloadExcel } from '#/utils/file/download';
|
||||
import { commonDownloadExcel } from '#/utils/file/download';
|
||||
import DeptTree from '#/views/system/user/dept-tree.vue';
|
||||
|
||||
import { columns, querySchema } from './data';
|
||||
@@ -37,7 +36,9 @@ const formOptions: VbenFormProps = {
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
const { formApi, reload } = tableApi;
|
||||
await formApi.resetForm();
|
||||
await reload();
|
||||
const formValues = formApi.form.values;
|
||||
formApi.setLatestSubmissionValues(formValues);
|
||||
await reload(formValues);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -56,21 +57,6 @@ const gridOptions: VxeGridProps = {
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues = {}) => {
|
||||
// 区间选择器处理
|
||||
if (formValues?.createTime) {
|
||||
formValues.params = {
|
||||
beginTime: dayjs(formValues.createTime[0]).format(
|
||||
'YYYY-MM-DD 00:00:00',
|
||||
),
|
||||
endTime: dayjs(formValues.createTime[1]).format(
|
||||
'YYYY-MM-DD 23:59:59',
|
||||
),
|
||||
};
|
||||
Reflect.deleteProperty(formValues, 'createTime');
|
||||
} else {
|
||||
Reflect.deleteProperty(formValues, 'params');
|
||||
}
|
||||
|
||||
// 部门树选择处理
|
||||
if (selectDeptId.value.length === 1) {
|
||||
formValues.belongDeptId = selectDeptId.value[0];
|
||||
@@ -136,6 +122,10 @@ function handleMultiDelete() {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function handleDownloadExcel() {
|
||||
commonDownloadExcel(postExport, '岗位信息', tableApi.formApi.form.values);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -143,20 +133,15 @@ function handleMultiDelete() {
|
||||
<DeptTree
|
||||
v-model:select-dept-id="selectDeptId"
|
||||
class="w-[260px]"
|
||||
@select="() => tableApi.query()"
|
||||
@reload="() => tableApi.reload()"
|
||||
@select="() => tableApi.reload()"
|
||||
/>
|
||||
<BasicTable class="flex-1 overflow-hidden" table-title="岗位列表">
|
||||
<template #toolbar-tools>
|
||||
<Space>
|
||||
<a-button
|
||||
v-access:code="['system:post:export']"
|
||||
@click="
|
||||
downloadExcel(
|
||||
postExport,
|
||||
'岗位信息数据',
|
||||
tableApi.formApi.form.values,
|
||||
)
|
||||
"
|
||||
@click="handleDownloadExcel"
|
||||
>
|
||||
{{ $t('pages.common.export') }}
|
||||
</a-button>
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { nextTick, ref } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { Page, useVbenDrawer, type VbenFormProps } from '@vben/common-ui';
|
||||
@@ -79,24 +79,8 @@ const [BasicTable, tableApi] = useVbenVxeGrid({
|
||||
},
|
||||
});
|
||||
|
||||
const init = ref(true);
|
||||
const [RoleAssignDrawer, drawerApi] = useVbenDrawer({
|
||||
connectedComponent: roleAssignDrawer,
|
||||
/**
|
||||
* TODO: 等待官方修复
|
||||
* 临时解决方案 for https://github.com/vbenjs/vue-vben-admin/issues/4752
|
||||
* 通过在关闭时使用v-if切换来造成Modal的重新渲染
|
||||
* 目前Modal逻辑为每次打开 内部的元素会重新mount 但是Modal本身不会重新渲染
|
||||
* 该方案会造成关闭动画丢失(应该放在afterClose中)
|
||||
* @param isOpen 是否打开
|
||||
*/
|
||||
onOpenChange: async (isOpen) => {
|
||||
if (!isOpen) {
|
||||
init.value = false;
|
||||
await nextTick();
|
||||
init.value = true;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
function handleAdd() {
|
||||
@@ -172,6 +156,6 @@ function handleMultipleAuthCancel() {
|
||||
</Popconfirm>
|
||||
</template>
|
||||
</BasicTable>
|
||||
<RoleAssignDrawer v-if="init" @reload="tableApi.query()" />
|
||||
<RoleAssignDrawer @reload="tableApi.query()" />
|
||||
</Page>
|
||||
</template>
|
||||
|
@@ -21,7 +21,6 @@ import {
|
||||
Popconfirm,
|
||||
Space,
|
||||
} from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import {
|
||||
tableCheckboxEvent,
|
||||
@@ -35,7 +34,7 @@ import {
|
||||
roleRemove,
|
||||
} from '#/api/system/role';
|
||||
import { TableSwitch } from '#/components/table';
|
||||
import { downloadExcel } from '#/utils/file/download';
|
||||
import { commonDownloadExcel } from '#/utils/file/download';
|
||||
|
||||
import { columns, querySchema } from './data';
|
||||
import roleAuthModal from './role-auth-modal.vue';
|
||||
@@ -50,6 +49,14 @@ const formOptions: VbenFormProps = {
|
||||
},
|
||||
schema: querySchema(),
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
|
||||
// 日期选择格式化
|
||||
fieldMappingTime: [
|
||||
[
|
||||
'createTime',
|
||||
['params[beginTime]', 'params[endTime]'],
|
||||
['YYYY-MM-DD 00:00:00', 'YYYY-MM-DD 23:59:59'],
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
const gridOptions: VxeGridProps = {
|
||||
@@ -69,21 +76,6 @@ const gridOptions: VxeGridProps = {
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues = {}) => {
|
||||
// 区间选择器处理
|
||||
if (formValues?.createTime) {
|
||||
formValues.params = {
|
||||
beginTime: dayjs(formValues.createTime[0]).format(
|
||||
'YYYY-MM-DD 00:00:00',
|
||||
),
|
||||
endTime: dayjs(formValues.createTime[1]).format(
|
||||
'YYYY-MM-DD 23:59:59',
|
||||
),
|
||||
};
|
||||
Reflect.deleteProperty(formValues, 'createTime');
|
||||
} else {
|
||||
Reflect.deleteProperty(formValues, 'params');
|
||||
}
|
||||
|
||||
return await roleList({
|
||||
pageNum: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
@@ -142,6 +134,12 @@ function handleMultiDelete() {
|
||||
});
|
||||
}
|
||||
|
||||
function handleDownloadExcel() {
|
||||
commonDownloadExcel(roleExport, '角色数据', tableApi.formApi.form.values, {
|
||||
fieldMappingTime: formOptions.fieldMappingTime,
|
||||
});
|
||||
}
|
||||
|
||||
const { hasAccessByCodes, hasAccessByRoles } = useAccess();
|
||||
|
||||
const isSuperAdmin = computed(() => hasAccessByRoles(['superadmin']));
|
||||
@@ -168,13 +166,7 @@ function handleAssignRole(record: Recordable<any>) {
|
||||
<Space>
|
||||
<a-button
|
||||
v-access:code="['system:role:export']"
|
||||
@click="
|
||||
downloadExcel(
|
||||
roleExport,
|
||||
'角色数据',
|
||||
tableApi.formApi.form.values,
|
||||
)
|
||||
"
|
||||
@click="handleDownloadExcel"
|
||||
>
|
||||
{{ $t('pages.common.export') }}
|
||||
</a-button>
|
||||
|
@@ -9,7 +9,6 @@ import { Fallback } from '@vben/common-ui';
|
||||
import { getVxePopupContainer } from '@vben/utils';
|
||||
|
||||
import { Modal, Popconfirm, Space } from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import {
|
||||
tableCheckboxEvent,
|
||||
@@ -17,6 +16,7 @@ import {
|
||||
type VxeGridProps,
|
||||
} from '#/adapter/vxe-table';
|
||||
import {
|
||||
dictSyncTenant,
|
||||
tenantExport,
|
||||
tenantList,
|
||||
tenantRemove,
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
} from '#/api/system/tenant';
|
||||
import { TableSwitch } from '#/components/table';
|
||||
import { useTenantStore } from '#/store/tenant';
|
||||
import { downloadExcel } from '#/utils/file/download';
|
||||
import { commonDownloadExcel } from '#/utils/file/download';
|
||||
|
||||
import { columns, querySchema } from './data';
|
||||
import tenantDrawer from './tenant-drawer.vue';
|
||||
@@ -58,21 +58,6 @@ const gridOptions: VxeGridProps = {
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues = {}) => {
|
||||
// 区间选择器处理
|
||||
if (formValues?.createTime) {
|
||||
formValues.params = {
|
||||
beginTime: dayjs(formValues.createTime[0]).format(
|
||||
'YYYY-MM-DD 00:00:00',
|
||||
),
|
||||
endTime: dayjs(formValues.createTime[1]).format(
|
||||
'YYYY-MM-DD 23:59:59',
|
||||
),
|
||||
};
|
||||
Reflect.deleteProperty(formValues, 'createTime');
|
||||
} else {
|
||||
Reflect.deleteProperty(formValues, 'params');
|
||||
}
|
||||
|
||||
return await tenantList({
|
||||
pageNum: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
@@ -142,6 +127,11 @@ function handleMultiDelete() {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function handleDownloadExcel() {
|
||||
commonDownloadExcel(tenantExport, '租户数据', tableApi.formApi.form.values);
|
||||
}
|
||||
|
||||
/**
|
||||
* 与后台逻辑相同
|
||||
* 只有超级管理员能访问租户相关
|
||||
@@ -151,6 +141,18 @@ const { hasAccessByCodes, hasAccessByRoles } = useAccess();
|
||||
const isSuperAdmin = computed(() => {
|
||||
return hasAccessByRoles(['superadmin']);
|
||||
});
|
||||
|
||||
function handleSyncTenantDict() {
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
iconType: 'warning',
|
||||
content: '确认同步租户字典?',
|
||||
onOk: async () => {
|
||||
await dictSyncTenant();
|
||||
await tableApi.query();
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -158,15 +160,15 @@ const isSuperAdmin = computed(() => {
|
||||
<BasicTable table-title="租户列表">
|
||||
<template #toolbar-tools>
|
||||
<Space>
|
||||
<a-button
|
||||
v-access:code="['system:tenant:edit']"
|
||||
@click="handleSyncTenantDict"
|
||||
>
|
||||
同步租户字典
|
||||
</a-button>
|
||||
<a-button
|
||||
v-access:code="['system:tenant:export']"
|
||||
@click="
|
||||
downloadExcel(
|
||||
tenantExport,
|
||||
'租户数据',
|
||||
tableApi.formApi.form.values,
|
||||
)
|
||||
"
|
||||
@click="handleDownloadExcel"
|
||||
>
|
||||
{{ $t('pages.common.export') }}
|
||||
</a-button>
|
||||
|
@@ -1,12 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, h, ref } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenDrawer } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
import { cloneDeep } from '@vben/utils';
|
||||
|
||||
import { Tag } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { tenantAdd, tenantInfo, tenantUpdate } from '#/api/system/tenant';
|
||||
import { packageSelectList } from '#/api/system/tenant-package';
|
||||
@@ -34,18 +32,14 @@ const [BasicForm, formApi] = useVbenForm({
|
||||
async function setupPackageSelect() {
|
||||
const tenantPackageList = await packageSelectList();
|
||||
const options = tenantPackageList.map((item) => ({
|
||||
label: h('div', { class: 'flex items-center gap-[6px]' }, [
|
||||
h('span', null, item.packageName),
|
||||
h(Tag, { color: 'processing' }, () => `${item.menuIds.length}个菜单项`),
|
||||
]),
|
||||
title: item.packageName,
|
||||
label: item.packageName,
|
||||
value: item.packageId,
|
||||
}));
|
||||
formApi.updateSchema([
|
||||
{
|
||||
componentProps: {
|
||||
optionFilterProp: 'title',
|
||||
optionLabelProp: 'title',
|
||||
optionFilterProp: 'label',
|
||||
optionLabelProp: 'label',
|
||||
options,
|
||||
showSearch: true,
|
||||
},
|
||||
|
@@ -9,7 +9,6 @@ import { Fallback } from '@vben/common-ui';
|
||||
import { getVxePopupContainer } from '@vben/utils';
|
||||
|
||||
import { Modal, Popconfirm, Space } from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import {
|
||||
tableCheckboxEvent,
|
||||
@@ -23,7 +22,7 @@ import {
|
||||
packageRemove,
|
||||
} from '#/api/system/tenant-package';
|
||||
import { TableSwitch } from '#/components/table';
|
||||
import { downloadExcel } from '#/utils/file/download';
|
||||
import { commonDownloadExcel } from '#/utils/file/download';
|
||||
|
||||
import { columns, querySchema } from './data';
|
||||
import tenantPackageDrawer from './tenant-package-drawer.vue';
|
||||
@@ -55,21 +54,6 @@ const gridOptions: VxeGridProps = {
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues = {}) => {
|
||||
// 区间选择器处理
|
||||
if (formValues?.createTime) {
|
||||
formValues.params = {
|
||||
beginTime: dayjs(formValues.createTime[0]).format(
|
||||
'YYYY-MM-DD 00:00:00',
|
||||
),
|
||||
endTime: dayjs(formValues.createTime[1]).format(
|
||||
'YYYY-MM-DD 23:59:59',
|
||||
),
|
||||
};
|
||||
Reflect.deleteProperty(formValues, 'createTime');
|
||||
} else {
|
||||
Reflect.deleteProperty(formValues, 'params');
|
||||
}
|
||||
|
||||
return await packageList({
|
||||
pageNum: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
@@ -129,6 +113,14 @@ function handleMultiDelete() {
|
||||
});
|
||||
}
|
||||
|
||||
function handleDownloadExcel() {
|
||||
commonDownloadExcel(
|
||||
packageExport,
|
||||
'租户套餐数据',
|
||||
tableApi.formApi.form.values,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 与后台逻辑相同
|
||||
* 只有超级管理员能访问租户相关
|
||||
@@ -147,13 +139,7 @@ const isSuperAdmin = computed(() => {
|
||||
<Space>
|
||||
<a-button
|
||||
v-access:code="['system:tenantPackage:export']"
|
||||
@click="
|
||||
downloadExcel(
|
||||
packageExport,
|
||||
'租户套餐数据',
|
||||
tableApi.formApi.form.values,
|
||||
)
|
||||
"
|
||||
@click="handleDownloadExcel"
|
||||
>
|
||||
{{ $t('pages.common.export') }}
|
||||
</a-button>
|
||||
|
@@ -10,7 +10,16 @@ import { getDeptTree } from '#/api/system/user';
|
||||
|
||||
defineOptions({ inheritAttrs: false });
|
||||
|
||||
const emit = defineEmits<{ select: [] }>();
|
||||
const emit = defineEmits<{
|
||||
/**
|
||||
* 点击刷新按钮的事件
|
||||
*/
|
||||
reload: [];
|
||||
/**
|
||||
* 点击节点的事件
|
||||
*/
|
||||
select: [];
|
||||
}>();
|
||||
|
||||
const selectDeptId = defineModel('selectDeptId', {
|
||||
required: true,
|
||||
@@ -28,19 +37,23 @@ const deptTreeArray = ref<DeptTreeArray>([]);
|
||||
/** 骨架屏加载 */
|
||||
const showTreeSkeleton = ref<boolean>(true);
|
||||
|
||||
async function reload() {
|
||||
async function loadTree() {
|
||||
showTreeSkeleton.value = true;
|
||||
searchValue.value = '';
|
||||
selectDeptId.value = [];
|
||||
|
||||
const ret = await getDeptTree();
|
||||
emit('select');
|
||||
|
||||
deptTreeArray.value = ret;
|
||||
showTreeSkeleton.value = false;
|
||||
}
|
||||
|
||||
onMounted(reload);
|
||||
async function handleReload() {
|
||||
await loadTree();
|
||||
emit('reload');
|
||||
}
|
||||
|
||||
onMounted(loadTree);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -62,7 +75,7 @@ onMounted(reload);
|
||||
size="small"
|
||||
>
|
||||
<template #enterButton>
|
||||
<a-button @click="reload">
|
||||
<a-button @click="handleReload">
|
||||
<SyncOutlined class="text-primary" />
|
||||
</a-button>
|
||||
</template>
|
||||
|
@@ -23,7 +23,6 @@ import {
|
||||
Popconfirm,
|
||||
Space,
|
||||
} from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import {
|
||||
tableCheckboxEvent,
|
||||
@@ -37,7 +36,7 @@ import {
|
||||
userStatusChange,
|
||||
} from '#/api/system/user';
|
||||
import { TableSwitch } from '#/components/table';
|
||||
import { downloadExcel } from '#/utils/file/download';
|
||||
import { commonDownloadExcel } from '#/utils/file/download';
|
||||
|
||||
import { columns, querySchema } from './data';
|
||||
import DeptTree from './dept-tree.vue';
|
||||
@@ -74,8 +73,18 @@ const formOptions: VbenFormProps = {
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
const { formApi, reload } = tableApi;
|
||||
await formApi.resetForm();
|
||||
await reload();
|
||||
const formValues = formApi.form.values;
|
||||
formApi.setLatestSubmissionValues(formValues);
|
||||
await reload(formValues);
|
||||
},
|
||||
// 日期选择格式化
|
||||
fieldMappingTime: [
|
||||
[
|
||||
'createTime',
|
||||
['params[beginTime]', 'params[endTime]'],
|
||||
['YYYY-MM-DD 00:00:00', 'YYYY-MM-DD 23:59:59'],
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
const gridOptions: VxeGridProps = {
|
||||
@@ -95,20 +104,6 @@ const gridOptions: VxeGridProps = {
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues = {}) => {
|
||||
// 区间选择器处理
|
||||
if (formValues?.createTime) {
|
||||
formValues.params = {
|
||||
beginTime: dayjs(formValues.createTime[0]).format(
|
||||
'YYYY-MM-DD 00:00:00',
|
||||
),
|
||||
endTime: dayjs(formValues.createTime[1]).format(
|
||||
'YYYY-MM-DD 23:59:59',
|
||||
),
|
||||
};
|
||||
Reflect.deleteProperty(formValues, 'createTime');
|
||||
} else {
|
||||
Reflect.deleteProperty(formValues, 'params');
|
||||
}
|
||||
// 部门树选择处理
|
||||
if (selectDeptId.value.length === 1) {
|
||||
formValues.deptId = selectDeptId.value[0];
|
||||
@@ -175,6 +170,12 @@ function handleMultiDelete() {
|
||||
});
|
||||
}
|
||||
|
||||
function handleDownloadExcel() {
|
||||
commonDownloadExcel(userExport, '用户管理', tableApi.formApi.form.values, {
|
||||
fieldMappingTime: formOptions.fieldMappingTime,
|
||||
});
|
||||
}
|
||||
|
||||
const [UserInfoModal, userInfoModalApi] = useVbenModal({
|
||||
connectedComponent: userInfoModal,
|
||||
});
|
||||
@@ -201,20 +202,15 @@ const { hasAccessByCodes } = useAccess();
|
||||
<DeptTree
|
||||
v-model:select-dept-id="selectDeptId"
|
||||
class="w-[260px]"
|
||||
@select="() => tableApi.query()"
|
||||
@reload="() => tableApi.reload()"
|
||||
@select="() => tableApi.reload()"
|
||||
/>
|
||||
<BasicTable class="flex-1 overflow-hidden" table-title="用户列表">
|
||||
<template #toolbar-tools>
|
||||
<Space>
|
||||
<a-button
|
||||
v-access:code="['system:user:export']"
|
||||
@click="
|
||||
downloadExcel(
|
||||
userExport,
|
||||
'用户管理',
|
||||
tableApi.formApi.form.values,
|
||||
)
|
||||
"
|
||||
@click="handleDownloadExcel"
|
||||
>
|
||||
{{ $t('pages.common.export') }}
|
||||
</a-button>
|
||||
@@ -243,8 +239,8 @@ const { hasAccessByCodes } = useAccess();
|
||||
</Space>
|
||||
</template>
|
||||
<template #avatar="{ row }">
|
||||
<Avatar v-if="row.avatar" :src="row.avatar" />
|
||||
<Avatar v-else :src="preferences.app.defaultAvatar" />
|
||||
<!-- 可能要判断空字符串情况 所以没有使用?? -->
|
||||
<Avatar :src="row.avatar || preferences.app.defaultAvatar" />
|
||||
</template>
|
||||
<template #status="{ row }">
|
||||
<TableSwitch
|
||||
@@ -304,7 +300,7 @@ const { hasAccessByCodes } = useAccess();
|
||||
</template>
|
||||
</BasicTable>
|
||||
</div>
|
||||
<UserImpotModal />
|
||||
<UserImpotModal @reload="tableApi.query()" />
|
||||
<UserDrawer @reload="tableApi.query()" />
|
||||
<UserInfoModal />
|
||||
<UserResetPwdModal />
|
||||
|
@@ -9,7 +9,7 @@ import { ExcelIcon, InBoxIcon } from '@vben/icons';
|
||||
import { Modal, Switch, Upload } from 'ant-design-vue';
|
||||
|
||||
import { downloadImportTemplate, userImportData } from '#/api/system/user';
|
||||
import { downloadExcel } from '#/utils/file/download';
|
||||
import { commonDownloadExcel } from '#/utils/file/download';
|
||||
|
||||
const emit = defineEmits<{ reload: [] }>();
|
||||
|
||||
@@ -89,7 +89,7 @@ function handleCancel() {
|
||||
<span>允许导入xlsx, xls文件</span>
|
||||
<a-button
|
||||
type="link"
|
||||
@click="downloadExcel(downloadImportTemplate, '用户导入模板')"
|
||||
@click="commonDownloadExcel(downloadImportTemplate, '用户导入模板')"
|
||||
>
|
||||
<div class="flex items-center gap-[4px]">
|
||||
<ExcelIcon />
|
||||
|
@@ -33,6 +33,11 @@ const gridOptions: VxeGridProps = {
|
||||
proxyConfig: {
|
||||
enabled: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: false,
|
||||
zoom: false,
|
||||
custom: false,
|
||||
},
|
||||
height: 'auto',
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
@@ -89,8 +94,10 @@ async function handleSubmit() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-[calc(100vh-165px)] flex-col gap-[16px] p-[12px]">
|
||||
<BasicTable />
|
||||
<div class="flex flex-col gap-[16px] p-[12px]">
|
||||
<div class="h-[calc(100vh-235px)] overflow-y-hidden">
|
||||
<BasicTable />
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
<Space>
|
||||
<a-button @click="toCurrentStep(0)">上一步</a-button>
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { nextTick, onMounted, ref } from 'vue';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { Page, useVbenModal, type VbenFormProps } from '@vben/common-ui';
|
||||
@@ -37,6 +37,14 @@ const formOptions: VbenFormProps = {
|
||||
},
|
||||
},
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
|
||||
// 日期选择格式化
|
||||
fieldMappingTime: [
|
||||
[
|
||||
'createTime',
|
||||
['params[beginTime]', 'params[endTime]'],
|
||||
['YYYY-MM-DD 00:00:00', 'YYYY-MM-DD 23:59:59'],
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
const gridOptions: VxeGridProps = {
|
||||
@@ -55,21 +63,6 @@ const gridOptions: VxeGridProps = {
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues = {}) => {
|
||||
// 区间选择器处理
|
||||
if (formValues?.createTime) {
|
||||
formValues.params = {
|
||||
beginTime: dayjs(formValues.createTime[0]).format(
|
||||
'YYYY-MM-DD 00:00:00',
|
||||
),
|
||||
endTime: dayjs(formValues.createTime[1]).format(
|
||||
'YYYY-MM-DD 23:59:59',
|
||||
),
|
||||
};
|
||||
Reflect.deleteProperty(formValues, 'createTime');
|
||||
} else {
|
||||
Reflect.deleteProperty(formValues, 'params');
|
||||
}
|
||||
|
||||
return await generatedList({
|
||||
pageNum: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
@@ -189,20 +182,8 @@ function handleMultiDelete() {
|
||||
});
|
||||
}
|
||||
|
||||
const init = ref(true);
|
||||
const [TableImportModal, tableImportModalApi] = useVbenModal({
|
||||
connectedComponent: tableImportModal,
|
||||
/**
|
||||
* TODO: 等待官方修复
|
||||
* 临时解决方案 for https://github.com/vbenjs/vue-vben-admin/issues/4752
|
||||
* 通过在关闭时使用v-if切换来造成Modal的重新渲染
|
||||
* 目前Modal逻辑为每次打开 内部的元素会重新mount 但是Modal本身不会重新渲染
|
||||
*/
|
||||
async onClosed() {
|
||||
init.value = false;
|
||||
await nextTick();
|
||||
init.value = true;
|
||||
},
|
||||
});
|
||||
|
||||
function handleImport() {
|
||||
@@ -305,6 +286,6 @@ function handleImport() {
|
||||
</template>
|
||||
</BasicTable>
|
||||
<CodePreviewModal />
|
||||
<TableImportModal v-if="init" @reload="tableApi.query()" />
|
||||
<TableImportModal @reload="tableApi.query()" />
|
||||
</Page>
|
||||
</template>
|
||||
|
@@ -84,6 +84,11 @@ const gridOptions: VxeGridProps = {
|
||||
isHover: true,
|
||||
keyField: 'tableId',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: false,
|
||||
zoom: false,
|
||||
custom: false,
|
||||
},
|
||||
};
|
||||
|
||||
const [BasicTable, tableApi] = useVbenVxeGrid({ formOptions, gridOptions });
|
||||
|
@@ -4,7 +4,7 @@ import type { Recordable } from '@vben/types';
|
||||
import { nextTick } from 'vue';
|
||||
|
||||
import { Page, useVbenModal, type VbenFormProps } from '@vben/common-ui';
|
||||
import { getVxePopupContainer, listToTree } from '@vben/utils';
|
||||
import { getVxePopupContainer } from '@vben/utils';
|
||||
|
||||
import { Popconfirm, Space } from 'ant-design-vue';
|
||||
|
||||
@@ -38,12 +38,7 @@ const gridOptions: VxeGridProps = {
|
||||
const resp = await categoryList({
|
||||
...formValues,
|
||||
});
|
||||
const treeData = listToTree(resp, {
|
||||
id: 'id',
|
||||
pid: 'parentId',
|
||||
children: 'children',
|
||||
});
|
||||
return { rows: treeData };
|
||||
return { rows: resp };
|
||||
},
|
||||
// 默认请求接口后展开全部 不需要可以删除这段
|
||||
querySuccess: () => {
|
||||
@@ -53,13 +48,20 @@ const gridOptions: VxeGridProps = {
|
||||
},
|
||||
},
|
||||
},
|
||||
/**
|
||||
* 虚拟滚动 默认关闭
|
||||
*/
|
||||
scrollY: {
|
||||
enabled: false,
|
||||
gt: 0,
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
treeConfig: {
|
||||
parentField: 'parentId',
|
||||
rowField: 'id',
|
||||
transform: false,
|
||||
transform: true,
|
||||
},
|
||||
// 表格全局唯一表示 保存列配置需要用到
|
||||
id: 'workflow-category-index',
|
||||
|
@@ -0,0 +1,61 @@
|
||||
<script setup lang="ts">
|
||||
import { Avatar, Descriptions, DescriptionsItem, Tag } from 'ant-design-vue';
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
endTime: string;
|
||||
startTime: string;
|
||||
title: string;
|
||||
desc: string;
|
||||
status: string;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<{ info: Props }>(), {});
|
||||
|
||||
const emit = defineEmits<{ click: [string] }>();
|
||||
|
||||
function handleClick() {
|
||||
emit('click', props.info.id);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="{
|
||||
'border-primary': info.active,
|
||||
'border-[2px]': info.active,
|
||||
}"
|
||||
class="cursor-pointer rounded-lg border-[1px] border-solid p-3 transition-shadow duration-300 ease-in-out hover:shadow-lg"
|
||||
@click.stop="handleClick"
|
||||
>
|
||||
<Descriptions :column="1" :title="info.title" size="middle">
|
||||
<template #extra>
|
||||
<Tag color="warning">审批中</Tag>
|
||||
</template>
|
||||
<DescriptionsItem label="描述">{{ info.desc }}</DescriptionsItem>
|
||||
<DescriptionsItem label="开始时间">{{ info.startTime }}</DescriptionsItem>
|
||||
<DescriptionsItem label="结束时间">{{ info.endTime }}</DescriptionsItem>
|
||||
</Descriptions>
|
||||
<div class="flex items-center justify-between text-[14px]">
|
||||
<div class="flex items-center gap-1">
|
||||
<Avatar
|
||||
size="small"
|
||||
src="https://plus.dapdap.top/minio-server/plus/2024/11/21/925ed278e2d441beb7f695b41e13c4dd.jpg"
|
||||
/>
|
||||
<span class="opacity-50">疯狂的牛子Li</span>
|
||||
</div>
|
||||
<div class="opacity-50">处理时间: 2022-01-01</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.ant-descriptions .ant-descriptions-header) {
|
||||
margin-bottom: 12px !important;
|
||||
}
|
||||
|
||||
:deep(.ant-descriptions-item) {
|
||||
padding-bottom: 8px !important;
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,50 @@
|
||||
<script setup lang="ts">
|
||||
import { Timeline, TimelineItem } from 'ant-design-vue';
|
||||
|
||||
/**
|
||||
* TODO: 仅为demo 后期会替换
|
||||
*/
|
||||
import { VbenAvatar } from '../../../../../../packages/@core/ui-kit/shadcn-ui/src/components';
|
||||
|
||||
interface ApprovalItem {
|
||||
id: string;
|
||||
name: string;
|
||||
status: string;
|
||||
remark?: string;
|
||||
time: string;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
list: ApprovalItem[];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Timeline>
|
||||
<TimelineItem v-for="item in props.list" :key="item.id">
|
||||
<template #dot>
|
||||
<div class="relative rounded-full border">
|
||||
<VbenAvatar
|
||||
class="size-[36px]"
|
||||
src="https://plus.dapdap.top/minio-server/plus/2024/11/21/925ed278e2d441beb7f695b41e13c4dd.jpg"
|
||||
/>
|
||||
<div
|
||||
class="border-background absolute bottom-0 right-0 size-[16px] rounded-full border-2 bg-green-500 content-['']"
|
||||
>
|
||||
<div class="flex items-center justify-center">
|
||||
<span class="icon-[mdi--success-bold] text-white"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="ml-2 flex flex-col">
|
||||
<div>发起人</div>
|
||||
<div>疯狂的牛子Li</div>
|
||||
<div>2022-01-01 12:00:00</div>
|
||||
<div class="rounded-lg border p-1">
|
||||
<span class="opacity-70">这里是备注信息</span>
|
||||
</div>
|
||||
</div>
|
||||
</TimelineItem>
|
||||
</Timeline>
|
||||
</template>
|
2
apps/web-antd/src/views/workflow/components/index.ts
Normal file
2
apps/web-antd/src/views/workflow/components/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as ApprovalCard } from './approval-card.vue';
|
||||
export { default as ApprovalTimeline } from './approval-timeline.vue';
|
BIN
apps/web-antd/src/views/workflow/components/rejection.png
Normal file
BIN
apps/web-antd/src/views/workflow/components/rejection.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
@@ -1,9 +1,172 @@
|
||||
<script setup lang="ts">
|
||||
import CommonSkeleton from '#/views/common';
|
||||
import { reactive, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import {
|
||||
Alert,
|
||||
Avatar,
|
||||
Card,
|
||||
Divider,
|
||||
InputSearch,
|
||||
Space,
|
||||
TabPane,
|
||||
Tabs,
|
||||
Tag,
|
||||
} from 'ant-design-vue';
|
||||
import { debounce, uniqueId } from 'lodash-es';
|
||||
|
||||
import { ApprovalCard, ApprovalTimeline } from '../components';
|
||||
import RejectionPng from '../components/rejection.png';
|
||||
|
||||
const handleScroll = debounce((e: Event) => {
|
||||
if (!e.target) {
|
||||
return;
|
||||
}
|
||||
// e.target.scrollTop 是元素顶部到当前可视区域顶部的距离,即已滚动的高度。
|
||||
// e.target.clientHeight 是元素的可视高度。
|
||||
// e.target.scrollHeight 是元素的总高度。
|
||||
const { scrollTop, clientHeight, scrollHeight } = e.target as HTMLElement;
|
||||
// 判断是否滚动到底部
|
||||
const isBottom = scrollTop + clientHeight >= scrollHeight;
|
||||
console.log(isBottom);
|
||||
// console.log(scrollTop + clientHeight);
|
||||
// console.log(scrollHeight);
|
||||
}, 200);
|
||||
|
||||
const data = reactive(
|
||||
Array.from({ length: 10 }).map(() => ({
|
||||
id: uniqueId(),
|
||||
startTime: '2022-01-01',
|
||||
endTime: '2022-01-02',
|
||||
title: '审批任务',
|
||||
desc: '审批任务描述',
|
||||
status: '审批中',
|
||||
active: false,
|
||||
})),
|
||||
);
|
||||
|
||||
const timeLine = Array.from({ length: 5 }).map(() => ({
|
||||
id: uniqueId(),
|
||||
name: '张三',
|
||||
status: '审批中',
|
||||
remark: '审批任务描述',
|
||||
time: '2022-01-01',
|
||||
}));
|
||||
|
||||
const lastSelectId = ref('');
|
||||
function handleCardClick(id: string) {
|
||||
// 点击的是同一个
|
||||
if (lastSelectId.value === id) {
|
||||
return;
|
||||
}
|
||||
// 反选状态 & 如果已经点击了 不变 & 保持只能有一个选中
|
||||
data.forEach((item) => {
|
||||
item.active = item.id === id;
|
||||
});
|
||||
lastSelectId.value = id;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<CommonSkeleton />
|
||||
</div>
|
||||
<Page :auto-content-height="true">
|
||||
<div class="flex h-full gap-2">
|
||||
<div class="bg-background flex h-full w-[320px] flex-col rounded-lg">
|
||||
<!-- 搜索条件 -->
|
||||
<div
|
||||
class="bg-background z-100 sticky left-0 top-0 w-full rounded-t-lg border-b-[1px] border-solid p-2"
|
||||
>
|
||||
<InputSearch placeholder="搜索" />
|
||||
</div>
|
||||
<div
|
||||
class="thin-scrollbar flex flex-1 flex-col gap-2 overflow-y-auto py-3"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<ApprovalCard
|
||||
v-for="item in data"
|
||||
:key="item.id"
|
||||
:info="item"
|
||||
class="mx-2"
|
||||
@click="handleCardClick"
|
||||
/>
|
||||
</div>
|
||||
<!-- total显示 -->
|
||||
<div
|
||||
class="bg-background sticky bottom-0 w-full rounded-b-lg border-t-[1px] py-2"
|
||||
>
|
||||
<div class="flex items-center justify-center">
|
||||
共 {{ data.length }} 条记录
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Card
|
||||
:body-style="{ overflowY: 'auto', height: '100%' }"
|
||||
class="thin-scrollbar flex-1 overflow-y-hidden"
|
||||
size="small"
|
||||
title="编号: 1234567890123456789012"
|
||||
>
|
||||
<div class="flex flex-col gap-5 p-4">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="text-2xl font-bold">报销申请</div>
|
||||
<div>
|
||||
<Tag color="warning">申请中</Tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Avatar
|
||||
size="small"
|
||||
src="https://plus.dapdap.top/minio-server/plus/2024/11/21/925ed278e2d441beb7f695b41e13c4dd.jpg"
|
||||
/>
|
||||
<span>疯狂的牛子Li</span>
|
||||
<div class="flex items-center opacity-50">
|
||||
<span>XXXX有限公司</span>
|
||||
<Divider type="vertical" />
|
||||
<span>提交于: 2022-01-01 12:00:00</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右侧图标 -->
|
||||
<div class="z-100 absolute right-3 top-3">
|
||||
<img :src="RejectionPng" class="size-[96px]" />
|
||||
</div>
|
||||
</div>
|
||||
<Tabs class="flex-1">
|
||||
<TabPane key="1" tab="审批详情">
|
||||
<div class="h-fulloverflow-y-auto">
|
||||
<Alert message="该页面仅为静态页 后期可能会用到!" type="info" />
|
||||
<Divider />
|
||||
<ApprovalTimeline :list="timeLine" />
|
||||
</div>
|
||||
</TabPane>
|
||||
<TabPane key="2" tab="审批记录">审批记录</TabPane>
|
||||
<TabPane key="3" tab="全文评论(999+)">全文评论</TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
<!-- 固定底部 -->
|
||||
<div
|
||||
class="border-t-solid bg-background absolute bottom-0 left-0 w-full border-t-[1px] p-3"
|
||||
>
|
||||
<div class="flex justify-end">
|
||||
<Space>
|
||||
<a-button type="primary">通过</a-button>
|
||||
<a-button danger type="primary">驳回</a-button>
|
||||
<a-button>其他</a-button>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.thin-scrollbar {
|
||||
&::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-card-body) {
|
||||
@apply thin-scrollbar;
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/web-ele",
|
||||
"version": "5.4.4",
|
||||
"version": "5.4.8",
|
||||
"homepage": "https://vben.pro",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
|
@@ -15,6 +15,7 @@ import {
|
||||
ElButton,
|
||||
ElCheckbox,
|
||||
ElCheckboxGroup,
|
||||
ElDatePicker,
|
||||
ElDivider,
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
@@ -64,7 +65,7 @@ async function initComponentAdapter() {
|
||||
Checkbox: ElCheckbox,
|
||||
CheckboxGroup: ElCheckboxGroup,
|
||||
// 自定义默认按钮
|
||||
DefaulButton: (props, { attrs, slots }) => {
|
||||
DefaultButton: (props, { attrs, slots }) => {
|
||||
return h(ElButton, { ...props, attrs, type: 'info' }, slots);
|
||||
},
|
||||
// 自定义主要按钮
|
||||
@@ -79,6 +80,7 @@ async function initComponentAdapter() {
|
||||
Space: ElSpace,
|
||||
Switch: ElSwitch,
|
||||
TimePicker: ElTimePicker,
|
||||
DatePicker: ElDatePicker,
|
||||
TreeSelect: withDefaultPlaceholder(ElTreeSelect, 'select'),
|
||||
Upload: ElUpload,
|
||||
};
|
||||
|
@@ -17,12 +17,12 @@ const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
|
||||
|
||||
/** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统(不会显示在菜单中) */
|
||||
// const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
|
||||
/** 不需要权限的菜单列表(会显示在菜单中) */
|
||||
// const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
|
||||
const staticRoutes: RouteRecordRaw[] = [];
|
||||
const externalRoutes: RouteRecordRaw[] = [];
|
||||
|
||||
/** 路由列表,由基本路由+静态路由组成 */
|
||||
/** 路由列表,由基本路由、外部路由和404兜底路由组成
|
||||
* 无需走权限验证(会一直显示在菜单中) */
|
||||
const routes: RouteRecordRaw[] = [
|
||||
...coreRoutes,
|
||||
...externalRoutes,
|
||||
@@ -32,5 +32,6 @@ const routes: RouteRecordRaw[] = [
|
||||
/** 基本路由列表,这些路由不需要进入权限拦截 */
|
||||
const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
|
||||
|
||||
/** 有权限校验的路由列表,包含动态路由和静态路由 */
|
||||
const accessRoutes = [...dynamicRoutes, ...staticRoutes];
|
||||
export { accessRoutes, coreRouteNames, routes };
|
||||
|
@@ -7,6 +7,7 @@ import type {
|
||||
} from '@vben/common-ui';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import {
|
||||
AnalysisChartCard,
|
||||
@@ -18,11 +19,15 @@ import {
|
||||
} from '@vben/common-ui';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { useUserStore } from '@vben/stores';
|
||||
import { openWindow } from '@vben/utils';
|
||||
|
||||
import AnalyticsVisitsSource from '../analytics/analytics-visits-source.vue';
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
// 这是一个示例数据,实际项目中需要根据实际情况进行调整
|
||||
// url 也可以是内部路由,在 navTo 方法中识别处理,进行内部跳转
|
||||
// 例如:url: /dashboard/workspace
|
||||
const projectItems: WorkbenchProjectItem[] = [
|
||||
{
|
||||
color: '',
|
||||
@@ -31,6 +36,7 @@ const projectItems: WorkbenchProjectItem[] = [
|
||||
group: '开源组',
|
||||
icon: 'carbon:logo-github',
|
||||
title: 'Github',
|
||||
url: 'https://github.com',
|
||||
},
|
||||
{
|
||||
color: '#3fb27f',
|
||||
@@ -39,6 +45,7 @@ const projectItems: WorkbenchProjectItem[] = [
|
||||
group: '算法组',
|
||||
icon: 'ion:logo-vue',
|
||||
title: 'Vue',
|
||||
url: 'https://vuejs.org',
|
||||
},
|
||||
{
|
||||
color: '#e18525',
|
||||
@@ -47,6 +54,7 @@ const projectItems: WorkbenchProjectItem[] = [
|
||||
group: '上班摸鱼',
|
||||
icon: 'ion:logo-html5',
|
||||
title: 'Html5',
|
||||
url: 'https://developer.mozilla.org/zh-CN/docs/Web/HTML',
|
||||
},
|
||||
{
|
||||
color: '#bf0c2c',
|
||||
@@ -55,6 +63,7 @@ const projectItems: WorkbenchProjectItem[] = [
|
||||
group: 'UI',
|
||||
icon: 'ion:logo-angular',
|
||||
title: 'Angular',
|
||||
url: 'https://angular.io',
|
||||
},
|
||||
{
|
||||
color: '#00d8ff',
|
||||
@@ -63,6 +72,7 @@ const projectItems: WorkbenchProjectItem[] = [
|
||||
group: '技术牛',
|
||||
icon: 'bx:bxl-react',
|
||||
title: 'React',
|
||||
url: 'https://reactjs.org',
|
||||
},
|
||||
{
|
||||
color: '#EBD94E',
|
||||
@@ -71,39 +81,47 @@ const projectItems: WorkbenchProjectItem[] = [
|
||||
group: '架构组',
|
||||
icon: 'ion:logo-javascript',
|
||||
title: 'Js',
|
||||
url: 'https://developer.mozilla.org/zh-CN/docs/Web/JavaScript',
|
||||
},
|
||||
];
|
||||
|
||||
// 同样,这里的 url 也可以使用以 http 开头的外部链接
|
||||
const quickNavItems: WorkbenchQuickNavItem[] = [
|
||||
{
|
||||
color: '#1fdaca',
|
||||
icon: 'ion:home-outline',
|
||||
title: '首页',
|
||||
url: '/',
|
||||
},
|
||||
{
|
||||
color: '#bf0c2c',
|
||||
icon: 'ion:grid-outline',
|
||||
title: '仪表盘',
|
||||
url: '/dashboard',
|
||||
},
|
||||
{
|
||||
color: '#e18525',
|
||||
icon: 'ion:layers-outline',
|
||||
title: '组件',
|
||||
url: '/demos/features/icons',
|
||||
},
|
||||
{
|
||||
color: '#3fb27f',
|
||||
icon: 'ion:settings-outline',
|
||||
title: '系统管理',
|
||||
url: '/demos/features/login-expired', // 这里的 URL 是示例,实际项目中需要根据实际情况进行调整
|
||||
},
|
||||
{
|
||||
color: '#4daf1bc9',
|
||||
icon: 'ion:key-outline',
|
||||
title: '权限管理',
|
||||
url: '/demos/access/page-control',
|
||||
},
|
||||
{
|
||||
color: '#00d8ff',
|
||||
icon: 'ion:bar-chart-outline',
|
||||
title: '图表',
|
||||
url: '/analytics',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -195,6 +213,24 @@ const trendItems: WorkbenchTrendItem[] = [
|
||||
title: 'Vben',
|
||||
},
|
||||
];
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// 这是一个示例方法,实际项目中需要根据实际情况进行调整
|
||||
// This is a sample method, adjust according to the actual project requirements
|
||||
function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
|
||||
if (nav.url?.startsWith('http')) {
|
||||
openWindow(nav.url);
|
||||
return;
|
||||
}
|
||||
if (nav.url?.startsWith('/')) {
|
||||
router.push(nav.url).catch((error) => {
|
||||
console.error('Navigation failed:', error);
|
||||
});
|
||||
} else {
|
||||
console.warn(`Unknown URL for navigation item: ${nav.title} -> ${nav.url}`);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -210,7 +246,7 @@ const trendItems: WorkbenchTrendItem[] = [
|
||||
|
||||
<div class="mt-5 flex flex-col lg:flex-row">
|
||||
<div class="mr-4 w-full lg:w-3/5">
|
||||
<WorkbenchProject :items="projectItems" title="项目" />
|
||||
<WorkbenchProject :items="projectItems" title="项目" @click="navTo" />
|
||||
<WorkbenchTrends :items="trendItems" class="mt-5" title="最新动态" />
|
||||
</div>
|
||||
<div class="w-full lg:w-2/5">
|
||||
@@ -218,6 +254,7 @@ const trendItems: WorkbenchTrendItem[] = [
|
||||
:items="quickNavItems"
|
||||
class="mt-5 lg:mt-0"
|
||||
title="快捷导航"
|
||||
@click="navTo"
|
||||
/>
|
||||
<WorkbenchTodo :items="todoItems" class="mt-5" title="待办事项" />
|
||||
<AnalysisChartCard class="mt-5" title="访问来源">
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/web-naive",
|
||||
"version": "5.4.4",
|
||||
"version": "5.4.8",
|
||||
"homepage": "https://vben.pro",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
|
@@ -17,12 +17,12 @@ const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
|
||||
|
||||
/** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统(不会显示在菜单中) */
|
||||
// const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
|
||||
/** 不需要权限的菜单列表(会显示在菜单中) */
|
||||
// const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
|
||||
const staticRoutes: RouteRecordRaw[] = [];
|
||||
const externalRoutes: RouteRecordRaw[] = [];
|
||||
|
||||
/** 路由列表,由基本路由+静态路由组成 */
|
||||
/** 路由列表,由基本路由、外部路由和404兜底路由组成
|
||||
* 无需走权限验证(会一直显示在菜单中) */
|
||||
const routes: RouteRecordRaw[] = [
|
||||
...coreRoutes,
|
||||
...externalRoutes,
|
||||
@@ -32,5 +32,6 @@ const routes: RouteRecordRaw[] = [
|
||||
/** 基本路由列表,这些路由不需要进入权限拦截 */
|
||||
const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
|
||||
|
||||
/** 有权限校验的路由列表,包含动态路由和静态路由 */
|
||||
const accessRoutes = [...dynamicRoutes, ...staticRoutes];
|
||||
export { accessRoutes, coreRouteNames, routes };
|
||||
|
@@ -7,6 +7,7 @@ import type {
|
||||
} from '@vben/common-ui';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import {
|
||||
AnalysisChartCard,
|
||||
@@ -18,11 +19,15 @@ import {
|
||||
} from '@vben/common-ui';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { useUserStore } from '@vben/stores';
|
||||
import { openWindow } from '@vben/utils';
|
||||
|
||||
import AnalyticsVisitsSource from '../analytics/analytics-visits-source.vue';
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
// 这是一个示例数据,实际项目中需要根据实际情况进行调整
|
||||
// url 也可以是内部路由,在 navTo 方法中识别处理,进行内部跳转
|
||||
// 例如:url: /dashboard/workspace
|
||||
const projectItems: WorkbenchProjectItem[] = [
|
||||
{
|
||||
color: '',
|
||||
@@ -31,6 +36,7 @@ const projectItems: WorkbenchProjectItem[] = [
|
||||
group: '开源组',
|
||||
icon: 'carbon:logo-github',
|
||||
title: 'Github',
|
||||
url: 'https://github.com',
|
||||
},
|
||||
{
|
||||
color: '#3fb27f',
|
||||
@@ -39,6 +45,7 @@ const projectItems: WorkbenchProjectItem[] = [
|
||||
group: '算法组',
|
||||
icon: 'ion:logo-vue',
|
||||
title: 'Vue',
|
||||
url: 'https://vuejs.org',
|
||||
},
|
||||
{
|
||||
color: '#e18525',
|
||||
@@ -47,6 +54,7 @@ const projectItems: WorkbenchProjectItem[] = [
|
||||
group: '上班摸鱼',
|
||||
icon: 'ion:logo-html5',
|
||||
title: 'Html5',
|
||||
url: 'https://developer.mozilla.org/zh-CN/docs/Web/HTML',
|
||||
},
|
||||
{
|
||||
color: '#bf0c2c',
|
||||
@@ -55,6 +63,7 @@ const projectItems: WorkbenchProjectItem[] = [
|
||||
group: 'UI',
|
||||
icon: 'ion:logo-angular',
|
||||
title: 'Angular',
|
||||
url: 'https://angular.io',
|
||||
},
|
||||
{
|
||||
color: '#00d8ff',
|
||||
@@ -63,6 +72,7 @@ const projectItems: WorkbenchProjectItem[] = [
|
||||
group: '技术牛',
|
||||
icon: 'bx:bxl-react',
|
||||
title: 'React',
|
||||
url: 'https://reactjs.org',
|
||||
},
|
||||
{
|
||||
color: '#EBD94E',
|
||||
@@ -71,39 +81,47 @@ const projectItems: WorkbenchProjectItem[] = [
|
||||
group: '架构组',
|
||||
icon: 'ion:logo-javascript',
|
||||
title: 'Js',
|
||||
url: 'https://developer.mozilla.org/zh-CN/docs/Web/JavaScript',
|
||||
},
|
||||
];
|
||||
|
||||
// 同样,这里的 url 也可以使用以 http 开头的外部链接
|
||||
const quickNavItems: WorkbenchQuickNavItem[] = [
|
||||
{
|
||||
color: '#1fdaca',
|
||||
icon: 'ion:home-outline',
|
||||
title: '首页',
|
||||
url: '/',
|
||||
},
|
||||
{
|
||||
color: '#bf0c2c',
|
||||
icon: 'ion:grid-outline',
|
||||
title: '仪表盘',
|
||||
url: '/dashboard',
|
||||
},
|
||||
{
|
||||
color: '#e18525',
|
||||
icon: 'ion:layers-outline',
|
||||
title: '组件',
|
||||
url: '/demos/features/icons',
|
||||
},
|
||||
{
|
||||
color: '#3fb27f',
|
||||
icon: 'ion:settings-outline',
|
||||
title: '系统管理',
|
||||
url: '/demos/features/login-expired', // 这里的 URL 是示例,实际项目中需要根据实际情况进行调整
|
||||
},
|
||||
{
|
||||
color: '#4daf1bc9',
|
||||
icon: 'ion:key-outline',
|
||||
title: '权限管理',
|
||||
url: '/demos/access/page-control',
|
||||
},
|
||||
{
|
||||
color: '#00d8ff',
|
||||
icon: 'ion:bar-chart-outline',
|
||||
title: '图表',
|
||||
url: '/analytics',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -195,6 +213,24 @@ const trendItems: WorkbenchTrendItem[] = [
|
||||
title: 'Vben',
|
||||
},
|
||||
];
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// 这是一个示例方法,实际项目中需要根据实际情况进行调整
|
||||
// This is a sample method, adjust according to the actual project requirements
|
||||
function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
|
||||
if (nav.url?.startsWith('http')) {
|
||||
openWindow(nav.url);
|
||||
return;
|
||||
}
|
||||
if (nav.url?.startsWith('/')) {
|
||||
router.push(nav.url).catch((error) => {
|
||||
console.error('Navigation failed:', error);
|
||||
});
|
||||
} else {
|
||||
console.warn(`Unknown URL for navigation item: ${nav.title} -> ${nav.url}`);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -210,7 +246,7 @@ const trendItems: WorkbenchTrendItem[] = [
|
||||
|
||||
<div class="mt-5 flex flex-col lg:flex-row">
|
||||
<div class="mr-4 w-full lg:w-3/5">
|
||||
<WorkbenchProject :items="projectItems" title="项目" />
|
||||
<WorkbenchProject :items="projectItems" title="项目" @click="navTo" />
|
||||
<WorkbenchTrends :items="trendItems" class="mt-5" title="最新动态" />
|
||||
</div>
|
||||
<div class="w-full lg:w-2/5">
|
||||
@@ -218,6 +254,7 @@ const trendItems: WorkbenchTrendItem[] = [
|
||||
:items="quickNavItems"
|
||||
class="mt-5 lg:mt-0"
|
||||
title="快捷导航"
|
||||
@click="navTo"
|
||||
/>
|
||||
<WorkbenchTodo :items="todoItems" class="mt-5" title="待办事项" />
|
||||
<AnalysisChartCard class="mt-5" title="访问来源">
|
||||
|
@@ -25,9 +25,12 @@
|
||||
"lockb",
|
||||
"logininfor",
|
||||
"lucide",
|
||||
"minh",
|
||||
"minw",
|
||||
"mkdist",
|
||||
"mockjs",
|
||||
"vitejs",
|
||||
"naiveui",
|
||||
"nocheck",
|
||||
"noopener",
|
||||
"noreferrer",
|
||||
"nprogress",
|
||||
@@ -51,6 +54,7 @@
|
||||
"vben",
|
||||
"vbenjs",
|
||||
"vite",
|
||||
"vitejs",
|
||||
"vitepress",
|
||||
"vnode",
|
||||
"vueuse",
|
||||
|
@@ -221,9 +221,9 @@ function nav(): DefaultTheme.NavItem[] {
|
||||
link: '/commercial/community',
|
||||
text: '👨👦👦 Community',
|
||||
},
|
||||
{
|
||||
link: '/friend-links/',
|
||||
text: '🤝 Friend Links',
|
||||
},
|
||||
// {
|
||||
// link: '/friend-links/',
|
||||
// text: '🤝 Friend Links',
|
||||
// },
|
||||
];
|
||||
}
|
||||
|
@@ -62,6 +62,11 @@ export const shared = defineConfig({
|
||||
postcssIsolateStyles({ includeFiles: [/vp-doc\.css/] }),
|
||||
],
|
||||
},
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
api: 'modern',
|
||||
},
|
||||
},
|
||||
},
|
||||
json: {
|
||||
stringify: true,
|
||||
@@ -97,6 +102,7 @@ export const shared = defineConfig({
|
||||
host: true,
|
||||
port: 6173,
|
||||
},
|
||||
|
||||
ssr: {
|
||||
external: ['@vue/repl'],
|
||||
},
|
||||
|
@@ -124,7 +124,7 @@ function sidebarCommercial(): DefaultTheme.SidebarItem[] {
|
||||
return [
|
||||
{
|
||||
link: 'community',
|
||||
text: '社区',
|
||||
text: '交流群',
|
||||
},
|
||||
{
|
||||
link: 'technical-support',
|
||||
@@ -266,7 +266,7 @@ function nav(): DefaultTheme.NavItem[] {
|
||||
},
|
||||
{
|
||||
link: '/commercial/community',
|
||||
text: '👨👦👦 社区',
|
||||
text: '👨👦👦 交流群',
|
||||
// items: [
|
||||
// {
|
||||
// link: 'https://qun.qq.com/qqweb/qunpro/share?_wv=3&_wwv=128&appChannel=share&inviteCode=22ySzj7pKiw&businessType=9&from=246610&biz=ka&mainSourceId=share&subSourceId=others&jumpsource=shorturl#/pc',
|
||||
@@ -282,10 +282,10 @@ function nav(): DefaultTheme.NavItem[] {
|
||||
// },
|
||||
// ],
|
||||
},
|
||||
{
|
||||
link: '/friend-links/',
|
||||
text: '🤝 友情链接',
|
||||
},
|
||||
// {
|
||||
// link: '/friend-links/',
|
||||
// text: '🤝 友情链接',
|
||||
// },
|
||||
];
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/docs",
|
||||
"version": "5.4.4",
|
||||
"version": "5.4.8",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "vitepress build",
|
||||
|
@@ -20,7 +20,10 @@
|
||||
|
||||
::: tip
|
||||
|
||||
因为微信群人数有限制,加微信群前,你可以通过[赞助](../sponsor/personal.md)任意金额,主动发送截图给作者,备注`加入微信群`即可。
|
||||
因为微信群人数有限制,加微信群要求:
|
||||
|
||||
- 通过[赞助](../sponsor/personal.md)任意金额。
|
||||
- 发送赞助`截图`,备注`加入微信群`即可。
|
||||
|
||||
:::
|
||||
|
||||
|
@@ -280,6 +280,7 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
|
||||
| 方法名 | 描述 | 类型 |
|
||||
| --- | --- | --- |
|
||||
| submitForm | 提交表单 | `(e:Event)=>Promise<Record<string,any>>` |
|
||||
| validateAndSubmitForm | 提交并校验表单 | `(e:Event)=>Promise<Record<string,any>>` |
|
||||
| resetForm | 重置表单 | `()=>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>` |
|
||||
@@ -309,6 +310,7 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
|
||||
| collapsed | 是否折叠,在`是否展开,在showCollapseButton=true`时生效 | `boolean` | `false` |
|
||||
| collapseTriggerResize | 折叠时,触发`resize`事件 | `boolean` | `false` |
|
||||
| collapsedRows | 折叠时保持的行数 | `number` | `1` |
|
||||
| fieldMappingTime | 用于将表单内时间区域的应设成 2 个字段 | `[string, [string, string], string?][]` | - |
|
||||
| commonConfig | 表单项的通用配置,每个配置都会传递到每个表单项,表单项可覆盖 | `FormCommonConfig` | - |
|
||||
| schema | 表单项的每一项配置 | `FormSchema` | - |
|
||||
| submitOnEnter | 按下回车健时提交表单 | `boolean` | false |
|
||||
@@ -320,7 +322,7 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
|
||||
```ts
|
||||
export interface ActionButtonOptions {
|
||||
/** 样式 */
|
||||
class?: any;
|
||||
class?: ClassType;
|
||||
/** 是否禁用 */
|
||||
disabled?: boolean;
|
||||
/** 是否加载中 */
|
||||
|
@@ -2,42 +2,66 @@
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# Routing and Menus
|
||||
# Routes and Menus
|
||||
|
||||
In the project, the framework provides a basic routing system and **automatically generates the corresponding menu structure based on the routing file**.
|
||||
::: info
|
||||
|
||||
## Route Types
|
||||
This page is translated by machine translation and may not be very accurate.
|
||||
|
||||
Routes are divided into static routes and dynamic routes. Static routes are routes that have been determined when the project starts. Dynamic routes are generally routes that are dynamically generated based on the user's permissions after the user logs in.
|
||||
:::
|
||||
|
||||
In the project, the framework provides a basic routing system and **automatically generates the corresponding menu structure based on the routing files**.
|
||||
|
||||
## Types of Routes
|
||||
|
||||
Routes are divided into core routes, static routes, and dynamic routes. Core routes are built-in routes of the framework, including root routes, login routes, 404 routes, etc.; static routes are routes that are determined when the project starts; dynamic routes are generally generated dynamically based on the user's permissions after the user logs in.
|
||||
|
||||
Both static and dynamic routes go through permission control, which can be controlled by configuring the `authority` field in the `meta` property of the route.
|
||||
|
||||
### Core Routes
|
||||
|
||||
Core routes are built-in routes of the framework, including root routes, login routes, 404 routes, etc. The configuration of core routes is in the `src/router/routes/core` directory under the application.
|
||||
|
||||
::: tip
|
||||
|
||||
Core routes are mainly used for the basic functions of the framework, so it is not recommended to put business-related routes in core routes. It is recommended to put business-related routes in static or dynamic routes.
|
||||
|
||||
:::
|
||||
|
||||
### Static Routes
|
||||
|
||||
If your page project does not require permission control, you can directly use static routes. The configuration of static routes is in the `src/router/routes/index` directory under the application. Open the commented file content:
|
||||
The configuration of static routes is in the `src/router/routes/index` directory under the application. Open the commented file content:
|
||||
|
||||
::: tip
|
||||
|
||||
Permission control is controlled by the `authority` field in the `meta` property of the route. If your page project does not require permission control, you can omit the `authority` field.
|
||||
|
||||
:::
|
||||
|
||||
```ts
|
||||
// If necessary, you can open your own comments and create folders
|
||||
// Uncomment if needed and create the folder
|
||||
// const externalRouteFiles = import.meta.glob('./external/**/*.ts', { eager: true }); // [!code --]
|
||||
const staticRouteFiles = import.meta.glob('./static/**/*.ts', { eager: true }); // [!code ++]
|
||||
/** Dynamic routing */
|
||||
/** Dynamic routes */
|
||||
const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
|
||||
|
||||
/** External routing lists, which can be accessed without Layout, may be used for embedding in other systems */
|
||||
/** External route list, these pages can be accessed without Layout, possibly used for embedding in other systems */
|
||||
// const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles) // [!code --]
|
||||
const externalRoutes: RouteRecordRaw[] = []; // [!code --]
|
||||
const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles); // [!code ++]
|
||||
```
|
||||
|
||||
### Dynamic routing
|
||||
### Dynamic Routes
|
||||
|
||||
The configuration of dynamic routing is in the corresponding application `src/router/routes/modules` directory. All routing files are stored in this directory. The content format of each file is as follows, which is consistent with the routing configuration format of Vue Router. The following is the configuration of secondary routes and multi-level routes.
|
||||
The configuration of dynamic routes is in the `src/router/routes/modules` directory under the corresponding application. This directory contains all the route files. The content format of each file is consistent with the Vue Router route configuration format. Below is the configuration of secondary and multi-level routes.
|
||||
|
||||
## Define the route
|
||||
## Route Definition
|
||||
|
||||
Static routes and dynamic routes are configured in the same way. The configuration of the level-2 and multi-level routes is as follows:
|
||||
The configuration method of static routes and dynamic routes is the same. Below is the configuration of secondary and multi-level routes:
|
||||
|
||||
### Secondary route
|
||||
### Secondary Routes
|
||||
|
||||
::: details Example code of the secondary route
|
||||
::: details Secondary Route Example Code
|
||||
|
||||
```ts
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
@@ -81,17 +105,16 @@ export default routes;
|
||||
|
||||
:::
|
||||
|
||||
### Multilevel routing
|
||||
### Multi-level Routes
|
||||
|
||||
::: tip
|
||||
|
||||
- The parent route of multi-level routing does not need to set the 'component' attribute, only the 'children' attribute needs to be set. Unless you really need to display content under nested parent routing.
|
||||
|
||||
- If there are no special circumstances, the 'redirect' attribute of the parent route does not need to be specified and will default to the first child route.
|
||||
- The parent route of multi-level routes does not need to set the `component` property, just set the `children` property. Unless you really need to display content nested under the parent route.
|
||||
- In most cases, the `redirect` property of the parent route does not need to be specified, it will default to the first child route.
|
||||
|
||||
:::
|
||||
|
||||
::: details Multilevel Routing Example Code
|
||||
::: details Multi-level Route Example Code
|
||||
|
||||
```ts
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
@@ -112,7 +135,7 @@ const routes: RouteRecordRaw[] = [
|
||||
path: '/demos',
|
||||
redirect: '/demos/access',
|
||||
children: [
|
||||
// 嵌套菜单
|
||||
// Nested menu
|
||||
{
|
||||
meta: {
|
||||
icon: 'ic:round-menu',
|
||||
@@ -208,13 +231,13 @@ export default routes;
|
||||
|
||||
:::
|
||||
|
||||
## Add a New Page
|
||||
## Adding a New Page
|
||||
|
||||
To add a new page, you only need to add a route and the corresponding page component.
|
||||
|
||||
### Add a Route
|
||||
### Adding a Route
|
||||
|
||||
Add a route object in the corresponding routing file as follows:
|
||||
Add a route object in the corresponding route file, as follows:
|
||||
|
||||
```ts
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
@@ -251,9 +274,9 @@ const routes: RouteRecordRaw[] = [
|
||||
export default routes;
|
||||
```
|
||||
|
||||
### Add Page Component
|
||||
### Adding a Page Component
|
||||
|
||||
In `#/views/home/`, add a new `index.vue` file as follows:
|
||||
In `#/views/home/`, add a new `index.vue` file, as follows:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
@@ -265,11 +288,11 @@ In `#/views/home/`, add a new `index.vue` file as follows:
|
||||
|
||||
### Verification
|
||||
|
||||
At this point, the page has been added. Access `http://localhost:5555/home/index` to see the corresponding page.
|
||||
At this point, the page has been added. Visit `http://localhost:5555/home/index` to see the corresponding page.
|
||||
|
||||
## Route Configuration
|
||||
|
||||
The route configuration mainly resides in the `meta` attribute of the route object. Below are some commonly used configuration items:
|
||||
The route configuration items are mainly in the `meta` property of the route object. The following are common configuration items:
|
||||
|
||||
```ts {5-8}
|
||||
const routes = [
|
||||
@@ -293,22 +316,21 @@ interface RouteMeta {
|
||||
*/
|
||||
activeIcon?: string;
|
||||
/**
|
||||
* The currently active menu, used when you want to activate a parent menu instead of the existing one
|
||||
* @default false
|
||||
* The currently active menu, sometimes you don't want to activate the existing menu, use this to activate the parent menu
|
||||
*/
|
||||
activePath?: string;
|
||||
/**
|
||||
* Whether to affix the tab
|
||||
* Whether to fix the tab
|
||||
* @default false
|
||||
*/
|
||||
affixTab?: boolean;
|
||||
/**
|
||||
* The order of the affixed tab
|
||||
* The order of fixed tabs
|
||||
* @default 0
|
||||
*/
|
||||
affixTabOrder?: number;
|
||||
/**
|
||||
* Specific role identifiers required for access
|
||||
* Specific roles required to access
|
||||
* @default []
|
||||
*/
|
||||
authority?: string[];
|
||||
@@ -331,22 +353,22 @@ interface RouteMeta {
|
||||
| 'warning'
|
||||
| string;
|
||||
/**
|
||||
* Children of the current route do not show in the menu
|
||||
* The children of the current route are not displayed in the menu
|
||||
* @default false
|
||||
*/
|
||||
hideChildrenInMenu?: boolean;
|
||||
/**
|
||||
* The current route does not show in the breadcrumb
|
||||
* The current route is not displayed in the breadcrumb
|
||||
* @default false
|
||||
*/
|
||||
hideInBreadcrumb?: boolean;
|
||||
/**
|
||||
* The current route does not show in the menu
|
||||
* The current route is not displayed in the menu
|
||||
* @default false
|
||||
*/
|
||||
hideInMenu?: boolean;
|
||||
/**
|
||||
* The current route does not show in tabs
|
||||
* The current route is not displayed in the tab
|
||||
* @default false
|
||||
*/
|
||||
hideInTab?: boolean;
|
||||
@@ -359,16 +381,16 @@ interface RouteMeta {
|
||||
*/
|
||||
iframeSrc?: string;
|
||||
/**
|
||||
* Ignore access, can be accessed directly
|
||||
* Ignore permissions, can be accessed directly
|
||||
* @default false
|
||||
*/
|
||||
ignoreAccess?: boolean;
|
||||
/**
|
||||
* Enable KeepAlive caching
|
||||
* Enable KeepAlive cache
|
||||
*/
|
||||
keepAlive?: boolean;
|
||||
/**
|
||||
* External link - redirect path
|
||||
* External link - jump path
|
||||
*/
|
||||
link?: string;
|
||||
/**
|
||||
@@ -381,7 +403,7 @@ interface RouteMeta {
|
||||
*/
|
||||
maxNumOfOpenTab?: number;
|
||||
/**
|
||||
* The menu is visible, but access will be redirected to 403
|
||||
* The menu can be seen, but access will be redirected to 403
|
||||
*/
|
||||
menuVisibleWithForbidden?: boolean;
|
||||
/**
|
||||
@@ -389,9 +411,13 @@ interface RouteMeta {
|
||||
*/
|
||||
openInNewWindow?: boolean;
|
||||
/**
|
||||
* Used for route->menu sorting
|
||||
* Used for route -> menu sorting
|
||||
*/
|
||||
order?: number;
|
||||
/**
|
||||
* Parameters carried by the menu
|
||||
*/
|
||||
query?: Recordable;
|
||||
/**
|
||||
* Title name
|
||||
*/
|
||||
@@ -404,153 +430,169 @@ interface RouteMeta {
|
||||
### title
|
||||
|
||||
- Type: `string`
|
||||
- Default value: `''`
|
||||
- Default: `''`
|
||||
|
||||
Used to configure the page title, which will be displayed in the menu and tabs. It is generally used in conjunction with internationalization.
|
||||
Used to configure the title of the page, which will be displayed in the menu and tab. Generally used with internationalization.
|
||||
|
||||
### icon
|
||||
|
||||
- Type: `string`
|
||||
- Default value: `''`
|
||||
- Default: `''`
|
||||
|
||||
Used to configure the page icon, which will be displayed in the menu and tabs. It is generally used in conjunction with an icon library. If it is an `http` link, the image will be automatically loaded.
|
||||
Used to configure the icon of the page, which will be displayed in the menu and tab. Generally used with an icon library, if it is an `http` link, the image will be loaded automatically.
|
||||
|
||||
### activeIcon
|
||||
|
||||
- Type: `string`
|
||||
- Default value: `''`
|
||||
- Default: `''`
|
||||
|
||||
Used to configure the active icon of the page, which will be displayed in the menu. It is generally used in conjunction with an icon library. If it is an `http` link, the image will be automatically loaded.
|
||||
Used to configure the active icon of the page, which will be displayed in the menu. Generally used with an icon library, if it is an `http` link, the image will be loaded automatically.
|
||||
|
||||
### keepAlive
|
||||
|
||||
- Type: `boolean`
|
||||
- Default value: `false`
|
||||
- Default: `false`
|
||||
|
||||
Used to configure whether the page caching is enabled. Once enabled, the page will be cached and not reloaded, only effective when tabs are enabled.
|
||||
Used to configure whether the page cache is enabled. When enabled, the page will be cached and will not reload, only effective when the tab is enabled.
|
||||
|
||||
### hideInMenu
|
||||
|
||||
- Type: `boolean`
|
||||
- Default value: `false`
|
||||
- Default: `false`
|
||||
|
||||
Used to configure whether the page is hidden in the menu. If hidden, the page will not be displayed in the menu.
|
||||
Used to configure whether the page is hidden in the menu. When hidden, the page will not be displayed in the menu.
|
||||
|
||||
### hideInTab
|
||||
|
||||
- Type: `boolean`
|
||||
- Default value: `false`
|
||||
- Default: `false`
|
||||
|
||||
Used to configure whether the page is hidden in tabs. If hidden, the page will not be displayed in tabs.
|
||||
Used to configure whether the page is hidden in the tab. When hidden, the page will not be displayed in the tab.
|
||||
|
||||
### hideInBreadcrumb
|
||||
|
||||
- Type: `boolean`
|
||||
- Default value: `false`
|
||||
- Default: `false`
|
||||
|
||||
Used to configure whether the page is hidden in the breadcrumb. If hidden, the page will not be displayed in the breadcrumb.
|
||||
Used to configure whether the page is hidden in the breadcrumb. When hidden, the page will not be displayed in the breadcrumb.
|
||||
|
||||
### hideChildrenInMenu
|
||||
|
||||
- Type: `boolean`
|
||||
- Default value: `false`
|
||||
- Default: `false`
|
||||
|
||||
Used to configure whether the child pages of the page are hidden in the menu. If hidden, the child pages will not be displayed in the menu.
|
||||
Used to configure whether the subpages of the page are hidden in the menu. When hidden, the subpages will not be displayed in the menu.
|
||||
|
||||
### authority
|
||||
|
||||
- Type: `string[]`
|
||||
- Default value: `[]`
|
||||
- Default: `[]`
|
||||
|
||||
Used to configure the page's permissions. Only users with corresponding permissions can access the page. If not configured, no permissions are required.
|
||||
Used to configure the permissions of the page. Only users with the corresponding permissions can access the page. If not configured, no permissions are required.
|
||||
|
||||
### badge
|
||||
|
||||
- Type: `string`
|
||||
- Default value: `''`
|
||||
- Default: `''`
|
||||
|
||||
Used to configure the page's badge, which will be displayed in the menu.
|
||||
Used to configure the badge of the page, which will be displayed in the menu.
|
||||
|
||||
### badgeType
|
||||
|
||||
- Type: `'dot' | 'normal'`
|
||||
- Default value: `'normal'`
|
||||
- Default: `'normal'`
|
||||
|
||||
Used to configure the type of the page's badge. `dot` is a small red dot, `normal` is text.
|
||||
Used to configure the badge type of the page. `dot` is a small red dot, `normal` is text.
|
||||
|
||||
### badgeVariants
|
||||
|
||||
- Type: `'default' | 'destructive' | 'primary' | 'success' | 'warning' | string`
|
||||
- Default value: `'success'`
|
||||
- Default: `'success'`
|
||||
|
||||
Used to configure the color of the page's badge.
|
||||
Used to configure the badge color of the page.
|
||||
|
||||
### activePath
|
||||
|
||||
- Type: `string`
|
||||
- Default value: `''`
|
||||
- Default: `''`
|
||||
|
||||
Used to configure the currently active menu. Sometimes when the page is not displayed in the menu, it is used to activate the parent menu.
|
||||
Used to configure the currently active menu. Sometimes the page is not displayed in the menu, and this is used to activate the parent menu.
|
||||
|
||||
### affixTab
|
||||
|
||||
- Type: `boolean`
|
||||
- Default value: `false`
|
||||
- Default: `false`
|
||||
|
||||
Used to configure whether the page tab is pinned. Once pinned, the page cannot be closed.
|
||||
Used to configure whether the page is fixed in the tab. When fixed, the page cannot be closed.
|
||||
|
||||
### affixTabOrder
|
||||
|
||||
- Type: `number`
|
||||
- Default value: `0`
|
||||
- Default: `0`
|
||||
|
||||
Used to configure the order of the pinned page tabs, sorted in ascending order.
|
||||
Used to configure the order of fixed tabs, sorted in ascending order.
|
||||
|
||||
### iframeSrc
|
||||
|
||||
- Type: `string`
|
||||
- Default value: `''`
|
||||
- Default: `''`
|
||||
|
||||
Used to configure the `iframe` address of the embedded page. Once set, the corresponding page will be embedded in the current page.
|
||||
Used to configure the `iframe` address of the embedded page. When set, the corresponding page will be embedded in the current page.
|
||||
|
||||
### ignoreAccess
|
||||
|
||||
- Type: `boolean`
|
||||
- Default value: `false`
|
||||
- Default: `false`
|
||||
|
||||
Used to configure whether the page ignores permissions and can be accessed directly.
|
||||
|
||||
### link
|
||||
|
||||
- Type: `string`
|
||||
- Default value: `''`
|
||||
- Default: `''`
|
||||
|
||||
Used to configure the external link jump path, which will be opened in a new window.
|
||||
Used to configure the external link jump path, which will open in a new window.
|
||||
|
||||
### maxNumOfOpenTab
|
||||
|
||||
- Type: `number`
|
||||
- Default value: `-1`
|
||||
- Default: `-1`
|
||||
|
||||
Used to configure the maximum number of open tabs. Once set, the earliest opened tab will be automatically closed when a new tab is opened (only effective when opening tabs with the same name).
|
||||
Used to configure the maximum number of open tabs. When set, the earliest opened tab will be automatically closed when opening a new tab (only effective when opening tabs with the same name).
|
||||
|
||||
### menuVisibleWithForbidden
|
||||
|
||||
- Type: `boolean`
|
||||
- Default value: `false`
|
||||
- Default: `false`
|
||||
|
||||
Used to configure whether the page can be seen in the menu, but access will be redirected to 403.
|
||||
|
||||
### openInNewWindow
|
||||
|
||||
- Type: `boolean`
|
||||
- Default: `false`
|
||||
|
||||
When set to `true`, the page will open in a new window.
|
||||
|
||||
### order
|
||||
|
||||
- Type: `number`
|
||||
- Default value: `0`
|
||||
- Default: `0`
|
||||
|
||||
Used to configure the page's order, for routing to menu sorting.
|
||||
Used to configure the sorting of the page, used for route to menu sorting.
|
||||
|
||||
_Note:_ Sorting is only effective for first-level menus. The sorting of second-level menus needs to be set in the corresponding first-level menu in code order.
|
||||
|
||||
### query
|
||||
|
||||
- Type: `Recordable`
|
||||
- Default: `{}`
|
||||
|
||||
Used to configure the menu parameters of the page, which will be passed to the page in the menu.
|
||||
|
||||
## Route Refresh
|
||||
|
||||
The way to refresh the route is as follows:
|
||||
The route refresh method is as follows:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
|
@@ -18,7 +18,7 @@
|
||||
- **Permission Validation**: Comprehensive permission validation solutions, including button-level permission control.
|
||||
- **Multi-Theme**: Built-in multiple theme configurations & dark mode to meet personalized needs.
|
||||
- **Dynamic Menu**: Supports dynamic menus that can display based on permissions.
|
||||
- **Mock Data**: High-performance local Mock data solution based on Nitro.
|
||||
- **Mock Data**: High-performance local Mock data solution based on `Nitro`.
|
||||
- **Rich Components**: Provides a wide range of components to meet most business needs.
|
||||
- **Standardization**: Code quality is ensured with tools like `ESLint`, `Prettier`, `Stylelint`, `Publint`, and `CSpell`.
|
||||
- **Engineering**: Development efficiency is improved with tools like `Pnpm Monorepo`, `TurboRepo`, and `Changeset`.
|
||||
@@ -26,9 +26,9 @@
|
||||
|
||||
## Browser Support
|
||||
|
||||
**Local development** is recommended using the **latest version of Chrome**. **Versions below Chrome 80 are not supported**.
|
||||
- **Local development** is recommended using the **latest version of Chrome**. **Versions below Chrome 80 are not supported**.
|
||||
|
||||
**Production environment** supports modern browsers, IE is not supported.
|
||||
- **Production environment** supports modern browsers, IE is not supported.
|
||||
|
||||
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/archive/internet-explorer_9-11/internet-explorer_9-11_48x48.png" alt="IE" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)IE | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)Safari |
|
||||
| :-: | :-: | :-: | :-: | :-: |
|
||||
@@ -37,12 +37,10 @@
|
||||
## Contribution
|
||||
|
||||
- [Vben Admin](https://github.com/vbenjs/vue-vben-admin) is still being actively updated. Contributions are welcome to help maintain and improve the project, aiming to create a better mid- to backend solution.
|
||||
- If you wish to join us, you can provide suggestions or submit pull requests. We will invite you to join based on your activity.
|
||||
- If you wish to join us, you can start by contributing in the following ways, and we will invite you to join based on your activity.
|
||||
|
||||
::: info Join Us
|
||||
|
||||
If you wish to join us, you can start by contributing in the following ways, and we will invite you to join based on your activity:
|
||||
|
||||
- Regularly submit `PRs`.
|
||||
- Provide valuable suggestions.
|
||||
- Participate in discussions and help resolve some `issues`.
|
||||
|
@@ -67,7 +67,7 @@ import { SvgTestIcon } from '@vben/icons';
|
||||
</template>
|
||||
```
|
||||
|
||||
## Tailwind CSS 图标 <Badge text="不推荐" type="danger"/>
|
||||
## Tailwind CSS 图标
|
||||
|
||||
### 使用
|
||||
|
||||
|
@@ -8,11 +8,29 @@ outline: deep
|
||||
|
||||
## 路由类型
|
||||
|
||||
路由分为静态路由和动态路由,静态路由是在项目启动时就已经确定的路由。动态路由一般是在用户登录后,根据用户的权限动态生成的路由。
|
||||
路由分为核心路由、静态路由和动态路由,核心路由是框架内置的路由,包含了根路由、登录路由、404路由等;静态路由是在项目启动时就已经确定的路由;动态路由一般是在用户登录后,根据用户的权限动态生成的路由。
|
||||
|
||||
静态路由和动态路由都会走权限控制,可以通过配置路由的 `meta` 属性中的 `authority` 字段来控制权限,可以参考[路由权限控制](https://github.com/vbenjs/vue-vben-admin/blob/main/playground/src/router/routes/modules/demos.ts)。
|
||||
|
||||
### 核心路由
|
||||
|
||||
核心路由是框架内置的路由,包含了根路由、登录路由、404路由等,核心路由的配置在应用下 `src/router/routes/core` 目录下
|
||||
|
||||
::: tip
|
||||
|
||||
核心路由主要用于框架的基础功能,因此不建议将业务相关的路由放在核心路由中,推荐将业务相关的路由放在静态路由或动态路由中。
|
||||
|
||||
:::
|
||||
|
||||
### 静态路由
|
||||
|
||||
如果你的页面项目不需要权限控制,可以直接使用静态路由,静态路由的配置在应用下 `src/router/routes/index` 目录下,打开注释的文件内容:
|
||||
静态路由的配置在应用下 `src/router/routes/index` 目录下,打开注释的文件内容:
|
||||
|
||||
::: tip
|
||||
|
||||
权限控制是通过路由的 `meta` 属性中的 `authority` 字段来控制的,如果你的页面项目不需要权限控制,可以不设置 `authority` 字段。
|
||||
|
||||
:::
|
||||
|
||||
```ts
|
||||
// 有需要可以自行打开注释,并创建文件夹
|
||||
|
@@ -18,7 +18,7 @@
|
||||
- **权限验证**:完善的权限验证方案,按钮级别权限控制。
|
||||
- **多主题**:内置多种主题配置和黑暗模式,满足个性化需求。
|
||||
- **动态菜单**:支持动态菜单,可以根据权限配置显示菜单。
|
||||
- **Mock 数据**:基于 Nitro 的本地高性能 Mock 数据方案。
|
||||
- **Mock 数据**:基于 `Nitro` 的本地高性能 Mock 数据方案。
|
||||
- **组件丰富**:提供了丰富的组件,可以满足大部分的业务需求。
|
||||
- **规范**:代码规范,使用 `ESLint`、`Prettier`、`Stylelint`、`Publint`、`CSpell` 等工具保证代码质量。
|
||||
- **工程化**:使用 `Pnpm Monorepo`、`TurboRepo`、`Changeset` 等工具,提高开发效率。
|
||||
@@ -26,9 +26,9 @@
|
||||
|
||||
## 浏览器支持
|
||||
|
||||
**本地开发**推荐使用`Chrome 最新版`浏览器,**不支持**`Chrome 80`以下版本。
|
||||
- **本地开发**推荐使用`Chrome 最新版`浏览器,**不支持**`Chrome 80`以下版本。
|
||||
|
||||
**生产环境**支持现代浏览器,不支持 IE。
|
||||
- **生产环境**支持现代浏览器,不支持 IE。
|
||||
|
||||
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/archive/internet-explorer_9-11/internet-explorer_9-11_48x48.png" alt="IE" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)IE | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)Safari |
|
||||
| :-: | :-: | :-: | :-: | :-: |
|
||||
@@ -37,13 +37,13 @@
|
||||
## 贡献
|
||||
|
||||
- [Vben Admin](https://github.com/vbenjs/vue-vben-admin) 还在持续更新中,本项目欢迎您的参与,共同维护,逐步完善,打造更好的中后台解决方案。
|
||||
- 如果你想加入我们,可以提供有价值的建议或者参与讨论,协助解决 issue,- 如果你想加入我们,可以提供有价值的建议或者参与讨论,协助解决 issue,我们会根据你的活跃度邀请你加入。。
|
||||
- 如果你有兴趣加入我们,可以通过以下方式开始,我们会根据你的活跃度邀请你加入。
|
||||
|
||||
::: info 加入我们
|
||||
|
||||
- 长期提交 `PR`。
|
||||
- 提供一些好的建议。
|
||||
- 参与讨论,帮助解决一些 `issue`。
|
||||
- 提供有价值的建议。
|
||||
- 参与讨论,帮助解决 `issue`。
|
||||
- 共同维护文档。
|
||||
|
||||
:::
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/commitlint-config",
|
||||
"version": "5.4.4",
|
||||
"version": "5.4.8",
|
||||
"private": true,
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
|
@@ -22,7 +22,7 @@ export async function typescript(): Promise<Linter.Config[]> {
|
||||
ecmaVersion: 'latest',
|
||||
extraFileExtensions: ['.vue'],
|
||||
jsxPragma: 'React',
|
||||
project: './tsconfig.*?.json',
|
||||
project: './tsconfig.*.json',
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
|
@@ -18,6 +18,7 @@ export async function unicorn(): Promise<Linter.Config[]> {
|
||||
'unicorn/better-regex': 'off',
|
||||
'unicorn/consistent-destructuring': 'off',
|
||||
'unicorn/consistent-function-scoping': 'off',
|
||||
'unicorn/expiring-todo-comments': 'off',
|
||||
'unicorn/filename-case': 'off',
|
||||
'unicorn/import-style': 'off',
|
||||
'unicorn/no-array-for-each': 'off',
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/stylelint-config",
|
||||
"version": "5.4.4",
|
||||
"version": "5.4.8",
|
||||
"private": true,
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/node-utils",
|
||||
"version": "5.4.4",
|
||||
"version": "5.4.8",
|
||||
"private": true,
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/tailwind-config",
|
||||
"version": "5.4.4",
|
||||
"version": "5.4.8",
|
||||
"private": true,
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/tsconfig",
|
||||
"version": "5.4.4",
|
||||
"version": "5.4.8",
|
||||
"private": true,
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/vite-config",
|
||||
"version": "5.4.4",
|
||||
"version": "5.4.8",
|
||||
"private": true,
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
|
@@ -6,6 +6,7 @@ import path, { relative } from 'node:path';
|
||||
|
||||
import { findMonorepoRoot } from '@vben/node-utils';
|
||||
|
||||
import { NodePackageImporter } from 'sass';
|
||||
import { defineConfig, loadEnv, mergeConfig } from 'vite';
|
||||
|
||||
import { defaultImportmapOptions, getDefaultPwaOptions } from '../options';
|
||||
@@ -85,7 +86,7 @@ function defineApplicationConfig(userConfigPromise?: DefineApplicationOptions) {
|
||||
clientFiles: [
|
||||
'./index.html',
|
||||
'./src/bootstrap.ts',
|
||||
'./src/{views,layouts,router,store,api}/*',
|
||||
'./src/{views,layouts,router,store,api,adapter}/*',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -113,7 +114,8 @@ function createCssOptions(injectGlobalScss = true) {
|
||||
}
|
||||
return content;
|
||||
},
|
||||
api: 'modern-compiler',
|
||||
api: 'modern',
|
||||
importers: [new NodePackageImporter()],
|
||||
},
|
||||
}
|
||||
: {},
|
||||
|
@@ -4,35 +4,6 @@ import { lazyImport, VxeResolver } from 'vite-plugin-lazy-import';
|
||||
|
||||
async function viteVxeTableImportsPlugin(): Promise<PluginOption> {
|
||||
return [
|
||||
// {
|
||||
// config() {
|
||||
// return {
|
||||
// optimizeDeps: {
|
||||
// include: [
|
||||
// 'vxe-pc-ui/es/vxe-button/index.js',
|
||||
// 'vxe-pc-ui/es/vxe-checkbox/index.js',
|
||||
// 'vxe-pc-ui/es/vxe-icon/index.js',
|
||||
// 'vxe-pc-ui/es/vxe-input/index.js',
|
||||
// 'vxe-pc-ui/es/vxe-loading/index.js',
|
||||
// 'vxe-pc-ui/es/vxe-modal/index.js',
|
||||
// 'vxe-pc-ui/es/vxe-pager/index.js',
|
||||
// 'vxe-pc-ui/es/vxe-radio-group/index.js',
|
||||
// 'vxe-pc-ui/es/vxe-select/index.js',
|
||||
// 'vxe-pc-ui/es/vxe-tooltip/index.js',
|
||||
// 'vxe-pc-ui/es/vxe-ui/index.js',
|
||||
// 'vxe-pc-ui/es/vxe-upload/index.js',
|
||||
// 'vxe-table/es/vxe-colgroup/index.js',
|
||||
// 'vxe-table/es/vxe-column/index.js',
|
||||
// 'vxe-table/es/vxe-grid/index.js',
|
||||
// 'vxe-table/es/vxe-table/index.js',
|
||||
// 'vxe-table/es/vxe-toolbar/index.js',
|
||||
// 'vxe-table/es/vxe-ui/index.js',
|
||||
// ],
|
||||
// },
|
||||
// };
|
||||
// },
|
||||
// name: 'vxe-table-adapter',
|
||||
// },
|
||||
lazyImport({
|
||||
resolvers: [
|
||||
VxeResolver({
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vben-admin-monorepo",
|
||||
"version": "5.4.4",
|
||||
"version": "5.4.8",
|
||||
"private": true,
|
||||
"keywords": [
|
||||
"monorepo",
|
||||
@@ -99,7 +99,7 @@
|
||||
"node": ">=20.10.0",
|
||||
"pnpm": ">=9.12.0"
|
||||
},
|
||||
"packageManager": "pnpm@9.12.3",
|
||||
"packageManager": "pnpm@9.14.2",
|
||||
"pnpm": {
|
||||
"peerDependencyRules": {
|
||||
"allowedVersions": {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben-core/design",
|
||||
"version": "5.4.4",
|
||||
"version": "5.4.8",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
|
@@ -31,7 +31,7 @@
|
||||
#app,
|
||||
body,
|
||||
html {
|
||||
@apply size-full overscroll-none;
|
||||
@apply size-full;
|
||||
|
||||
/* scrollbar-gutter: stable; */
|
||||
}
|
||||
|
@@ -3,19 +3,5 @@ import { defineBuildConfig } from 'unbuild';
|
||||
export default defineBuildConfig({
|
||||
clean: true,
|
||||
declaration: true,
|
||||
entries: [
|
||||
{
|
||||
builder: 'mkdist',
|
||||
input: './src',
|
||||
loaders: ['vue'],
|
||||
pattern: ['**/*.vue'],
|
||||
},
|
||||
{
|
||||
builder: 'mkdist',
|
||||
format: 'esm',
|
||||
input: './src',
|
||||
loaders: ['js'],
|
||||
pattern: ['**/*.ts'],
|
||||
},
|
||||
],
|
||||
entries: ['src/index'],
|
||||
});
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben-core/icons",
|
||||
"version": "5.4.4",
|
||||
"version": "5.4.8",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
|
@@ -1,10 +1,14 @@
|
||||
export { default as EmptyIcon } from './components/empty.vue';
|
||||
export * from './create-icon';
|
||||
|
||||
export * from './lucide';
|
||||
|
||||
export type { IconifyIcon as IconifyIconStructure } from '@iconify/vue';
|
||||
export { addCollection, addIcon, Icon as IconifyIcon } from '@iconify/vue';
|
||||
export {
|
||||
addCollection,
|
||||
addIcon,
|
||||
Icon as IconifyIcon,
|
||||
listIcons,
|
||||
} from '@iconify/vue';
|
||||
|
||||
/**
|
||||
* 从@iconify/vue/dist/offline'导出的组件为离线ICON 不支持在线
|
||||
|
@@ -27,6 +27,7 @@ export {
|
||||
FoldHorizontal,
|
||||
Fullscreen,
|
||||
Github,
|
||||
Grip,
|
||||
Info,
|
||||
InspectionPanel,
|
||||
Languages,
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben-core/shared",
|
||||
"version": "5.4.4",
|
||||
"version": "5.4.8",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
@@ -81,12 +81,11 @@
|
||||
"dependencies": {
|
||||
"@ctrl/tinycolor": "catalog:",
|
||||
"@tanstack/vue-store": "catalog:",
|
||||
"@types/lodash.get": "catalog:",
|
||||
"@vue/shared": "catalog:",
|
||||
"clsx": "catalog:",
|
||||
"dayjs": "catalog:",
|
||||
"defu": "catalog:",
|
||||
"lodash.clonedeep": "catalog:",
|
||||
"lodash.get": "catalog:",
|
||||
"nprogress": "catalog:",
|
||||
"tailwind-merge": "catalog:",
|
||||
"theme-colors": "catalog:"
|
||||
|
18
packages/@core/base/shared/src/utils/date.ts
Normal file
18
packages/@core/base/shared/src/utils/date.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
export function formatDate(time: number | string, format = 'YYYY-MM-DD') {
|
||||
try {
|
||||
const date = dayjs(time);
|
||||
if (!date.isValid()) {
|
||||
throw new Error('Invalid date');
|
||||
}
|
||||
return date.format(format);
|
||||
} catch (error) {
|
||||
console.error(`Error formatting date: ${error}`);
|
||||
return time;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatDateTime(time: number | string) {
|
||||
return formatDate(time, 'YYYY-MM-DD HH:mm:ss');
|
||||
}
|
156
packages/@core/base/shared/src/utils/download.ts
Normal file
156
packages/@core/base/shared/src/utils/download.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import { openWindow } from './window';
|
||||
|
||||
interface DownloadOptions<T = string> {
|
||||
fileName?: string;
|
||||
source: T;
|
||||
target?: string;
|
||||
}
|
||||
|
||||
const DEFAULT_FILENAME = 'downloaded_file';
|
||||
|
||||
/**
|
||||
* 通过 URL 下载文件,支持跨域
|
||||
* @throws {Error} - 当下载失败时抛出错误
|
||||
*/
|
||||
export async function downloadFileFromUrl({
|
||||
fileName,
|
||||
source,
|
||||
target = '_blank',
|
||||
}: DownloadOptions): Promise<void> {
|
||||
if (!source || typeof source !== 'string') {
|
||||
throw new Error('Invalid URL.');
|
||||
}
|
||||
|
||||
const isChrome = window.navigator.userAgent.toLowerCase().includes('chrome');
|
||||
const isSafari = window.navigator.userAgent.toLowerCase().includes('safari');
|
||||
|
||||
if (/iP/.test(window.navigator.userAgent)) {
|
||||
console.error('Your browser does not support download!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isChrome || isSafari) {
|
||||
triggerDownload(source, resolveFileName(source, fileName));
|
||||
}
|
||||
if (!source.includes('?')) {
|
||||
source += '?download';
|
||||
}
|
||||
|
||||
openWindow(source, { target });
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 Base64 下载文件
|
||||
*/
|
||||
export function downloadFileFromBase64({ fileName, source }: DownloadOptions) {
|
||||
if (!source || typeof source !== 'string') {
|
||||
throw new Error('Invalid Base64 data.');
|
||||
}
|
||||
|
||||
const resolvedFileName = fileName || DEFAULT_FILENAME;
|
||||
triggerDownload(source, resolvedFileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过图片 URL 下载图片文件
|
||||
*/
|
||||
export async function downloadFileFromImageUrl({
|
||||
fileName,
|
||||
source,
|
||||
}: DownloadOptions) {
|
||||
const base64 = await urlToBase64(source);
|
||||
downloadFileFromBase64({ fileName, source: base64 });
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 Blob 下载文件
|
||||
*/
|
||||
export function downloadFileFromBlob({
|
||||
fileName = DEFAULT_FILENAME,
|
||||
source,
|
||||
}: DownloadOptions<Blob>): void {
|
||||
if (!(source instanceof Blob)) {
|
||||
throw new TypeError('Invalid Blob data.');
|
||||
}
|
||||
|
||||
const url = URL.createObjectURL(source);
|
||||
triggerDownload(url, fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文件,支持 Blob、字符串和其他 BlobPart 类型
|
||||
*/
|
||||
export function downloadFileFromBlobPart({
|
||||
fileName = DEFAULT_FILENAME,
|
||||
source,
|
||||
}: DownloadOptions<BlobPart>): void {
|
||||
// 如果 data 不是 Blob,则转换为 Blob
|
||||
const blob =
|
||||
source instanceof Blob
|
||||
? source
|
||||
: new Blob([source], { type: 'application/octet-stream' });
|
||||
|
||||
// 创建对象 URL 并触发下载
|
||||
const url = URL.createObjectURL(blob);
|
||||
triggerDownload(url, fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* img url to base64
|
||||
* @param url
|
||||
*/
|
||||
export function urlToBase64(url: string, mineType?: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let canvas = document.createElement('CANVAS') as HTMLCanvasElement | null;
|
||||
const ctx = canvas?.getContext('2d');
|
||||
const img = new Image();
|
||||
img.crossOrigin = '';
|
||||
img.addEventListener('load', () => {
|
||||
if (!canvas || !ctx) {
|
||||
return reject(new Error('Failed to create canvas.'));
|
||||
}
|
||||
canvas.height = img.height;
|
||||
canvas.width = img.width;
|
||||
ctx.drawImage(img, 0, 0);
|
||||
const dataURL = canvas.toDataURL(mineType || 'image/png');
|
||||
canvas = null;
|
||||
resolve(dataURL);
|
||||
});
|
||||
img.src = url;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用下载触发函数
|
||||
* @param href - 文件下载的 URL
|
||||
* @param fileName - 下载文件的名称,如果未提供则自动识别
|
||||
* @param revokeDelay - 清理 URL 的延迟时间 (毫秒)
|
||||
*/
|
||||
export function triggerDownload(
|
||||
href: string,
|
||||
fileName: string | undefined,
|
||||
revokeDelay: number = 100,
|
||||
): void {
|
||||
const defaultFileName = 'downloaded_file';
|
||||
const finalFileName = fileName || defaultFileName;
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = href;
|
||||
link.download = finalFileName;
|
||||
link.style.display = 'none';
|
||||
|
||||
if (link.download === undefined) {
|
||||
link.setAttribute('target', '_blank');
|
||||
}
|
||||
|
||||
document.body.append(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
|
||||
// 清理临时 URL 以释放内存
|
||||
setTimeout(() => URL.revokeObjectURL(href), revokeDelay);
|
||||
}
|
||||
|
||||
function resolveFileName(url: string, fileName?: string): string {
|
||||
return fileName || url.slice(url.lastIndexOf('/') + 1) || DEFAULT_FILENAME;
|
||||
}
|
@@ -1,6 +1,8 @@
|
||||
export * from './cn';
|
||||
export * from './date';
|
||||
export * from './diff';
|
||||
export * from './dom';
|
||||
export * from './download';
|
||||
export * from './inference';
|
||||
export * from './letter';
|
||||
export * from './merge';
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben-core/typings",
|
||||
"version": "5.4.4",
|
||||
"version": "5.4.8",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
|
4
packages/@core/base/typings/src/basic.d.ts
vendored
4
packages/@core/base/typings/src/basic.d.ts
vendored
@@ -34,4 +34,6 @@ interface BasicUserInfo {
|
||||
username: string;
|
||||
}
|
||||
|
||||
export type { BasicOption, BasicUserInfo, SelectOption, TabOption };
|
||||
type ClassType = Array<object | string> | object | string;
|
||||
|
||||
export type { BasicOption, BasicUserInfo, ClassType, SelectOption, TabOption };
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben-core/composables",
|
||||
"version": "5.4.4",
|
||||
"version": "5.4.8",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben-core/preferences",
|
||||
"version": "5.4.4",
|
||||
"version": "5.4.8",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben-core/form-ui",
|
||||
"version": "5.2.1",
|
||||
"version": "5.4.8",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
@@ -40,6 +40,7 @@
|
||||
"@vben-core/composables": "workspace:*",
|
||||
"@vben-core/shadcn-ui": "workspace:*",
|
||||
"@vben-core/shared": "workspace:*",
|
||||
"@vben-core/typings": "workspace:*",
|
||||
"@vee-validate/zod": "catalog:",
|
||||
"@vueuse/core": "catalog:",
|
||||
"vee-validate": "catalog:",
|
||||
|
@@ -3,7 +3,12 @@ import { computed, toRaw, unref, watch } from 'vue';
|
||||
|
||||
import { useSimpleLocale } from '@vben-core/composables';
|
||||
import { VbenExpandableArrow } from '@vben-core/shadcn-ui';
|
||||
import { cn, isFunction, triggerWindowResize } from '@vben-core/shared/utils';
|
||||
import {
|
||||
cn,
|
||||
formatDate,
|
||||
isFunction,
|
||||
triggerWindowResize,
|
||||
} from '@vben-core/shared/utils';
|
||||
|
||||
import { COMPONENT_MAP } from '../config';
|
||||
import { injectFormProps } from '../use-form-context';
|
||||
@@ -52,20 +57,69 @@ async function handleSubmit(e: Event) {
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
await unref(rootProps).handleSubmit?.(toRaw(form.values));
|
||||
|
||||
const values = handleRangeTimeValue(toRaw(form.values));
|
||||
await unref(rootProps).handleSubmit?.(values);
|
||||
}
|
||||
|
||||
async function handleReset(e: Event) {
|
||||
e?.preventDefault();
|
||||
e?.stopPropagation();
|
||||
const props = unref(rootProps);
|
||||
|
||||
const values = toRaw(form.values);
|
||||
// 清理时间字段
|
||||
props.fieldMappingTime &&
|
||||
props.fieldMappingTime.forEach(([_, [startTimeKey, endTimeKey]]) => {
|
||||
delete values[startTimeKey];
|
||||
delete values[endTimeKey];
|
||||
});
|
||||
|
||||
if (isFunction(props.handleReset)) {
|
||||
await props.handleReset?.(form.values);
|
||||
await props.handleReset?.(values);
|
||||
} else {
|
||||
form.resetForm();
|
||||
}
|
||||
}
|
||||
|
||||
function handleRangeTimeValue(values: Record<string, any>) {
|
||||
const fieldMappingTime = unref(rootProps).fieldMappingTime;
|
||||
|
||||
if (!fieldMappingTime || !Array.isArray(fieldMappingTime)) {
|
||||
return values;
|
||||
}
|
||||
|
||||
fieldMappingTime.forEach(
|
||||
([field, [startTimeKey, endTimeKey], format = 'YYYY-MM-DD']) => {
|
||||
if (startTimeKey && endTimeKey && values[field] === null) {
|
||||
delete values[startTimeKey];
|
||||
delete values[endTimeKey];
|
||||
}
|
||||
|
||||
if (!values[field]) {
|
||||
delete values[field];
|
||||
return;
|
||||
}
|
||||
|
||||
const [startTime, endTime] = values[field];
|
||||
const [startTimeFormat, endTimeFormat] = Array.isArray(format)
|
||||
? format
|
||||
: [format, format];
|
||||
|
||||
values[startTimeKey] = startTime
|
||||
? formatDate(startTime, startTimeFormat)
|
||||
: undefined;
|
||||
values[endTimeKey] = endTime
|
||||
? formatDate(endTime, endTimeFormat)
|
||||
: undefined;
|
||||
|
||||
delete values[field];
|
||||
},
|
||||
);
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => collapsed.value,
|
||||
() => {
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import type { Recordable } from '@vben-core/typings';
|
||||
import type {
|
||||
FormState,
|
||||
GenericObject,
|
||||
@@ -12,13 +13,13 @@ import { toRaw } from 'vue';
|
||||
import { Store } from '@vben-core/shared/store';
|
||||
import {
|
||||
bindMethods,
|
||||
createMerge,
|
||||
isFunction,
|
||||
isObject,
|
||||
mergeWithArrayOverride,
|
||||
StateHandler,
|
||||
} from '@vben-core/shared/utils';
|
||||
|
||||
import { objectPick } from '@vueuse/core';
|
||||
|
||||
function getDefaultState(): VbenFormProps {
|
||||
return {
|
||||
actionWrapperClass: '',
|
||||
@@ -41,11 +42,14 @@ function getDefaultState(): VbenFormProps {
|
||||
}
|
||||
|
||||
export class FormApi {
|
||||
// 最后一次点击提交时的表单值
|
||||
private latestSubmissionValues: null | Recordable<any> = null;
|
||||
private prevState: null | VbenFormProps = null;
|
||||
|
||||
// private api: Pick<VbenFormProps, 'handleReset' | 'handleSubmit'>;
|
||||
public form = {} as FormActions;
|
||||
|
||||
isMounted = false;
|
||||
|
||||
public state: null | VbenFormProps = null;
|
||||
|
||||
stateHandler: StateHandler;
|
||||
@@ -110,6 +114,10 @@ export class FormApi {
|
||||
this.store.batch(cb);
|
||||
}
|
||||
|
||||
getLatestSubmissionValues() {
|
||||
return this.latestSubmissionValues || {};
|
||||
}
|
||||
|
||||
getState() {
|
||||
return this.state;
|
||||
}
|
||||
@@ -164,6 +172,7 @@ export class FormApi {
|
||||
if (!this.isMounted) {
|
||||
Object.assign(this.form, formActions);
|
||||
this.stateHandler.setConditionTrue();
|
||||
this.setLatestSubmissionValues({ ...toRaw(this.form.values) });
|
||||
this.isMounted = true;
|
||||
}
|
||||
}
|
||||
@@ -207,6 +216,10 @@ export class FormApi {
|
||||
form.setFieldValue(field, value, shouldValidate);
|
||||
}
|
||||
|
||||
setLatestSubmissionValues(values: null | Recordable<any>) {
|
||||
this.latestSubmissionValues = { ...toRaw(values) };
|
||||
}
|
||||
|
||||
setState(
|
||||
stateOrFn:
|
||||
| ((prev: VbenFormProps) => Partial<VbenFormProps>)
|
||||
@@ -237,8 +250,17 @@ export class FormApi {
|
||||
form.setValues(fields, shouldValidate);
|
||||
return;
|
||||
}
|
||||
const fieldNames = this.state?.schema?.map((item) => item.fieldName) ?? [];
|
||||
const filteredFields = objectPick(fields, fieldNames);
|
||||
|
||||
const fieldMergeFn = createMerge((obj, key, value) => {
|
||||
if (key in obj) {
|
||||
obj[key] =
|
||||
!Array.isArray(obj[key]) && isObject(obj[key])
|
||||
? fieldMergeFn(obj[key], value)
|
||||
: value;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
const filteredFields = fieldMergeFn(fields, form.values);
|
||||
form.setValues(filteredFields, shouldValidate);
|
||||
}
|
||||
|
||||
@@ -249,12 +271,14 @@ export class FormApi {
|
||||
await form.submitForm();
|
||||
const rawValues = toRaw(form.values || {});
|
||||
await this.state?.handleSubmit?.(rawValues);
|
||||
|
||||
return rawValues;
|
||||
}
|
||||
|
||||
unmount() {
|
||||
this.form?.resetForm?.();
|
||||
// this.state = null;
|
||||
this.latestSubmissionValues = null;
|
||||
this.isMounted = false;
|
||||
this.stateHandler.reset();
|
||||
}
|
||||
@@ -303,4 +327,13 @@ export class FormApi {
|
||||
}
|
||||
return validateResult;
|
||||
}
|
||||
|
||||
async validateAndSubmitForm() {
|
||||
const form = await this.getForm();
|
||||
const { valid } = await form.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
return await this.submitForm();
|
||||
}
|
||||
}
|
||||
|
@@ -209,8 +209,9 @@ function fieldBindEvent(slotProps: Record<string, any>) {
|
||||
if (modelValue && isObject(modelValue) && bindEventField) {
|
||||
value = isEventObjectLike(modelValue)
|
||||
? modelValue?.target?.[bindEventField]
|
||||
: modelValue;
|
||||
: (modelValue?.[bindEventField] ?? modelValue);
|
||||
}
|
||||
|
||||
if (bindEventField) {
|
||||
return {
|
||||
[`onUpdate:${bindEventField}`]: handler,
|
||||
@@ -223,6 +224,7 @@ function fieldBindEvent(slotProps: Record<string, any>) {
|
||||
if (!shouldUnwrap) {
|
||||
return onChange?.(e);
|
||||
}
|
||||
|
||||
return onChange?.(e?.target?.[bindEventField] ?? e);
|
||||
},
|
||||
onInput: () => {},
|
||||
@@ -238,6 +240,12 @@ function createComponentProps(slotProps: Record<string, any>) {
|
||||
...slotProps.componentField,
|
||||
...computedProps.value,
|
||||
...bindEvents,
|
||||
...(Reflect.has(computedProps.value, 'onChange')
|
||||
? { onChange: computedProps.value.onChange }
|
||||
: {}),
|
||||
...(Reflect.has(computedProps.value, 'onInput')
|
||||
? { onInput: computedProps.value.onInput }
|
||||
: {}),
|
||||
};
|
||||
|
||||
return binds;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user