chore: merge branch 'main' of github.com:anncwb/vue-vben-admin into main

This commit is contained in:
shisan 2021-07-20 00:00:38 +08:00
commit e2d5fd0a02
69 changed files with 1747 additions and 1890 deletions

36
.env.test Normal file
View File

@ -0,0 +1,36 @@
NODE_ENV=production
# Whether to open mock
VITE_USE_MOCK = true
# public path
VITE_PUBLIC_PATH = /
# Delete console
VITE_DROP_CONSOLE = true
# Whether to enable gzip or brotli compression
# Optional: gzip | brotli | none
# If you need multiple forms, you can use `,` to separate
VITE_BUILD_COMPRESS = 'none'
# Whether to delete origin files when using compress, default false
VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE = false
# Basic interface address SPA
VITE_GLOB_API_URL=/basic-api
# File upload address optional
# It can be forwarded by nginx or write the actual address directly
VITE_GLOB_UPLOAD_URL=/upload
# Interface prefix
VITE_GLOB_API_URL_PREFIX=
# Whether to enable image compression
VITE_USE_IMAGEMIN= true
# use pwa
VITE_USE_PWA = false
# Is it compatible with older browsers
VITE_LEGACY = false

View File

@ -61,6 +61,7 @@ module.exports = defineConfig({
'vue/singleline-html-element-content-newline': 'off',
'vue/attribute-hyphenation': 'off',
'vue/require-default-prop': 'off',
'vue/script-setup-uses-vars': 'off',
'vue/html-self-closing': [
'error',
{

19
.vscode/settings.json vendored
View File

@ -130,6 +130,23 @@
"sider",
"pinia",
"sider",
"nprogress"
"nprogress",
"INTLIFY",
"stylelint",
"esno",
"vitejs",
"sortablejs",
"mockjs",
"codemirror",
"iconify",
"commitlint",
"vditor",
"echarts",
"cropperjs",
"logicflow",
"vueuse",
"zxcvbn",
"lintstagedrc",
"brotli"
]
}

View File

@ -1,3 +1,52 @@
## 2.6.1(2021-07-19)
### ✨ Features
- **NoticeList** Add pagination, auto omit for overlength, title click event, title strikethrough, etc.
- **MixSider** Optimize the style of the bottom collapse button in the Mix menu layout to be consistent with the style of other menu layouts
- **ApiTreeSelect** Extend `TreeSelect` component of `antdv` to support remote data source, similar to `ApiSelect`.
- **BasicTable** New `ApiTreeSelect` editing component
- Different backend home pages can be specified for different users.
- Add `homePath` field (optional) to the user information returned by the `getUserInfo` interface to customize the home page path for the current user
### 🐛 Bug Fixes
- **BasicTable**
- Fix scrollbar style issue (removed scroll style patch)
- Fix the alignment problem of cells with expanded icons in tree tables
- Add `headerTop` slot.
- Fix the color display of the operation column button in disabled state.
- Repair the problem that the values of editable cells cannot be updated by modifying `dataSource` directly.
- Repair the problem of data replay when using `ApiSelect` to edit components.
- Repair the problem that editing components may report `onXXX` type error in some scenarios.
- **TableAction**
- Create Tooltip component only if `action.tooltip` exists.
- Fix the problem that the content of the round button inside the component is not centered
- **AppSearch** Fix the problem that the hidden menu may be searched.
- **BasicUpload** Repair the problem of error when handling non-`array` values.
- **Form** Repair the `suffix` slot style problem of `FormItem`.
- **Menu**
- Repair the hovering trigger logic of the left mixed menu
- Repair the problem that the top bar menu is wrong when displaying menu items that need to be hidden.
- Fix the left mixed menu in hover trigger mode will jump to route directly when there is no submenu and it is activated
- **Breadcrumb** Repair the problem that the menu with redirection cannot be jumped when clicked
- **Markdown** fixes an initialization exception and an issue where value was not set dynamically correctly
- **Modal** Make sure props are passed correctly
- **MultipleTab** fixes an issue that could accidentally create login route tabs
- **BasicTree** Fix the problem that the search function may cause `checkedKeys` to be lost
- **CodeEditor** Fix the problem that value does not support v-model usage.
- **CountdownInput** Fix the problem that `input` slot is not supported.
- **ApiSelect** Fix the problem that the `options-change` event parameter is not the standard `options` data used by `select
- **Other**
- Fix the problem that the configuration of default menu collapse does not work
- Repair the problem that `safari` browser reports an error and the website cannot be opened.
- Repair the problem that eslint keeps error due to endOfLine after pulling the code on window.
- Fix `Vue Router warn` caused by dynamic routing
### 🎫 Chores
- Add test environment test command
## 2.6.0(2021-07-04)
### ✨ Features

View File

@ -1,3 +1,68 @@
## [2.6.1](https://github.com/anncwb/vue-vben-admin/compare/v2.6.0...v2.6.1) (2021-07-19)
### Bug Fixes
- **api-select:** fix `options-change` event data ([897bed9](https://github.com/anncwb/vue-vben-admin/commit/897bed97295a0b9101d33102340749689a4368de))
- **api-tree-select:** auto load data if necessary ([1b3058f](https://github.com/anncwb/vue-vben-admin/commit/1b3058f8253effe974feaf08a12250a111ab58c0))
- **api-tree-select:** fix `event` checked in form ([d9d0071](https://github.com/anncwb/vue-vben-admin/commit/d9d00714011fa7914c61f990ce1159351ee21a1a))
- **app-search:** exclude hidden items ([faf5c9f](https://github.com/anncwb/vue-vben-admin/commit/faf5c9fd7ea40c407419a5a5c473f9b0c32c2a53))
- **app-search:** exclude items by `hideChildrenInMenu` ([02d3dca](https://github.com/anncwb/vue-vben-admin/commit/02d3dca57efedc1322ae38e3f432cf1f6c2cf839))
- **basic-tree:** `checkedKeys` not worked with `search` ([b06a7ab](https://github.com/anncwb/vue-vben-admin/commit/b06a7ab77b99abee63dd55770ffd55b594ee42f9)), closes [#915](https://github.com/anncwb/vue-vben-admin/issues/915)
- **breadcrumb:** `redirect` not worked ([f5e31fe](https://github.com/anncwb/vue-vben-admin/commit/f5e31febbd18372a34166cac390b1d9b914fe80e))
- **code-editor:** `value` not support use as `v-model` ([8832a07](https://github.com/anncwb/vue-vben-admin/commit/8832a074dceb44f057c87289d3a99feef58c08fd)), closes [#933](https://github.com/anncwb/vue-vben-admin/issues/933)
- **countdown-input:** add `slots` support ([a764a95](https://github.com/anncwb/vue-vben-admin/commit/a764a95ae9a6cff831f75aa97b00724cadc48e92))
- **CountTo:** Fix displaying empty string when the value is 0 ([#864](https://github.com/anncwb/vue-vben-admin/issues/864)) ([82eb72b](https://github.com/anncwb/vue-vben-admin/commit/82eb72bbced931ba7f50069211f9511035ad09f4))
- **demo:** `setup` page route config ([d5d5c4b](https://github.com/anncwb/vue-vben-admin/commit/d5d5c4b4bfb3e3a5e54f9993966adc46a09a8b90))
- **demo:** add mock data `account detail` route ([993e19d](https://github.com/anncwb/vue-vben-admin/commit/993e19dcc319e2b4c68df2ab76174b7b4d7b0428)), closes [#858](https://github.com/anncwb/vue-vben-admin/issues/858)
- **demo:** fix display problem of editable table with `apiSelect` ([535bddd](https://github.com/anncwb/vue-vben-admin/commit/535bdddf91785e20295c18cf80c8a22cc2172681))
- **demo:** form pages support `keepAlive` ([9228282](https://github.com/anncwb/vue-vben-admin/commit/9228282ae27daaa246f42e441e27b1b05eb30464))
- **demo:** resolve `key not exist` warnings ([45a94e4](https://github.com/anncwb/vue-vben-admin/commit/45a94e41c1397b84d08373f84f766204d2488714))
- **form:** fix `suffix` slot style ([a9bbed1](https://github.com/anncwb/vue-vben-admin/commit/a9bbed19739376ab2bf67a14b04e872f14ca84cc))
- **formItem:** Fix labelcol type mismatch ([#903](https://github.com/anncwb/vue-vben-admin/issues/903)) ([03b17a8](https://github.com/anncwb/vue-vben-admin/commit/03b17a8f8bdb50322aa10e3b614bcc40b9e9dcc8))
- **markdown:** resolving markdown exceptions ([d95815b](https://github.com/anncwb/vue-vben-admin/commit/d95815b5031984e224140eb1b1d46e2dbf80abc1))
- **markdown:** set `value` error ([35e1347](https://github.com/anncwb/vue-vben-admin/commit/35e1347029e29a83a9648b6b398e6863cc40fca9))
- **menu:** display error when contains hidden items ([5ceeefd](https://github.com/anncwb/vue-vben-admin/commit/5ceeefd17d9ddc0e8844b900069b100f24d9c00e))
- **menu:** fix mix-menu incorrect jumping in `hover` mode ([cad021c](https://github.com/anncwb/vue-vben-admin/commit/cad021c34b71fa109640af75a0c2b72179e9e257))
- **mix-sider:** fix mix-sider hover logic ([0595a72](https://github.com/anncwb/vue-vben-admin/commit/0595a72da9c666af81a0916663e8e6a014e6fa69))
- **modal:** ensure that props are passed correctly,fix [#897](https://github.com/anncwb/vue-vben-admin/issues/897) ([ae7821e](https://github.com/anncwb/vue-vben-admin/commit/ae7821e29690bea8934ea724bfd0ae4e2cf30c77))
- **modal:** fixed `fullscreen` not worked ([5baaa58](https://github.com/anncwb/vue-vben-admin/commit/5baaa58581f22a915cda9fa39e4cb9f094254d3b)), closes [#918](https://github.com/anncwb/vue-vben-admin/issues/918)
- **model:** auto validate on value change ([f844017](https://github.com/anncwb/vue-vben-admin/commit/f8440175f35076073c9f53483cf6c0164d427ff4)), closes [#920](https://github.com/anncwb/vue-vben-admin/issues/920)
- **table:** fix index column style ([c7c0a7e](https://github.com/anncwb/vue-vben-admin/commit/c7c0a7e4c88a895000b1621981e4d4b2020c64b1))
- **table:** `value` show problem in editable cell ([61ce25b](https://github.com/anncwb/vue-vben-admin/commit/61ce25be1b40d7a0e26205ca6a6757c6c43fc21e)), closes [#922](https://github.com/anncwb/vue-vben-admin/issues/922)
- **table-action:** fixed icon `margin` without label ([dc51e6a](https://github.com/anncwb/vue-vben-admin/commit/dc51e6a8d4e4f2c97b387b37959944c9bb49d779))
- **tree:** fixed `checkedKeys` with `search` mode ([f707541](https://github.com/anncwb/vue-vben-admin/commit/f707541dda78146bda89814ddccbb259d9f5d8a2))
- fix homePage affix error ([c117802](https://github.com/anncwb/vue-vben-admin/commit/c1178027f0fab2791d02efcd7c52beff5fc7dc25))
- **table-action:** fix `circle` button style ([db7254a](https://github.com/anncwb/vue-vben-admin/commit/db7254a5e0ac6d10a7ea37334ad523b150facb19))
- `menuSetting` can not set collapsed to false as default ([808291b](https://github.com/anncwb/vue-vben-admin/commit/808291b503d59e3026f5f0b5e7a38b9c69bcc451))
- ensure PAGE_NOT_FOUND_ROUTE exist ([87583c8](https://github.com/anncwb/vue-vben-admin/commit/87583c8b54d335ddf1c416859ef62bbde189c809))
- ensure that safari is running properly, fix [#875](https://github.com/anncwb/vue-vben-admin/issues/875) ([dafcdd8](https://github.com/anncwb/vue-vben-admin/commit/dafcdd898caae57104f1155b0ec660ea333e7b19))
- infinite redirect in `BACK` mode ([4b46a84](https://github.com/anncwb/vue-vben-admin/commit/4b46a84c2b85e8da799426c54b3381ae93183db4))
- **multiple-tab:** ignore login page ([1e63379](https://github.com/anncwb/vue-vben-admin/commit/1e63379088e1d7c823f29f607ab49d62ca22cb25))
- resolving `Vue Router warn` ([237e65e](https://github.com/anncwb/vue-vben-admin/commit/237e65eac909368c4b4857da6c8deb1dc18e7684))
- **table:** fix tree node align ([1e61da6](https://github.com/anncwb/vue-vben-admin/commit/1e61da644f65a79ce10fde98ee017aba7d36be10)), closes [#829](https://github.com/anncwb/vue-vben-admin/issues/829)
- **table:** scrollbar style ([d8c3820](https://github.com/anncwb/vue-vben-admin/commit/d8c38207c08510d805a8dc66ffbba210e0cf4215))
- **table-action:** incorrect button color of `disabled` state ([0f28e80](https://github.com/anncwb/vue-vben-admin/commit/0f28e803d0b65537216cd9f40ad5cad63c20db9b)), closes [#891](https://github.com/anncwb/vue-vben-admin/issues/891)
- **upload:** ensure the value type is correct ([05329ce](https://github.com/anncwb/vue-vben-admin/commit/05329ce9501eb899a0bbb45320e5807c83372317))
- **useWatermark:** fix `func` call `createWatermark` call `clear` to resizeEvent removed ([#901](https://github.com/anncwb/vue-vben-admin/issues/901)) ([a1d956d](https://github.com/anncwb/vue-vben-admin/commit/a1d956d3697cd07e0ba8910768f2a73e55f18491))
### Features
- **api-tree-select:** add `api` options to tree-select ([d81db89](https://github.com/anncwb/vue-vben-admin/commit/d81db890dfeb533d60f378ddb86f8ac50a31252b))
- **basic-table:** add `ApiTreeSelect` edit component ([52af1dd](https://github.com/anncwb/vue-vben-admin/commit/52af1dd0d494e66c0af20f886dcc2b983cbb096f))
- **demo:** multi-modal in one page usage ([7a7dab0](https://github.com/anncwb/vue-vben-admin/commit/7a7dab0c4b3602b7bd3e9381408e4168d7494c52))
- customized user home page ([0a3683a](https://github.com/anncwb/vue-vben-admin/commit/0a3683a186ab55d34a12a5a3c6d794dfa1094ad4))
- **api-select:** clear options before fetch ([9cf070d](https://github.com/anncwb/vue-vben-admin/commit/9cf070dd6305bb69a67ab6be85ef00bddc86fda0))
- **demo:** add basicTree with async data expand all ([5421211](https://github.com/anncwb/vue-vben-admin/commit/542121129eb5bf65f61e7b484835591756c80f04))
- **demo:** add search demo for apiSelect ([41e6d94](https://github.com/anncwb/vue-vben-admin/commit/41e6d94b3b64dc0d40b7ec57ecfaa4d966f202ae))
- **demo:** demo default expanded tree table ([5f1a6cd](https://github.com/anncwb/vue-vben-admin/commit/5f1a6cdc599d5840df2dfebdaad029aac093cd81))
- **notice-list:** add `pagination` support ([c16be2c](https://github.com/anncwb/vue-vben-admin/commit/c16be2c499d90126dfa35d699da9294c21a4ab48)), closes [#894](https://github.com/anncwb/vue-vben-admin/issues/894)
- **table:** add `headerTop` slot ([540423e](https://github.com/anncwb/vue-vben-admin/commit/540423ecf741a815d28d7a6baa1541ac884efe95)), closes [#881](https://github.com/anncwb/vue-vben-admin/issues/881)
### Performance Improvements
- **menu:** Optimize the style of the bottom collapse button in the Mix menu layout ([#896](https://github.com/anncwb/vue-vben-admin/issues/896)) ([6f83070](https://github.com/anncwb/vue-vben-admin/commit/6f830703a2607c33e5d25d6d17d0e453fc2fac2e))
- image compression configuration optimization ([cf840e3](https://github.com/anncwb/vue-vben-admin/commit/cf840e3e73b9572de0ba7bf7b32d83f6a353a8ad))
# [2.6.0](https://github.com/anncwb/vue-vben-admin/compare/v2.5.2...v2.6.0) (2021-07-04)
### Bug Fixes

View File

@ -1,3 +1,52 @@
## 2.6.1(2021-07-19)
### ✨ Features
- **NoticeList** 添加分页、超长自动省略、标题点击事件、标题删除线等功能
- **MixSider** 优化 Mix 菜单布局时 底部折叠按钮 的样式,与其它菜单布局时的风格保持一致
- **ApiTreeSelect** 扩展`antdv`的`TreeSelect`组件,支持远程数据源,用法类似`ApiSelect`
- **BasicTable** 新增`ApiTreeSelect`编辑组件
- 可以为不同的用户指定不同的后台首页:
- 在`getUserInfo`接口返回的用户信息中增加`homePath`字段(可选)即可为当前用户定制首页路径
### 🐛 Bug Fixes
- **BasicTable**
- 修复滚动条样式问题(移除了滚动样式补丁)
- 修复树形表格的带有展开图标的单元格的内容对齐问题
- 新增`headerTop`插槽
- 修复操作列的按钮在 disabled 状态下的颜色显示
- 修复可编辑单元格的值不能直接通过修改`dataSource`来更新显示的问题
- 修复使用`ApiSelect`编辑组件时的数据回显问题
- 修复在部分场景下编辑组件可能会报`onXXX`类型错误的问题
- **TableAction**
- 仅在 `action.tooltip`存在的情况下 才创建 Tooltip 组件
- 修复组件内的圆形按钮内容没有居中的问题
- **AppSearch** 修复可能会搜索隐藏菜单的问题
- **BasicUpload** 修复处理非`array`值时报错的问题
- **Form** 修复`FormItem`的`suffix`插槽样式问题
- **Menu**
- 修复左侧混合菜单的悬停触发逻辑
- 修复顶栏菜单在显示包含需要隐藏的菜单项目时出错的问题
- 修复悬停触发模式下左侧混合菜单会在没有子菜单且被激活时直接跳转路由
- **Breadcrumb** 修复带有重定向的菜单点击无法跳转的问题
- **Markdown** 修复初始化异常以及不能正确地动态设置 value 的问题
- **Modal** 确保 props 正确被传递
- **MultipleTab** 修复可能会意外创建登录路由标签的问题
- **BasicTree** 修复搜索功能可能导致`checkedKeys`丢失的问题
- **CodeEditor** 修复 value 不支持 v-model 用法的问题
- **CountdownInput** 修复不支持`input`插槽的问题
- **ApiSelect** 修复`options-change`事件参数不是`select`所使用的标准`options`数据的问题
- **其它**
- 修复菜单默认折叠的配置不起作用的问题
- 修复`safari`浏览器报错导致网站打不开
- 修复在 window 上,拉取代码后 eslint 因 endOfLine 而保错问题
- 修复因动态路由而产生的 `Vue Router warn`
### 🎫 Chores
- 添加 test 环境测试命令
## 2.6.0(2021-07-04)
### ✨ Features

View File

@ -44,7 +44,7 @@ export function wrapperEnv(envConf: Recordable): ViteEnv {
*/
function getConfFiles() {
const script = process.env.npm_lifecycle_script;
const reg = new RegExp('--mode ([a-z]+) ');
const reg = new RegExp('--mode ([a-z]+)');
const result = reg.exec(script as string) as any;
if (result) {
const mode = result[1] as string;

View File

@ -13,7 +13,7 @@ export function configImageminPlugin() {
optimizationLevel: 7,
},
mozjpeg: {
quality: 8,
quality: 20,
},
pngquant: {
quality: [0.8, 0.9],
@ -22,10 +22,11 @@ export function configImageminPlugin() {
svgo: {
plugins: [
{
removeViewBox: false,
name: 'removeViewBox',
},
{
removeEmptyAttrs: false,
name: 'removeEmptyAttrs',
active: false,
},
],
},

View File

@ -1,31 +1,6 @@
module.exports = {
ignores: [(commit) => commit.includes('init')],
extends: ['@commitlint/config-conventional'],
parserPreset: {
parserOpts: {
headerPattern: /^(\w*|[\u4e00-\u9fa5]*)(?:[\(\](.*)[\)\])?[\:\] (.*)/,
headerCorrespondence: ['type', 'scope', 'subject'],
referenceActions: [
'close',
'closes',
'closed',
'fix',
'fixes',
'fixed',
'resolve',
'resolves',
'resolved',
],
issuePrefixes: ['#'],
noteKeywords: ['BREAKING CHANGE'],
fieldPattern: /^-(.*?)-$/,
revertPattern: /^Revert\s"([\s\S]*)"\s*This reverts commit (\w*)\./,
revertCorrespondence: ['header', 'hash'],
warn() {},
mergePattern: null,
mergeCorrespondence: null,
},
},
rules: {
'body-leading-blank': [2, 'always'],
'footer-leading-blank': [1, 'always'],

View File

@ -1,28 +1,28 @@
import { MockMethod } from 'vite-plugin-mock';
import { resultSuccess } from '../_util';
const list: any[] = [];
const demoList = (() => {
const demoList = (keyword) => {
const result = {
list: list,
list: [] as any[],
};
for (let index = 0; index < 20; index++) {
result.list.push({
name: `选项${index}`,
name: `${keyword ?? ''}选项${index}`,
id: `${index}`,
});
}
return result;
})();
};
export default [
{
url: '/basic-api/select/getDemoOptions',
timeout: 1000,
method: 'post',
method: 'get',
response: ({ query }) => {
console.log(query);
return resultSuccess(demoList);
const { keyword } = query;
console.log(keyword);
return resultSuccess(demoList(keyword));
},
},
] as MockMethod[];

38
mock/demo/tree-demo.ts Normal file
View File

@ -0,0 +1,38 @@
import { MockMethod } from 'vite-plugin-mock';
import { resultSuccess } from '../_util';
const demoTreeList = (keyword) => {
const result = {
list: [] as Recordable[],
};
for (let index = 0; index < 5; index++) {
const children: Recordable[] = [];
for (let j = 0; j < 3; j++) {
children.push({
title: `${keyword ?? ''}选项${index}-${j}`,
value: `${index}-${j}`,
key: `${index}-${j}`,
});
}
result.list.push({
title: `${keyword ?? ''}选项${index}`,
value: `${index}`,
key: `${index}`,
children,
});
}
return result;
};
export default [
{
url: '/basic-api/tree/getDemoOptions',
timeout: 1000,
method: 'get',
response: ({ query }) => {
const { keyword } = query;
console.log(keyword);
return resultSuccess(demoTreeList(keyword));
},
},
] as MockMethod[];

View File

@ -5,13 +5,40 @@ import { createFakeUserList } from './user';
// single
const dashboardRoute = {
path: '/dashboard',
name: 'Welcome',
component: '/dashboard/analysis/index',
name: 'Dashboard',
component: 'LAYOUT',
redirect: '/dashboard/analysis',
meta: {
title: 'routes.dashboard.analysis',
affix: true,
title: 'routes.dashboard.dashboard',
hideChildrenInMenu: true,
icon: 'bx:bx-home',
},
children: [
{
path: 'analysis',
name: 'Analysis',
component: '/dashboard/analysis/index',
meta: {
hideMenu: true,
hideBreadcrumb: true,
title: 'routes.dashboard.analysis',
currentActiveMenu: '/dashboard',
icon: 'bx:bx-home',
},
},
{
path: 'workbench',
name: 'Workbench',
component: '/dashboard/workbench/index',
meta: {
hideMenu: true,
hideBreadcrumb: true,
title: 'routes.dashboard.workbench',
currentActiveMenu: '/dashboard',
icon: 'bx:bx-home',
},
},
],
};
const backRoute = {
@ -128,6 +155,18 @@ const sysRoute = {
},
component: '/demo/system/account/index',
},
{
path: 'account_detail/:id',
name: 'AccountDetail',
meta: {
hideMenu: true,
title: 'routes.demo.system.account_detail',
ignoreKeepAlive: true,
showMenu: false,
currentActiveMenu: '/system/account',
},
component: '/demo/system/account/AccountDetail',
},
{
path: 'role',
name: 'RoleManagement',
@ -211,12 +250,21 @@ export default [
return resultError('Invalid user token!');
}
const id = checkUser.userId;
if (!id || id === '1') {
return resultSuccess([dashboardRoute, authRoute, levelRoute, sysRoute, linkRoute]);
}
if (id === '2') {
return resultSuccess([dashboardRoute, authRoute, levelRoute, linkRoute]);
let menu: Object[];
switch (id) {
case '1':
dashboardRoute.redirect = dashboardRoute.path + '/' + dashboardRoute.children[0].path;
menu = [dashboardRoute, authRoute, levelRoute, sysRoute, linkRoute];
break;
case '2':
dashboardRoute.redirect = dashboardRoute.path + '/' + dashboardRoute.children[1].path;
menu = [dashboardRoute, authRoute, levelRoute, linkRoute];
break;
default:
menu = [];
}
return resultSuccess(menu);
},
},
] as MockMethod[];

View File

@ -11,6 +11,7 @@ export function createFakeUserList() {
desc: 'manager',
password: '123456',
token: 'fakeToken1',
homePath: '/dashboard/analysis',
roles: [
{
roleName: 'Super Admin',
@ -26,6 +27,7 @@ export function createFakeUserList() {
avatar: 'https://q1.qlogo.cn/g?b=qq&nk=339449197&s=640',
desc: 'tester',
token: 'fakeToken2',
homePath: '/dashboard/workbench',
roles: [
{
roleName: 'Tester',

View File

@ -1,6 +1,6 @@
{
"name": "vben-admin",
"version": "2.6.0",
"version": "2.6.1",
"author": {
"name": "vben",
"email": "anncwb@126.com",
@ -11,6 +11,7 @@
"serve": "npm run dev",
"dev": "vite",
"build": "cross-env NODE_ENV=production vite build && esno ./build/script/postBuild.ts",
"build:test": "cross-env vite build --mode test && esno ./build/script/postBuild.ts",
"build:no-cache": "yarn clean:cache && npm run build",
"report": "cross-env REPORT=true npm run build",
"type:check": "vue-tsc --noEmit --skipLibCheck",
@ -20,7 +21,7 @@
"clean:cache": "rimraf node_modules/.cache/ && rimraf node_modules/.vite",
"clean:lib": "rimraf node_modules",
"lint:eslint": "eslint --cache --max-warnings 0 \"{src,mock}/**/*.{vue,ts,tsx}\" --fix",
"lint:prettier": "prettier --write --loglevel warn \"src/**/*.{js,json,tsx,css,less,scss,vue,html,md}\"",
"lint:prettier": "prettier --write \"src/**/*.{js,json,tsx,css,less,scss,vue,html,md}\"",
"lint:stylelint": "stylelint --cache --fix \"**/*.{vue,less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/",
"lint:lint-staged": "lint-staged -c ./.husky/lintstagedrc.js",
"lint:pretty": "pretty-quick --staged",
@ -34,11 +35,11 @@
},
"dependencies": {
"@iconify/iconify": "^2.0.3",
"@logicflow/core": "^0.5.0",
"@logicflow/extension": "^0.5.0",
"@vueuse/core": "^5.0.3",
"@logicflow/core": "^0.6.1",
"@logicflow/extension": "^0.6.1",
"@vueuse/core": "^5.1.4",
"@zxcvbn-ts/core": "^1.0.0-beta.0",
"ant-design-vue": "2.2.0-rc.1",
"ant-design-vue": "2.2.2",
"axios": "^0.21.1",
"crypto-js": "^4.0.0",
"echarts": "^5.1.2",
@ -46,11 +47,11 @@
"mockjs": "^1.1.0",
"nprogress": "^0.2.0",
"path-to-regexp": "^6.2.0",
"pinia": "^2.0.0-beta.3",
"pinia": "^2.0.0-beta.5",
"qrcode": "^1.4.4",
"resize-observer-polyfill": "^1.5.1",
"sortablejs": "^1.13.0",
"vue": "3.1.4",
"sortablejs": "^1.14.0",
"vue": "3.1.5",
"vue-i18n": "9.1.6",
"vue-router": "^4.0.10",
"vue-types": "^4.0.0"
@ -58,79 +59,79 @@
"devDependencies": {
"@commitlint/cli": "^12.1.4",
"@commitlint/config-conventional": "^12.1.4",
"@iconify/json": "^1.1.369",
"@iconify/json": "^1.1.374",
"@purge-icons/generated": "^0.7.0",
"@types/codemirror": "^5.60.1",
"@types/crypto-js": "^4.0.1",
"@types/fs-extra": "^9.0.11",
"@types/inquirer": "^7.3.2",
"@types/intro.js": "^3.0.1",
"@types/jest": "^26.0.23",
"@types/codemirror": "^5.60.2",
"@types/crypto-js": "^4.0.2",
"@types/fs-extra": "^9.0.12",
"@types/inquirer": "^7.3.3",
"@types/intro.js": "^3.0.2",
"@types/jest": "^26.0.24",
"@types/lodash-es": "^4.17.4",
"@types/mockjs": "^1.0.3",
"@types/node": "^16.0.0",
"@types/mockjs": "^1.0.4",
"@types/node": "^16.3.3",
"@types/nprogress": "^0.2.0",
"@types/qrcode": "^1.4.0",
"@types/qs": "^6.9.6",
"@types/sortablejs": "^1.10.6",
"@typescript-eslint/eslint-plugin": "^4.28.1",
"@typescript-eslint/parser": "^4.28.1",
"@vitejs/plugin-legacy": "^1.4.3",
"@vitejs/plugin-vue": "^1.2.4",
"@types/qrcode": "^1.4.1",
"@types/qs": "^6.9.7",
"@types/sortablejs": "^1.10.7",
"@typescript-eslint/eslint-plugin": "^4.28.3",
"@typescript-eslint/parser": "^4.28.3",
"@vitejs/plugin-legacy": "^1.4.4",
"@vitejs/plugin-vue": "^1.2.5",
"@vitejs/plugin-vue-jsx": "^1.1.6",
"@vue/compiler-sfc": "3.1.4",
"@vue/test-utils": "^2.0.0-rc.9",
"autoprefixer": "^10.2.6",
"@vue/compiler-sfc": "3.1.5",
"@vue/test-utils": "^2.0.0-rc.10",
"autoprefixer": "^10.3.1",
"commitizen": "^4.2.4",
"conventional-changelog-cli": "^2.1.1",
"cross-env": "^7.0.3",
"dotenv": "^10.0.0",
"eslint": "^7.30.0",
"eslint": "^7.31.0",
"eslint-config-prettier": "^8.3.0",
"eslint-define-config": "^1.0.9",
"eslint-plugin-jest": "^24.3.6",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-vue": "^7.12.1",
"eslint-plugin-vue": "^7.14.0",
"esno": "^0.7.3",
"fs-extra": "^10.0.0",
"http-server": "^0.12.3",
"husky": "^7.0.0",
"inquirer": "^8.1.1",
"husky": "^7.0.1",
"inquirer": "^8.1.2",
"is-ci": "^3.0.0",
"jest": "^27.0.6",
"less": "^4.1.1",
"lint-staged": "^11.0.0",
"lint-staged": "^11.0.1",
"npm-run-all": "^4.1.5",
"postcss": "^8.3.5",
"prettier": "^2.3.2",
"pretty-quick": "^3.1.1",
"rimraf": "^3.0.2",
"rollup-plugin-visualizer": "5.5.1",
"rollup-plugin-visualizer": "5.5.2",
"stylelint": "^13.13.1",
"stylelint-config-prettier": "^8.0.2",
"stylelint-config-standard": "^22.0.0",
"stylelint-order": "^4.1.0",
"tailwindcss": "^2.2.4",
"ts-jest": "^27.0.3",
"ts-node": "^10.0.0",
"ts-node": "^10.1.0",
"typescript": "4.3.5",
"vite": "2.4.0-beta.2",
"vite-plugin-compression": "^0.2.5",
"vite": "2.3.6",
"vite-plugin-compression": "^0.3.1",
"vite-plugin-html": "^2.0.7",
"vite-plugin-imagemin": "^0.3.2",
"vite-plugin-mock": "^2.8.0",
"vite-plugin-imagemin": "^0.4.1",
"vite-plugin-mock": "^2.9.3",
"vite-plugin-purge-icons": "^0.7.0",
"vite-plugin-pwa": "^0.8.1",
"vite-plugin-style-import": "^1.0.1",
"vite-plugin-svg-icons": "^1.0.0",
"vite-plugin-svg-icons": "^1.0.1",
"vite-plugin-theme": "^0.8.1",
"vue-eslint-parser": "^7.7.2",
"vue-tsc": "^0.2.0"
"vue-eslint-parser": "^7.9.0",
"vue-tsc": "^0.2.1"
},
"resolutions": {
"//": "Used to install imagemin dependencies, because imagemin may not be installed in China. If it is abroad, you can delete it",
"bin-wrapper": "npm:bin-wrapper-china",
"rollup": "^2.52.7"
"rollup": "^2.53.2"
},
"repository": {
"type": "git",

View File

@ -15,6 +15,6 @@ module.exports = {
requirePragma: false,
proseWrap: 'never',
htmlWhitespaceSensitivity: 'strict',
endOfLine: 'lf',
endOfLine: 'auto',
rangeStart: 0,
};

11
src/api/demo/tree.ts Normal file
View File

@ -0,0 +1,11 @@
import { defHttp } from '/@/utils/http/axios';
enum Api {
TREE_OPTIONS_LIST = '/tree/getDemoOptions',
}
/**
* @description: Get sample options value
*/
export const treeOptionsListApi = (params?: Recordable) =>
defHttp.get<Recordable[]>({ url: Api.TREE_OPTIONS_LIST, params });

View File

@ -17,6 +17,7 @@
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
import { useDesign } from '/@/hooks/web/useDesign';
import { PageEnum } from '/@/enums/pageEnum';
import { useUserStore } from '/@/store/modules/user';
const props = {
/**
@ -39,6 +40,7 @@
setup(props) {
const { prefixCls } = useDesign('app-logo');
const { getCollapsedShowTitle } = useMenuSetting();
const userStore = useUserStore();
const { title } = useGlobSetting();
const go = useGo();
@ -56,7 +58,7 @@
]);
function goHome() {
go(PageEnum.BASE_HOME);
go(userStore.getUserInfo.homePath || PageEnum.BASE_HOME);
}
return {

View File

@ -55,7 +55,7 @@ export function useMenuSearch(refs: Ref<HTMLElement[]>, scrollWrap: Ref<ElRef>,
}
const reg = createSearchReg(unref(keyword));
const filterMenu = filter(menuList, (item) => {
return reg.test(item.name);
return reg.test(item.name) && !item.hideMenu;
});
searchResult.value = handlerSearchResult(filterMenu, reg);
activeIndex.value = 0;
@ -64,15 +64,15 @@ export function useMenuSearch(refs: Ref<HTMLElement[]>, scrollWrap: Ref<ElRef>,
function handlerSearchResult(filterMenu: Menu[], reg: RegExp, parent?: Menu) {
const ret: SearchResult[] = [];
filterMenu.forEach((item) => {
const { name, path, icon, children } = item;
if (reg.test(name) && !children?.length) {
const { name, path, icon, children, hideMenu, meta } = item;
if (!hideMenu && reg.test(name) && (!children?.length || meta?.hideChildrenInMenu)) {
ret.push({
name: parent?.name ? `${parent.name} > ${name}` : name,
path,
icon,
});
}
if (Array.isArray(children) && children.length) {
if (!meta?.hideChildrenInMenu && Array.isArray(children) && children.length) {
ret.push(...handlerSearchResult(children, reg, item));
}
});

View File

@ -1,5 +1,5 @@
<script lang="ts">
import { defineComponent, h, unref, computed } from 'vue';
import { computed, defineComponent, h, unref } from 'vue';
import BasicButton from './BasicButton.vue';
import { Popconfirm } from 'ant-design-vue';
import { extendSlots } from '/@/utils/helper/tsxHelper';
@ -29,19 +29,20 @@
// get inherit binding value
const getBindValues = computed(() => {
const popValues = Object.assign(
return Object.assign(
{
okText: t('common.okText'),
cancelText: t('common.cancelText'),
},
{ ...props, ...unref(attrs) }
);
return popValues;
});
return () => {
const bindValues = omit(unref(getBindValues), 'icon');
const Button = h(BasicButton, omit(bindValues, 'title'), extendSlots(slots));
const btnBind = omit(bindValues, 'title');
if (btnBind.disabled) btnBind.color = '';
const Button = h(BasicButton, btnBind, extendSlots(slots));
// If it is not enabled, it is a normal button
if (!props.enable) {

View File

@ -29,7 +29,7 @@
name: 'CodeEditor',
components: { CodeMirrorEditor },
props,
emits: ['change'],
emits: ['change', 'update:value'],
setup(props, { emit }) {
const getValue = computed(() => {
const { value, mode } = props;
@ -42,6 +42,7 @@
});
function handleValueChange(v) {
emit('update:value', v);
emit('change', v);
}

View File

@ -3,6 +3,9 @@
<template #addonAfter>
<CountButton :size="size" :count="count" :value="state" :beforeStartFunc="sendCodeApi" />
</template>
<template #[item]="data" v-for="item in Object.keys($slots).filter((k) => k !== 'addonAfter')">
<slot :name="item" v-bind="data"></slot>
</template>
</a-input>
</template>
<script lang="ts">

View File

@ -84,7 +84,7 @@
}
function formatNumber(num: number | string) {
if (!num) {
if (!num && num !== 0) {
return '';
}
const { decimals, decimal, separator, suffix, prefix } = props;

View File

@ -8,5 +8,6 @@ export { useForm } from './src/hooks/useForm';
export { default as ApiSelect } from './src/components/ApiSelect.vue';
export { default as RadioButtonGroup } from './src/components/RadioButtonGroup.vue';
export { default as ApiTreeSelect } from './src/components/ApiTreeSelect.vue';
export { BasicForm };

View File

@ -229,6 +229,12 @@
function setFormModel(key: string, value: any) {
formModel[key] = value;
const { validateTrigger } = unref(getBindValue);
if (!validateTrigger || validateTrigger === 'change') {
try {
validateFields([key]);
} catch (e) {}
}
}
function handleEnterPress(e: KeyboardEvent) {

View File

@ -22,6 +22,7 @@ import {
import RadioButtonGroup from './components/RadioButtonGroup.vue';
import ApiSelect from './components/ApiSelect.vue';
import ApiTreeSelect from './components/ApiTreeSelect.vue';
import { BasicUpload } from '/@/components/Upload';
import { StrengthMeter } from '/@/components/StrengthMeter';
import { IconPicker } from '/@/components/Icon';
@ -40,6 +41,7 @@ componentMap.set('AutoComplete', AutoComplete);
componentMap.set('Select', Select);
componentMap.set('ApiSelect', ApiSelect);
componentMap.set('TreeSelect', TreeSelect);
componentMap.set('ApiTreeSelect', ApiTreeSelect);
componentMap.set('Switch', Switch);
componentMap.set('RadioButtonGroup', RadioButtonGroup);
componentMap.set('RadioGroup', Radio.Group);

View File

@ -106,7 +106,7 @@
async function fetch() {
const api = props.api;
if (!api || !isFunction(api)) return;
options.value = [];
try {
loading.value = true;
const res = await api(props.params);
@ -134,7 +134,7 @@
}
function emitChange() {
emit('options-change', unref(options));
emit('options-change', unref(getOptions));
}
function handleChange(_, ...args) {

View File

@ -0,0 +1,85 @@
<template>
<a-tree-select v-bind="getAttrs" @change="handleChange">
<template #[item]="data" v-for="item in Object.keys($slots)">
<slot :name="item" v-bind="data"></slot>
</template>
<template #suffixIcon v-if="loading">
<LoadingOutlined spin />
</template>
</a-tree-select>
</template>
<script lang="ts">
import { computed, defineComponent, watch, ref, onMounted, unref } from 'vue';
import { TreeSelect } from 'ant-design-vue';
import { isArray, isFunction } from '/@/utils/is';
import { get } from 'lodash-es';
import { propTypes } from '/@/utils/propTypes';
import { LoadingOutlined } from '@ant-design/icons-vue';
export default defineComponent({
name: 'ApiTreeSelect',
components: { ATreeSelect: TreeSelect, LoadingOutlined },
props: {
api: { type: Function as PropType<(arg?: Recordable) => Promise<Recordable>> },
params: { type: Object },
immediate: { type: Boolean, default: true },
resultField: propTypes.string.def(''),
},
emits: ['options-change', 'change'],
setup(props, { attrs, emit }) {
const treeData = ref<Recordable[]>([]);
const isFirstLoaded = ref<Boolean>(false);
const loading = ref(false);
const getAttrs = computed(() => {
return {
...(props.api ? { treeData: unref(treeData) } : {}),
...attrs,
};
});
function handleChange(...args) {
emit('change', ...args);
}
watch(
() => props.params,
() => {
isFirstLoaded.value && fetch();
}
);
watch(
() => props.immediate,
(v) => {
v && !isFirstLoaded.value && fetch();
}
);
onMounted(() => {
props.immediate && fetch();
});
async function fetch() {
const { api } = props;
if (!api || !isFunction(api)) return;
loading.value = true;
treeData.value = [];
let result;
try {
result = await api(props.params);
} catch (e) {
console.error(e);
}
loading.value = false;
if (!result) return;
if (!isArray(result)) {
result = get(result, props.resultField);
}
treeData.value = (result as Recordable[]) || [];
isFirstLoaded.value = true;
emit('options-change', treeData.value);
}
return { getAttrs, loading, handleChange };
},
});
</script>

View File

@ -1,40 +1,42 @@
<template>
<a-col v-bind="actionColOpt" :style="{ textAlign: 'right' }" v-if="showActionButtonGroup">
<FormItem>
<slot name="resetBefore"></slot>
<Button
type="default"
class="mr-2"
v-bind="getResetBtnOptions"
@click="resetAction"
v-if="showResetButton"
>
{{ getResetBtnOptions.text }}
</Button>
<slot name="submitBefore"></slot>
<div style="width: 100%; text-align: right">
<FormItem>
<slot name="resetBefore"></slot>
<Button
type="default"
class="mr-2"
v-bind="getResetBtnOptions"
@click="resetAction"
v-if="showResetButton"
>
{{ getResetBtnOptions.text }}
</Button>
<slot name="submitBefore"></slot>
<Button
type="primary"
class="mr-2"
v-bind="getSubmitBtnOptions"
@click="submitAction"
v-if="showSubmitButton"
>
{{ getSubmitBtnOptions.text }}
</Button>
<Button
type="primary"
class="mr-2"
v-bind="getSubmitBtnOptions"
@click="submitAction"
v-if="showSubmitButton"
>
{{ getSubmitBtnOptions.text }}
</Button>
<slot name="advanceBefore"></slot>
<Button
type="link"
size="small"
@click="toggleAdvanced"
v-if="showAdvancedButton && !hideAdvanceBtn"
>
{{ isAdvanced ? t('component.form.putAway') : t('component.form.unfold') }}
<BasicArrow class="ml-1" :expand="!isAdvanced" up />
</Button>
<slot name="advanceAfter"></slot>
</FormItem>
<slot name="advanceBefore"></slot>
<Button
type="link"
size="small"
@click="toggleAdvanced"
v-if="showAdvancedButton && !hideAdvanceBtn"
>
{{ isAdvanced ? t('component.form.putAway') : t('component.form.unfold') }}
<BasicArrow class="ml-1" :expand="!isAdvanced" up />
</Button>
<slot name="advanceAfter"></slot>
</FormItem>
</div>
</a-col>
</template>
<script lang="ts">
@ -43,7 +45,7 @@
import { defineComponent, computed, PropType } from 'vue';
import { Form, Col } from 'ant-design-vue';
import { Button, ButtonProps } from '/@/components/Button';
import { BasicArrow } from '/@/components/Basic/index';
import { BasicArrow } from '/@/components/Basic';
import { useFormContext } from '../hooks/useFormContext';
import { useI18n } from '/@/hooks/web/useI18n';
import { propTypes } from '/@/utils/propTypes';

View File

@ -326,10 +326,10 @@
labelCol={labelCol}
wrapperCol={wrapperCol}
>
<>
{getContent()}
<div style="display:flex">
<div style="flex:1">{getContent()}</div>
{showSuffix && <span class="suffix">{getSuffix}</span>}
</>
</div>
</Form.Item>
);
}

View File

@ -1,6 +1,6 @@
import type { NamePath } from 'ant-design-vue/lib/form/interface';
import type { ColProps } from 'ant-design-vue/lib/grid/Col';
import type { VNodeChild } from 'vue';
import type { HTMLAttributes, VNodeChild } from 'vue';
export interface FormItem {
/**
@ -39,7 +39,7 @@ export interface FormItem {
* The layout of label. You can set span offset to something like {span: 3, offset: 12} or sm: {span: 3, offset: 12} same as with <Col>
* @type Col
*/
labelCol?: ColProps;
labelCol?: ColProps & HTMLAttributes;
/**
* Whether provided or not, it will be generated by the validation rule.

View File

@ -91,6 +91,7 @@ export type ComponentType =
| 'Select'
| 'ApiSelect'
| 'TreeSelect'
| 'ApiTreeSelect'
| 'RadioButtonGroup'
| 'RadioGroup'
| 'Checkbox'

View File

@ -5,18 +5,19 @@
import {
defineComponent,
ref,
onMounted,
unref,
onUnmounted,
nextTick,
computed,
watch,
onBeforeUnmount,
onDeactivated,
} from 'vue';
import Vditor from 'vditor';
import 'vditor/dist/index.css';
import { useLocale } from '/@/locales/useLocale';
import { useModalContext } from '../../Modal';
import { useRootSetting } from '/@/hooks/setting/useRootSetting';
import { onMountedOrActivated } from '/@/hooks/core/onMountedOrActivated';
type Lang = 'zh_CN' | 'en_US' | 'ja_JP' | 'ko_KR' | undefined;
@ -26,7 +27,7 @@
height: { type: Number, default: 360 },
value: { type: String, default: '' },
},
emits: ['change', 'get'],
emits: ['change', 'get', 'update:value'],
setup(props, { attrs, emit }) {
const wrapRef = ref<ElRef>(null);
const vditorRef = ref<Nullable<Vditor>>(null);
@ -36,17 +37,16 @@
const { getLocale } = useLocale();
const { getDarkMode } = useRootSetting();
const valueRef = ref('');
watch(
[() => getDarkMode.value, () => initedRef.value],
([val]) => {
const vditor = unref(vditorRef);
if (!vditor) {
([val, inited]) => {
if (!inited) {
return;
}
const theme = val === 'dark' ? 'dark' : undefined;
vditor.setTheme(theme as 'dark');
const theme = val === 'dark' ? 'dark' : 'classic';
instance.getVditor()?.setTheme(theme);
},
{
immediate: true,
@ -54,6 +54,16 @@
}
);
watch(
() => props.value,
(v) => {
if (v !== valueRef.value) {
instance.getVditor()?.setValue(v);
}
valueRef.value = v;
}
);
const getCurrentLang = computed((): 'zh_CN' | 'en_US' | 'ja_JP' | 'ko_KR' => {
let lang: Lang;
switch (unref(getLocale)) {
@ -72,54 +82,58 @@
return lang;
});
function init() {
const wrapEl = unref(wrapRef);
const wrapEl = unref(wrapRef) as HTMLElement;
if (!wrapEl) return;
const bindValue = { ...attrs, ...props };
vditorRef.value = new Vditor(wrapEl, {
theme: 'classic',
const insEditor = new Vditor(wrapEl, {
theme: getDarkMode.value === 'dark' ? 'dark' : 'classic',
lang: unref(getCurrentLang),
mode: 'sv',
preview: {
actions: [],
},
input: (v) => {
// emit('update:value', v);
valueRef.value = v;
emit('update:value', v);
emit('change', v);
},
after: () => {
nextTick(() => {
modalFn?.redoModalHeight?.();
insEditor.setValue(valueRef.value);
vditorRef.value = insEditor;
initedRef.value = true;
emit('get', instance);
});
},
blur: () => {
unref(vditorRef)?.setValue(props.value);
//unref(vditorRef)?.setValue(props.value);
},
...bindValue,
cache: {
enable: false,
},
});
initedRef.value = true;
}
const instance = {
getVditor: (): Vditor => vditorRef.value!,
};
onMounted(() => {
nextTick(() => {
init();
setTimeout(() => {
modalFn?.redoModalHeight?.();
}, 200);
});
emit('get', instance);
});
onUnmounted(() => {
function destroy() {
const vditorInstance = unref(vditorRef);
if (!vditorInstance) return;
try {
vditorInstance?.destroy?.();
} catch (error) {}
});
vditorRef.value = null;
initedRef.value = false;
}
onMountedOrActivated(init);
onBeforeUnmount(destroy);
onDeactivated(destroy);
return {
wrapRef,
...instance,

View File

@ -18,7 +18,7 @@
</template>
<template #footer v-if="!$slots.footer">
<ModalFooter v-bind="getProps" @ok="handleOk" @cancel="handleCancel">
<ModalFooter v-bind="getBindValue" @ok="handleOk" @cancel="handleCancel">
<template #[item]="data" v-for="item in Object.keys($slots)">
<slot :name="item" v-bind="data"></slot>
</template>
@ -82,7 +82,7 @@
setup(props, { emit, attrs }) {
const visibleRef = ref(false);
const propsRef = ref<Partial<ModalProps> | null>(null);
const modalWrapperRef = ref<ComponentRef>(null);
const modalWrapperRef = ref<any>(null);
// modal Bottom and top height
const extHeightRef = ref(0);
@ -133,7 +133,12 @@
});
const getBindValue = computed((): Recordable => {
const attr = { ...attrs, ...unref(getProps) };
const attr = {
...attrs,
...unref(getMergeProps),
visible: unref(visibleRef),
wrapClassName: unref(getWrapClassName),
};
if (unref(fullScreenRef)) {
return omit(attr, 'height');
}

View File

@ -9,7 +9,7 @@ import {
TimePicker,
} from 'ant-design-vue';
import type { ComponentType } from './types/componentType';
import { ApiSelect } from '/@/components/Form';
import { ApiSelect, ApiTreeSelect } from '/@/components/Form';
const componentMap = new Map<ComponentType, Component>();
@ -17,6 +17,7 @@ componentMap.set('Input', Input);
componentMap.set('InputNumber', InputNumber);
componentMap.set('Select', Select);
componentMap.set('ApiSelect', ApiSelect);
componentMap.set('ApiTreeSelect', ApiTreeSelect);
componentMap.set('Switch', Switch);
componentMap.set('Checkbox', Checkbox);
componentMap.set('DatePicker', DatePicker);

View File

@ -3,11 +3,15 @@ import { BasicArrow } from '/@/components/Basic';
export default () => {
return (props: Recordable) => {
if (!props.expandable) {
return <span />;
if (props.expanded) {
return <span class="ant-table-row-expand-icon ant-table-row-spaced" />;
} else {
return <span />;
}
}
return (
<BasicArrow
class="mr-1"
style="margin-right: 8px"
iconStyle="margin-top: -2px;"
onClick={(e: Event) => {
props.onExpand(props.record, e);

View File

@ -1,12 +1,16 @@
<template>
<div :class="[prefixCls, getAlign]" @click="onCellClick">
<template v-for="(action, index) in getActions" :key="`${index}-${action.label}`">
<Tooltip v-bind="getTooltip(action.tooltip)">
<Tooltip v-if="action.tooltip" v-bind="getTooltip(action.tooltip)">
<PopConfirmButton v-bind="action">
<Icon :icon="action.icon" class="mr-1" v-if="action.icon" />
{{ action.label }}
<Icon :icon="action.icon" :class="{ 'mr-1': !!action.label }" v-if="action.icon" />
<template v-if="action.label">{{ action.label }}</template>
</PopConfirmButton>
</Tooltip>
<PopConfirmButton v-else v-bind="action">
<Icon :icon="action.icon" :class="{ 'mr-1': !!action.label }" v-if="action.icon" />
<template v-if="action.label">{{ action.label }}</template>
</PopConfirmButton>
<Divider
type="vertical"
class="action-divider"
@ -126,15 +130,13 @@
return actionColumn?.align ?? 'left';
});
const getTooltip = computed(() => {
return (data: string | TooltipProps): TooltipProps => {
if (isString(data)) {
return { title: data, placement: 'bottom' };
} else {
return Object.assign({ placement: 'bottom' }, data);
}
};
});
function getTooltip(data: string | TooltipProps): TooltipProps {
if (isString(data)) {
return { title: data, placement: 'bottom' };
} else {
return Object.assign({ placement: 'bottom' }, data);
}
}
function onCellClick(e: MouseEvent) {
if (!props.stopButtonPropagation) return;
@ -180,6 +182,12 @@
}
}
button.ant-btn-circle {
span {
margin: auto !important;
}
}
.ant-divider,
.ant-divider-vertical {
margin: 0 2px;

View File

@ -1,16 +1,25 @@
<template>
<slot name="tableTitle" v-if="$slots.tableTitle"></slot>
<TableTitle :helpMessage="titleHelpMessage" :title="title" v-if="!$slots.tableTitle && title" />
<div :class="`${prefixCls}__toolbar`">
<slot name="toolbar"></slot>
<Divider type="vertical" v-if="$slots.toolbar && showTableSetting" />
<TableSetting
:setting="tableSetting"
v-if="showTableSetting"
@columns-change="handleColumnChange"
/>
<div style="width: 100%">
<div v-if="$slots.headerTop" style="margin: 5px">
<slot name="headerTop"></slot>
</div>
<div style="width: 100%; display: flex">
<slot name="tableTitle" v-if="$slots.tableTitle"></slot>
<TableTitle
:helpMessage="titleHelpMessage"
:title="title"
v-if="!$slots.tableTitle && title"
/>
<div :class="`${prefixCls}__toolbar`">
<slot name="toolbar"></slot>
<Divider type="vertical" v-if="$slots.toolbar && showTableSetting" />
<TableSetting
:setting="tableSetting"
v-if="showTableSetting"
@columns-change="handleColumnChange"
/>
</div>
</div>
</div>
</template>
<script lang="ts">

View File

@ -19,7 +19,7 @@ export const CellComponent: FunctionalComponent = (
const Comp = componentMap.get(component) as typeof defineComponent;
const DefaultComp = h(Comp, attrs);
if (!rule) {
if (!rule || !popoverVisible) {
return DefaultComp;
}
return h(

View File

@ -45,6 +45,7 @@
import { isString, isBoolean, isFunction, isNumber, isArray } from '/@/utils/is';
import { createPlaceholderMessage } from './helper';
import { set, omit } from 'lodash-es';
import { treeToList } from '/@/utils/helper/treeHelper';
export default defineComponent({
name: 'EditableCell',
@ -154,6 +155,7 @@
watchEffect(() => {
defaultValueRef.value = props.value;
currentValueRef.value = props.value;
});
watchEffect(() => {
@ -275,9 +277,23 @@
}
}
// only ApiSelect
// only ApiSelect or TreeSelect
function handleOptionsChange(options: LabelValueOptions) {
optionsRef.value = options;
const { replaceFields } = props.column?.editComponentProps ?? {};
const component = unref(getComponent);
if (component === 'ApiTreeSelect') {
const { title = 'title', value = 'value', children = 'children' } = replaceFields || {};
let listOptions: Recordable[] = treeToList(options, { children });
listOptions = listOptions.map((item) => {
return {
label: item[title],
value: item[value],
};
});
optionsRef.value = listOptions as LabelValueOptions;
} else {
optionsRef.value = options;
}
}
function initCbs(cbs: 'submitCbs' | 'validCbs' | 'cancelCbs', handle: Fn) {

View File

@ -41,6 +41,11 @@ export function useTableHeader(
tableTitle: () => getSlot(slots, 'tableTitle'),
}
: {}),
...(slots.headerTop
? {
headerTop: () => getSlot(slots, 'headerTop'),
}
: {}),
}
),
};

View File

@ -68,6 +68,23 @@ export function useTableScroll(
bodyEl = tableEl.querySelector('.ant-table-body');
}
const hasScrollBarY = bodyEl.scrollHeight > bodyEl.clientHeight;
const hasScrollBarX = bodyEl.scrollWidth > bodyEl.clientWidth;
if (hasScrollBarY) {
tableEl.classList.contains('hide-scrollbar-y') &&
tableEl.classList.remove('hide-scrollbar-y');
} else {
!tableEl.classList.contains('hide-scrollbar-y') && tableEl.classList.add('hide-scrollbar-y');
}
if (hasScrollBarX) {
tableEl.classList.contains('hide-scrollbar-x') &&
tableEl.classList.remove('hide-scrollbar-x');
} else {
!tableEl.classList.contains('hide-scrollbar-x') && tableEl.classList.add('hide-scrollbar-x');
}
bodyEl!.style.height = 'unset';
if (!unref(getCanResize) || tableData.length === 0) return;

View File

@ -3,6 +3,7 @@ export type ComponentType =
| 'InputNumber'
| 'Select'
| 'ApiSelect'
| 'ApiTreeSelect'
| 'Checkbox'
| 'Switch'
| 'DatePicker'

View File

@ -18,8 +18,8 @@
import TreeHeader from './TreeHeader.vue';
import { ScrollContainer } from '/@/components/Container';
import { omit, get } from 'lodash-es';
import { isBoolean, isFunction } from '/@/utils/is';
import { omit, get, difference } from 'lodash-es';
import { isArray, isBoolean, isFunction } from '/@/utils/is';
import { extendSlots, getSlot } from '/@/utils/helper/tsxHelper';
import { filter } from '/@/utils/helper/treeHelper';
@ -90,8 +90,19 @@
emit('update:selectedKeys', v);
},
onCheck: (v: CheckKeys, e: CheckEvent) => {
state.checkedKeys = v;
const rawVal = toRaw(v);
let currentValue = toRaw(state.checkedKeys) as Keys;
if (isArray(currentValue) && searchState.startSearch) {
const { key } = unref(getReplaceFields);
currentValue = difference(currentValue, getChildrenKeys(e.node.$attrs.node[key]));
if (e.checked) {
currentValue.push(e.node.$attrs.node[key]);
}
state.checkedKeys = currentValue;
} else {
state.checkedKeys = v;
}
const rawVal = toRaw(state.checkedKeys);
emit('update:value', rawVal);
emit('check', rawVal, e);
},
@ -115,6 +126,7 @@
filterByLevel,
updateNodeByKey,
getAllKeys,
getChildrenKeys,
} = useTree(treeDataRef, getReplaceFields);
function getIcon(params: Recordable, icon?: string) {

View File

@ -27,6 +27,28 @@ export function useTree(
return keys as Keys;
}
function getChildrenKeys(nodeKey: string | number, list?: TreeDataItem[]): Keys {
const keys: Keys = [];
const treeData = list || unref(treeDataRef);
const { key: keyField, children: childrenField } = unref(getReplaceFields);
if (!childrenField || !keyField) return keys;
for (let index = 0; index < treeData.length; index++) {
const node = treeData[index];
const children = node[childrenField];
if (nodeKey === node[keyField]) {
keys.push(node[keyField]!);
if (children && children.length) {
keys.push(...(getAllKeys(children) as string[]));
}
} else {
if (children && children.length) {
keys.push(...getChildrenKeys(nodeKey, children));
}
}
}
return keys as Keys;
}
// Update node
function updateNodeByKey(key: string, node: TreeDataItem, list?: TreeDataItem[]) {
if (!key) return;
@ -146,5 +168,6 @@ export function useTree(
filterByLevel,
updateNodeByKey,
getAllKeys,
getChildrenKeys,
};
}

View File

@ -46,6 +46,7 @@
import { uploadContainerProps } from './props';
import { omit } from 'lodash-es';
import { useI18n } from '/@/hooks/web/useI18n';
import { isArray } from '/@/utils/is';
export default defineComponent({
name: 'BasicUpload',
@ -77,7 +78,7 @@
watch(
() => props.value,
(value = []) => {
fileList.value = value;
fileList.value = isArray(value) ? value : [];
},
{ immediate: true }
);

View File

@ -20,6 +20,7 @@
import { downloadByUrl } from '/@/utils/file/download';
import { createPreviewColumns, createPreviewActionColumn } from './data';
import { useI18n } from '/@/hooks/web/useI18n';
import { isArray } from '/@/utils/is';
export default defineComponent({
components: { BasicModal, FileList },
@ -33,6 +34,7 @@
watch(
() => props.value,
(value) => {
if (!isArray(value)) value = [];
fileListRef.value = value
.filter((item) => !!item)
.map((item) => {

View File

@ -1,7 +1,7 @@
@import './pagination.less';
@import './input.less';
@import './btn.less';
@import './table.less';
// @import './table.less';
// TODO beta.11 fix
.ant-col {

View File

@ -2,7 +2,7 @@
// fix table unnecessary scrollbar
.@{prefix-cls} {
.ant-table-wrapper {
.hide-scrollbar-y {
.ant-spin-nested-loading {
.ant-spin-container {
.ant-table {
@ -13,7 +13,7 @@
}
.ant-table-body {
overflow: auto !important;
overflow-y: auto !important;
}
}
@ -24,6 +24,50 @@
}
}
}
.ant-table-fixed-left {
.ant-table-body-outer {
.ant-table-body-inner {
overflow-y: auto !important;
}
}
}
}
}
}
}
}
.hide-scrollbar-x {
.ant-spin-nested-loading {
.ant-spin-container {
.ant-table {
.ant-table-content {
.ant-table-scroll {
.ant-table-hide-scrollbar {
//overflow-x: auto !important;
}
.ant-table-body {
overflow: auto !important;
}
}
.ant-table-fixed-right {
.ant-table-body-outer {
.ant-table-body-inner {
overflow-x: auto !important;
}
}
}
.ant-table-fixed-left {
.ant-table-body-outer {
.ant-table-body-inner {
overflow-x: auto !important;
}
}
}
}
}
}

View File

@ -1,26 +1,36 @@
import { getCurrentInstance, onBeforeUnmount, ref, Ref, unref } from 'vue';
import { getCurrentInstance, onBeforeUnmount, ref, Ref, shallowRef, unref } from 'vue';
import { useRafThrottle } from '/@/utils/domUtils';
import { addResizeListener, removeResizeListener } from '/@/utils/event';
import { isDef } from '/@/utils/is';
const domSymbol = Symbol('watermark-dom');
export function useWatermark(
appendEl: Ref<HTMLElement | null> = ref(document.body) as Ref<HTMLElement>
) {
let func: Fn = () => {};
const func = useRafThrottle(function () {
const el = unref(appendEl);
if (!el) return;
const { clientHeight: height, clientWidth: width } = el;
updateWatermark({ height, width });
});
const id = domSymbol.toString();
const clear = () => {
const domId = document.getElementById(id);
if (domId) {
const el = unref(appendEl);
el && el.removeChild(domId);
}
window.removeEventListener('resize', func);
};
const createWatermark = (str: string) => {
clear();
const watermarkEl = shallowRef<HTMLElement>();
const clear = () => {
const domId = unref(watermarkEl);
watermarkEl.value = undefined;
const el = unref(appendEl);
if (!el) return;
domId && el.removeChild(domId);
removeResizeListener(el, func);
};
function createBase64(str: string) {
const can = document.createElement('canvas');
can.width = 300;
can.height = 240;
const width = 300;
const height = 240;
Object.assign(can, { width, height });
const cans = can.getContext('2d');
if (cans) {
@ -29,30 +39,55 @@ export function useWatermark(
cans.fillStyle = 'rgba(0, 0, 0, 0.15)';
cans.textAlign = 'left';
cans.textBaseline = 'middle';
cans.fillText(str, can.width / 20, can.height);
cans.fillText(str, width / 20, height);
}
return can.toDataURL('image/png');
}
function updateWatermark(
options: {
width?: number;
height?: number;
str?: string;
} = {}
) {
const el = unref(watermarkEl);
if (!el) return;
if (isDef(options.width)) {
el.style.width = `${options.width}px`;
}
if (isDef(options.height)) {
el.style.height = `${options.height}px`;
}
if (isDef(options.str)) {
el.style.background = `url(${createBase64(options.str)}) left top repeat`;
}
}
const createWatermark = (str: string) => {
if (unref(watermarkEl)) {
updateWatermark({ str });
return id;
}
const div = document.createElement('div');
watermarkEl.value = div;
div.id = id;
div.style.pointerEvents = 'none';
div.style.top = '0px';
div.style.left = '0px';
div.style.position = 'absolute';
div.style.zIndex = '100000';
div.style.width = document.documentElement.clientWidth + 'px';
div.style.height = document.documentElement.clientHeight + 'px';
div.style.background = 'url(' + can.toDataURL('image/png') + ') left top repeat';
const el = unref(appendEl);
el && el.appendChild(div);
if (!el) return id;
const { clientHeight: height, clientWidth: width } = el;
updateWatermark({ str, width, height });
el.appendChild(div);
return id;
};
function setWatermark(str: string) {
createWatermark(str);
func = () => {
createWatermark(str);
};
window.addEventListener('resize', func);
addResizeListener(document.documentElement, func);
const instance = getCurrentInstance();
if (instance) {
onBeforeUnmount(() => {

View File

@ -1,9 +1,9 @@
<template>
<div :class="[prefixCls, `${prefixCls}--${theme}`]">
<a-breadcrumb :routes="routes">
<template #itemRender="{ route, routes, paths }">
<template #itemRender="{ route, routes: routesMatched, paths }">
<Icon :icon="getIcon(route)" v-if="getShowBreadCrumbIcon && getIcon(route)" />
<span v-if="!hasRedirect(routes, route)">
<span v-if="!hasRedirect(routesMatched, route)">
{{ t(route.name || route.meta.title) }}
</span>
<router-link v-else to="" @click="handleClick(route, paths, $event)">
@ -15,6 +15,7 @@
</template>
<script lang="ts">
import type { RouteLocationMatched } from 'vue-router';
import { useRouter } from 'vue-router';
import type { Menu } from '/@/router/types';
import { defineComponent, ref, watchEffect } from 'vue';
@ -26,7 +27,6 @@
import { useRootSetting } from '/@/hooks/setting/useRootSetting';
import { useGo } from '/@/hooks/web/usePage';
import { useI18n } from '/@/hooks/web/useI18n';
import { useRouter } from 'vue-router';
import { propTypes } from '/@/utils/propTypes';
import { isString } from '/@/utils/is';
@ -96,7 +96,7 @@
}
function filterItem(list: RouteLocationMatched[]) {
let resultList = filter(list, (item) => {
return filter(list, (item) => {
const { meta, name } = item;
if (!meta) {
return !!name;
@ -107,8 +107,6 @@
}
return true;
}).filter((item) => !item.meta?.hideBreadcrumb || !item.meta?.hideMenu);
return resultList;
}
function handleClick(route: RouteLocationMatched, paths: string[], e: Event) {
@ -140,10 +138,7 @@
}
function hasRedirect(routes: RouteLocationMatched[], route: RouteLocationMatched) {
if (routes.indexOf(route) === routes.length - 1) {
return false;
}
return true;
return routes.indexOf(route) !== routes.length - 1;
}
function getIcon(route) {

View File

@ -1,11 +1,20 @@
<template>
<a-list :class="prefixCls">
<template v-for="item in list" :key="item.id">
<a-list :class="prefixCls" bordered :pagination="getPagination">
<template v-for="item in getData" :key="item.id">
<a-list-item class="list-item">
<a-list-item-meta>
<template #title>
<div class="title">
{{ item.title }}
<a-typography-paragraph
@click="handleTitleClick(item)"
style="width: 100%; margin-bottom: 0 !important"
:style="{ cursor: isTitleClickable ? 'pointer' : '' }"
:delete="!!item.titleDelete"
:ellipsis="
$props.titleRows > 0 ? { rows: $props.titleRows, tooltip: item.title } : false
"
:content="item.title"
/>
<div class="extra" v-if="item.extra">
<a-tag class="tag" :color="item.color">
{{ item.extra }}
@ -21,8 +30,16 @@
<template #description>
<div>
<div class="description">
{{ item.description }}
<div class="description" v-if="item.description">
<a-typography-paragraph
style="width: 100%; margin-bottom: 0 !important"
:ellipsis="
$props.descRows > 0
? { rows: $props.descRows, tooltip: item.description }
: false
"
:content="item.description"
/>
</div>
<div class="datetime">
{{ item.datetime }}
@ -35,16 +52,18 @@
</a-list>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import { computed, defineComponent, PropType, ref, watch, unref } from 'vue';
import { ListItem } from './data';
import { useDesign } from '/@/hooks/web/useDesign';
import { List, Avatar, Tag } from 'ant-design-vue';
import { List, Avatar, Tag, Typography } from 'ant-design-vue';
import { isNumber } from '/@/utils/is';
export default defineComponent({
components: {
[Avatar.name]: Avatar,
[List.name]: List,
[List.Item.name]: List.Item,
AListItemMeta: List.Item.Meta,
ATypographyParagraph: Typography.Paragraph,
[Tag.name]: Tag,
},
props: {
@ -52,10 +71,67 @@
type: Array as PropType<ListItem[]>,
default: () => [],
},
pageSize: {
type: [Boolean, Number] as PropType<Boolean | Number>,
default: 5,
},
currentPage: {
type: Number,
default: 1,
},
titleRows: {
type: Number,
default: 1,
},
descRows: {
type: Number,
default: 2,
},
onTitleClick: {
type: Function as PropType<(Recordable) => void>,
},
},
setup() {
emits: ['update:currentPage'],
setup(props, { emit }) {
const { prefixCls } = useDesign('header-notify-list');
return { prefixCls };
const current = ref(props.currentPage || 1);
const getData = computed(() => {
const { pageSize, list } = props;
console.log('refreshData', list);
if (pageSize === false) return [];
let size = isNumber(pageSize) ? pageSize : 5;
return list.slice(size * (unref(current) - 1), size * unref(current));
});
watch(
() => props.currentPage,
(v) => {
current.value = v;
}
);
const isTitleClickable = computed(() => !!props.onTitleClick);
const getPagination = computed(() => {
const { list, pageSize } = props;
if (pageSize > 0 && list && list.length > pageSize) {
return {
total: list.length,
pageSize,
//size: 'small',
current: unref(current),
onChange(page) {
current.value = page;
emit('update:currentPage', page);
},
};
} else {
return false;
}
});
function handleTitleClick(item: ListItem) {
props.onTitleClick && props.onTitleClick(item);
}
return { prefixCls, getPagination, getData, handleTitleClick, isTitleClickable };
},
});
</script>
@ -67,6 +143,10 @@
display: none;
}
::v-deep(.ant-pagination-disabled) {
display: inline-block !important;
}
&-item {
padding: 6px;
overflow: hidden;

View File

@ -1,7 +1,10 @@
export interface ListItem {
id: string;
avatar: string;
// 通知的标题内容
title: string;
// 是否在标题上显示删除线
titleDelete?: boolean;
datetime: string;
type: string;
read?: boolean;
@ -56,6 +59,55 @@ export const tabListData: TabItem[] = [
datetime: '2017-08-07',
type: '1',
},
{
id: '000000005',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
title:
'标题可以设置自动显示省略号本例中标题行数已设为1行如果内容超过1行将自动截断并支持tooltip显示完整标题。',
description: '',
datetime: '2017-08-07',
type: '1',
},
{
id: '000000006',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
title: '左侧图标用于区分不同的类型',
description: '',
datetime: '2017-08-07',
type: '1',
},
{
id: '000000007',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
title: '左侧图标用于区分不同的类型',
description: '',
datetime: '2017-08-07',
type: '1',
},
{
id: '000000008',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
title: '左侧图标用于区分不同的类型',
description: '',
datetime: '2017-08-07',
type: '1',
},
{
id: '000000009',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
title: '左侧图标用于区分不同的类型',
description: '',
datetime: '2017-08-07',
type: '1',
},
{
id: '000000010',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
title: '左侧图标用于区分不同的类型',
description: '',
datetime: '2017-08-07',
type: '1',
},
],
},
{
@ -84,7 +136,8 @@ export const tabListData: TabItem[] = [
id: '000000008',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
title: '标题',
description: '这种模板用于提醒谁与你发生了互动',
description:
'请将鼠标移动到此处以便测试超长的消息在此处将如何处理。本例中设置的描述最大行数为2超过2行的描述内容将被省略并且可以通过tooltip查看完整内容',
datetime: '2017-08-07',
type: '2',
clickClose: true,

View File

@ -6,13 +6,15 @@
</Badge>
<template #content>
<Tabs>
<template v-for="item in tabListData" :key="item.key">
<template v-for="item in listData" :key="item.key">
<TabPane>
<template #tab>
{{ item.name }}
<span v-if="item.list.length !== 0">({{ item.list.length }})</span>
</template>
<NoticeList :list="item.list" />
<!-- 绑定title-click事件的通知列表中标题是可点击-->
<NoticeList :list="item.list" v-if="item.key === '1'" @title-click="onNoticeClick" />
<NoticeList :list="item.list" v-else />
</TabPane>
</template>
</Tabs>
@ -21,28 +23,40 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { computed, defineComponent, ref } from 'vue';
import { Popover, Tabs, Badge } from 'ant-design-vue';
import { BellOutlined } from '@ant-design/icons-vue';
import { tabListData } from './data';
import { tabListData, ListItem } from './data';
import NoticeList from './NoticeList.vue';
import { useDesign } from '/@/hooks/web/useDesign';
import { useMessage } from '/@/hooks/web/useMessage';
export default defineComponent({
components: { Popover, BellOutlined, Tabs, TabPane: Tabs.TabPane, Badge, NoticeList },
setup() {
const { prefixCls } = useDesign('header-notify');
const { createMessage } = useMessage();
const listData = ref(tabListData);
let count = 0;
const count = computed(() => {
let count = 0;
for (let i = 0; i < tabListData.length; i++) {
count += tabListData[i].list.length;
}
return count;
});
for (let i = 0; i < tabListData.length; i++) {
count += tabListData[i].list.length;
function onNoticeClick(record: ListItem) {
createMessage.success('你点击了通知ID=' + record.id);
// 线,线
record.titleDelete = !record.titleDelete;
}
return {
prefixCls,
tabListData,
listData,
count,
onNoticeClick,
numberStyle: {},
};
},

View File

@ -15,7 +15,6 @@
:collapsedWidth="getCollapsedWidth"
:theme="getMenuTheme"
@breakpoint="onBreakpointChange"
@collapse="toggleCollapsed"
:trigger="getTrigger"
v-bind="getTriggerAttr"
>

View File

@ -15,7 +15,7 @@
>
<AppLogo :showTitle="false" :class="`${prefixCls}-logo`" />
<Trigger :class="`${prefixCls}-trigger`" />
<LayoutTrigger :class="`${prefixCls}-trigger`" />
<ScrollContainer>
<ul :class="`${prefixCls}-module`">
@ -63,7 +63,7 @@
</div>
<ScrollContainer :class="`${prefixCls}-menu-list__content`">
<SimpleMenu
:items="chilrenMenus"
:items="childrenMenus"
:theme="getMenuTheme"
mixSider
@menuClick="handleMenuClick"
@ -80,24 +80,23 @@
<script lang="ts">
import type { Menu } from '/@/router/types';
import type { CSSProperties } from 'vue';
import { computed, defineComponent, onMounted, ref, unref } from 'vue';
import type { RouteLocationNormalized } from 'vue-router';
import { defineComponent, onMounted, ref, computed, unref } from 'vue';
import { ScrollContainer } from '/@/components/Container';
import { SimpleMenuTag } from '/@/components/SimpleMenu';
import { SimpleMenu, SimpleMenuTag } from '/@/components/SimpleMenu';
import { Icon } from '/@/components/Icon';
import { AppLogo } from '/@/components/Application';
import Trigger from '../trigger/HeaderTrigger.vue';
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
import { useDragLine } from './useLayoutSider';
import { useGlobSetting } from '/@/hooks/setting';
import { useDesign } from '/@/hooks/web/useDesign';
import { useI18n } from '/@/hooks/web/useI18n';
import { useGo } from '/@/hooks/web/usePage';
import { SIDE_BAR_SHOW_TIT_MINI_WIDTH, SIDE_BAR_MINI_WIDTH } from '/@/enums/appEnum';
import { SIDE_BAR_MINI_WIDTH, SIDE_BAR_SHOW_TIT_MINI_WIDTH } from '/@/enums/appEnum';
import clickOutside from '/@/directives/clickOutside';
import { getShallowMenus, getChildrenMenus, getCurrentParentPath } from '/@/router/menus';
import { getChildrenMenus, getCurrentParentPath, getShallowMenus } from '/@/router/menus';
import { listenerRouteChange } from '/@/logics/mitt/routeChange';
import { SimpleMenu } from '/@/components/SimpleMenu';
import LayoutTrigger from '../trigger/index.vue';
export default defineComponent({
name: 'LayoutMixSider',
@ -106,7 +105,7 @@
AppLogo,
SimpleMenu,
Icon,
Trigger,
LayoutTrigger,
SimpleMenuTag,
},
directives: {
@ -115,7 +114,7 @@
setup() {
let menuModules = ref<Menu[]>([]);
const activePath = ref('');
const chilrenMenus = ref<Menu[]>([]);
const childrenMenus = ref<Menu[]>([]);
const openMenu = ref(false);
const dragBarRef = ref<ElRef>(null);
const sideRef = ref<ElRef>(null);
@ -151,7 +150,7 @@
const getIsFixed = computed(() => {
/* eslint-disable-next-line */
mixSideHasChildren.value = unref(chilrenMenus).length > 0;
mixSideHasChildren.value = unref(childrenMenus).length > 0;
const isFixed = unref(getMixSideFixed) && unref(mixSideHasChildren);
if (isFixed) {
/* eslint-disable-next-line */
@ -179,6 +178,7 @@
return !unref(getMixSideFixed)
? {
onMouseleave: () => {
setActive(true);
closeMenu();
},
}
@ -209,9 +209,8 @@
}
// Process module menu click
async function hanldeModuleClick(path: string, hover = false) {
async function handleModuleClick(path: string, hover = false) {
const children = await getChildrenMenus(path);
if (unref(activePath) === path) {
if (!hover) {
if (!unref(openMenu)) {
@ -219,6 +218,10 @@
} else {
closeMenu();
}
} else {
if (!unref(openMenu)) {
openMenu.value = true;
}
}
if (!unref(openMenu)) {
setActive();
@ -229,20 +232,19 @@
}
if (!children || children.length === 0) {
go(path);
chilrenMenus.value = [];
if (!hover) go(path);
childrenMenus.value = [];
closeMenu();
return;
}
chilrenMenus.value = children;
childrenMenus.value = children;
}
// Set the currently active menu and submenu
async function setActive(setChildren = false) {
const path = currentRoute.value?.path;
if (!path) return;
const parentPath = await getCurrentParentPath(path);
activePath.value = parentPath;
activePath.value = await getCurrentParentPath(path);
// hanldeModuleClick(parentPath);
if (unref(getIsMixSidebar)) {
const activeMenu = unref(menuModules).find((item) => item.path === unref(activePath));
@ -250,14 +252,14 @@
if (p) {
const children = await getChildrenMenus(p);
if (setChildren) {
chilrenMenus.value = children;
childrenMenus.value = children;
if (unref(getMixSideFixed)) {
openMenu.value = children.length > 0;
}
}
if (children.length === 0) {
chilrenMenus.value = [];
childrenMenus.value = [];
}
}
}
@ -275,11 +277,15 @@
function getItemEvents(item: Menu) {
if (unref(getMixSideTrigger) === 'hover') {
return {
onMouseenter: () => hanldeModuleClick(item.path, true),
onMouseenter: () => handleModuleClick(item.path, true),
onClick: async () => {
const children = await getChildrenMenus(item.path);
if (item.path && (!children || children.length === 0)) go(item.path);
},
};
}
return {
onClick: () => hanldeModuleClick(item.path),
onClick: () => handleModuleClick(item.path),
};
}
@ -300,9 +306,9 @@
t,
prefixCls,
menuModules,
hanldeModuleClick,
handleModuleClick: handleModuleClick,
activePath,
chilrenMenus,
childrenMenus: childrenMenus,
getShowDragBar,
handleMenuClick,
getMenuStyle,
@ -480,23 +486,24 @@
bottom: 0;
left: 0;
width: 100%;
padding: 6px;
padding-left: 12px;
font-size: 18px;
font-size: 14px;
color: rgba(255, 255, 255, 0.65);
text-align: center;
cursor: pointer;
background-color: @sider-dark-bg-color;
background-color: @trigger-dark-bg-color;
height: 36px;
line-height: 36px;
}
&.light &-trigger {
color: rgba(0, 0, 0, 0.65);
background-color: #fff;
border-top: 1px solid #eee;
}
&-menu-list {
position: fixed;
top: 0;
width: 0;
width: 200px;
height: calc(100%);
background-color: #fff;

View File

@ -2,6 +2,8 @@ export const REDIRECT_NAME = 'Redirect';
export const PARENT_LAYOUT_NAME = 'ParentLayout';
export const PAGE_NOT_FOUND_NAME = 'PageNotFound';
export const EXCEPTION_COMPONENT = () => import('../views/sys/exception/Exception.vue');
/**

View File

@ -7,17 +7,25 @@ import { useUserStoreWithOut } from '/@/store/modules/user';
import { PAGE_NOT_FOUND_ROUTE } from '/@/router/routes/basic';
import { RootRoute } from '/@/router/routes';
const LOGIN_PATH = PageEnum.BASE_LOGIN;
const ROOT_PATH = RootRoute.path;
const whitePathList: PageEnum[] = [LOGIN_PATH];
export function createPermissionGuard(router: Router) {
const userStore = useUserStoreWithOut();
const permissionStore = usePermissionStoreWithOut();
router.beforeEach(async (to, from, next) => {
// Jump to the 404 page after processing the login
if (from.path === LOGIN_PATH && to.name === PAGE_NOT_FOUND_ROUTE.name) {
next(PageEnum.BASE_HOME);
if (
from.path === ROOT_PATH &&
to.path === PageEnum.BASE_HOME &&
userStore.getUserInfo.homePath &&
userStore.getUserInfo.homePath !== PageEnum.BASE_HOME
) {
next(userStore.getUserInfo.homePath);
return;
}
@ -52,6 +60,17 @@ export function createPermissionGuard(router: Router) {
return;
}
// Jump to the 404 page after processing the login
if (
from.path === LOGIN_PATH &&
to.name === PAGE_NOT_FOUND_ROUTE.name &&
to.fullPath !== (userStore.getUserInfo.homePath || PageEnum.BASE_HOME)
) {
next(userStore.getUserInfo.homePath || PageEnum.BASE_HOME);
console.log({ from, to });
return;
}
if (permissionStore.getIsDynamicAddedRoute) {
next();
return;
@ -63,10 +82,18 @@ export function createPermissionGuard(router: Router) {
router.addRoute(route as unknown as RouteRecordRaw);
});
const redirectPath = (from.query.redirect || to.path) as string;
const redirect = decodeURIComponent(redirectPath);
const nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect };
router.addRoute(PAGE_NOT_FOUND_ROUTE as unknown as RouteRecordRaw);
permissionStore.setDynamicAddedRoute(true);
next(nextData);
if (to.name === PAGE_NOT_FOUND_ROUTE.name) {
// 动态添加路由后此处应当重定向到fullPath否则会加载404页面内容
next({ path: to.fullPath, replace: true });
} else {
const redirectPath = (from.query.redirect || to.path) as string;
const redirect = decodeURIComponent(redirectPath);
const nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect };
next(nextData);
}
});
}

View File

@ -62,6 +62,7 @@ export function transformRouteToMenu(routeModList: AppRouteModule[], routerMappi
name: title,
hideMenu,
path: node.path,
...(node.redirect ? { redirect: node.redirect } : {}),
};
},
});
@ -72,14 +73,16 @@ export function transformRouteToMenu(routeModList: AppRouteModule[], routerMappi
/**
* config menu with given params
*/
const menuParamRegex = /(?<=:)([\s\S]+?)((?=\/)|$)/g;
const menuParamRegex = /(?::)([\s\S]+?)((?=\/)|$)/g;
export function configureDynamicParamsMenu(menu: Menu, params: RouteParams) {
const { path, paramPath } = toRaw(menu);
let realPath = paramPath ? paramPath : path;
const matchArr = realPath.match(menuParamRegex);
matchArr?.forEach((it) => {
if (params[it]) {
realPath = realPath.replace(`:${it}`, params[it] as string);
const realIt = it.substr(1);
if (params[realIt]) {
realPath = realPath.replace(`:${realIt}`, params[realIt] as string);
}
});
// save original param path.

View File

@ -54,7 +54,7 @@ const staticMenus: Menu[] = [];
async function getAsyncMenus() {
const permissionStore = usePermissionStore();
if (isBackMode()) {
return permissionStore.getBackMenuList;
return permissionStore.getBackMenuList.filter((item) => !item.meta?.hideMenu && !item.hideMenu);
}
if (isRouteMappingMode()) {
return permissionStore.getFrontMenuList.filter((item) => !item.hideMenu);

View File

@ -1,11 +1,16 @@
import type { AppRouteRecordRaw } from '/@/router/types';
import { t } from '/@/hooks/web/useI18n';
import { REDIRECT_NAME, LAYOUT, EXCEPTION_COMPONENT } from '/@/router/constant';
import {
REDIRECT_NAME,
LAYOUT,
EXCEPTION_COMPONENT,
PAGE_NOT_FOUND_NAME,
} from '/@/router/constant';
// 404 on a page
export const PAGE_NOT_FOUND_ROUTE: AppRouteRecordRaw = {
path: '/:path(.*)*',
name: 'ErrorPage',
name: PAGE_NOT_FOUND_NAME,
component: LAYOUT,
meta: {
title: 'ErrorPage',
@ -15,11 +20,12 @@ export const PAGE_NOT_FOUND_ROUTE: AppRouteRecordRaw = {
children: [
{
path: '/:path(.*)*',
name: 'ErrorPage',
name: PAGE_NOT_FOUND_NAME,
component: EXCEPTION_COMPONENT,
meta: {
title: 'ErrorPage',
hideBreadcrumb: true,
hideMenu: true,
},
},
],

View File

@ -37,4 +37,10 @@ export const LoginRoute: AppRouteRecordRaw = {
};
// Basic routing without permission
export const basicRoutes = [LoginRoute, RootRoute, ...mainOutRoutes, REDIRECT_ROUTE];
export const basicRoutes = [
LoginRoute,
RootRoute,
...mainOutRoutes,
REDIRECT_ROUTE,
PAGE_NOT_FOUND_ROUTE,
];

View File

@ -22,6 +22,7 @@ const dashboard: AppRouteModule = {
meta: {
title: t('routes.dashboard.about'),
icon: 'simple-icons:about-dot-me',
hideMenu: true,
},
},
],

View File

@ -19,7 +19,7 @@ const dashboard: AppRouteModule = {
name: 'Analysis',
component: () => import('/@/views/dashboard/analysis/index.vue'),
meta: {
affix: true,
// affix: true,
title: t('routes.dashboard.analysis'),
},
},

View File

@ -13,6 +13,7 @@ import { getRawRoute } from '/@/utils';
import { MULTIPLE_TABS_KEY } from '/@/enums/cacheEnum';
import projectSetting from '/@/settings/projectSetting';
import { useUserStore } from '/@/store/modules/user';
export interface MultipleTabState {
cacheTabList: Set<string>;
@ -113,6 +114,7 @@ export const useMultipleTabStore = defineStore({
// 404 The page does not need to add a tab
if (
path === PageEnum.ERROR_PAGE ||
path === PageEnum.BASE_LOGIN ||
!name ||
[REDIRECT_ROUTE.name, PAGE_NOT_FOUND_ROUTE.name].includes(name as string)
) {
@ -181,7 +183,8 @@ export const useMultipleTabStore = defineStore({
if (index === 0) {
// There is only one tab, then jump to the homepage, otherwise jump to the right tab
if (this.tabList.length === 1) {
toTarget = PageEnum.BASE_HOME;
const userStore = useUserStore();
toTarget = userStore.getUserInfo.homePath || PageEnum.BASE_HOME;
} else {
// Jump to the right tab
const page = this.tabList[index + 1];

View File

@ -22,6 +22,7 @@ import { getMenuList } from '/@/api/sys/menu';
import { getPermCode } from '/@/api/sys/user';
import { useMessage } from '/@/hooks/web/useMessage';
import { PageEnum } from '/@/enums/pageEnum';
interface PermissionState {
// Permission code list
@ -117,6 +118,36 @@ export const usePermissionStore = defineStore({
return !ignoreRoute;
};
/**
* @description pathroutes中的affix标记
* */
const patchHomeAffix = (routes: AppRouteRecordRaw[]) => {
if (!routes || routes.length === 0) return;
let homePath: string = userStore.getUserInfo.homePath || PageEnum.BASE_HOME;
function patcher(routes: AppRouteRecordRaw[], parentPath = '') {
if (parentPath) parentPath = parentPath + '/';
routes.forEach((route: AppRouteRecordRaw) => {
const { path, children, redirect } = route;
const currentPath = path.startsWith('/') ? path : parentPath + path;
if (currentPath === homePath) {
if (redirect) {
homePath = route.redirect! as string;
} else {
route.meta = Object.assign({}, route.meta, { affix: true });
throw new Error('end');
}
}
children && children.length > 0 && patcher(children, currentPath);
});
}
try {
patcher(routes);
} catch (e) {
// 已处理完毕跳出循环
}
return;
};
switch (permissionMode) {
case PermissionModeEnum.ROLE:
routes = filter(asyncRoutes, routeFilter);
@ -176,6 +207,7 @@ export const usePermissionStore = defineStore({
}
routes.push(ERROR_LOG_ROUTE);
patchHomeAffix(routes);
return routes;
},
},

View File

@ -11,6 +11,9 @@ import { doLogout, getUserInfo, loginApi } from '/@/api/sys/user';
import { useI18n } from '/@/hooks/web/useI18n';
import { useMessage } from '/@/hooks/web/useMessage';
import { router } from '/@/router';
import { usePermissionStore } from '/@/store/modules/permission';
import { RouteRecordRaw } from 'vue-router';
import { PAGE_NOT_FOUND_ROUTE } from '/@/router/routes/basic';
interface UserState {
userInfo: Nullable<UserInfo>;
@ -87,14 +90,26 @@ export const useUserStore = defineStore({
const userInfo = await this.getUserInfoAction();
const sessionTimeout = this.sessionTimeout;
sessionTimeout && this.setSessionTimeout(false);
!sessionTimeout && goHome && (await router.replace(PageEnum.BASE_HOME));
if (sessionTimeout) {
this.setSessionTimeout(false);
} else if (goHome) {
const permissionStore = usePermissionStore();
if (!permissionStore.isDynamicAddedRoute) {
const routes = await permissionStore.buildRoutesAction();
routes.forEach((route) => {
router.addRoute(route as unknown as RouteRecordRaw);
});
router.addRoute(PAGE_NOT_FOUND_ROUTE as unknown as RouteRecordRaw);
permissionStore.setDynamicAddedRoute(true);
}
await router.replace(userInfo.homePath || PageEnum.BASE_HOME);
}
return userInfo;
} catch (error) {
return Promise.reject(error);
}
},
async getUserInfoAction() {
async getUserInfoAction(): Promise<UserInfo> {
const userInfo = await getUserInfo();
const { roles } = userInfo;
const roleList = roles.map((item) => item.value) as RoleEnum[];

View File

@ -1,3 +1,4 @@
import type { FunctionArgs } from '@vueuse/core';
import { upperFirst } from 'lodash-es';
export interface ViewportOffsetResult {
@ -163,3 +164,17 @@ export function once(el: HTMLElement, event: string, fn: EventListener): void {
};
on(el, event, listener);
}
export function useRafThrottle<T extends FunctionArgs>(fn: T): T {
let locked = false;
// @ts-ignore
return function (...args: any[]) {
if (locked) return;
locked = true;
window.requestAnimationFrame(() => {
// @ts-ignore
fn.apply(this, args);
locked = false;
});
};
}

3
types/store.d.ts vendored
View File

@ -1,5 +1,6 @@
import { ErrorTypeEnum } from '/@/enums/exceptionEnum';
import { MenuModeEnum, MenuTypeEnum } from '/@/enums/menuEnum';
import { RoleInfo } from '/@/api/sys/model/userModel';
// Lock screen information
export interface LockInfo {
@ -35,6 +36,8 @@ export interface UserInfo {
realName: string;
avatar: string;
desc?: string;
homePath?: string;
roles: RoleInfo[];
}
export interface BeforeMiniState {

2156
yarn.lock

File diff suppressed because it is too large Load Diff