Compare commits

..

8 Commits

Author SHA1 Message Date
sanmen359
de231901a6 main代码合并到electron-main (#1834)
* upgrade electron version:11->18.

upgrade electron version:11->18.
Packaging failed because the original electronic version was too old.
将electron 11 改成18.因为11太老导致打包错误。

* main->electron-main合并代码

* main->electron-main合并代码

* main->electron-main合并代码
2022-04-21 13:34:29 +08:00
vben
006d93229c chore: delete workflows 2021-09-14 00:17:19 +08:00
vben
563f2b470e chore: merge branch 'main' of github.com:anncwb/vue-vben-admin into main 2021-09-14 00:13:51 +08:00
Vben
99cabf048d chore: merge main 2021-06-20 21:50:38 +08:00
Vben
941f0cc96c feat: support electron 2021-06-08 02:18:02 +08:00
vben
8dfa1778e8 wip: support electron 2021-06-08 01:26:18 +08:00
Vben
9a2577465e chore: merge main 2021-06-07 22:38:40 +08:00
Gavin Chain
d4c4215b08 wip: support electron 2021-06-07 22:33:38 +08:00
204 changed files with 16236 additions and 13659 deletions

View File

@@ -2,7 +2,7 @@
VITE_USE_MOCK = true
# public path
VITE_PUBLIC_PATH = /
VITE_PUBLIC_PATH = ./
# Delete console
VITE_DROP_CONSOLE = true

View File

@@ -1,6 +1,4 @@
// @ts-check
const { defineConfig } = require('eslint-define-config');
module.exports = defineConfig({
module.exports = {
root: true,
env: {
browser: true,
@@ -20,9 +18,7 @@ module.exports = defineConfig({
extends: [
'plugin:vue/vue3-recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
'plugin:prettier/recommended',
'plugin:jest/recommended',
],
rules: {
'vue/script-setup-uses-vars': 'error',
@@ -62,6 +58,7 @@ module.exports = defineConfig({
'vue/singleline-html-element-content-newline': 'off',
'vue/attribute-hyphenation': 'off',
'vue/require-default-prop': 'off',
'vue/require-explicit-emits': 'off',
'vue/html-self-closing': [
'error',
{
@@ -74,5 +71,6 @@ module.exports = defineConfig({
math: 'always',
},
],
'vue/multi-word-component-names': 'off',
},
});
};

View File

@@ -1,118 +0,0 @@
name: deploy
on:
push:
branches:
- main
jobs:
# push-to-ftp:
# if: "contains(github.event.head_commit.message, '[deploy]')"
# runs-on: ubuntu-latest
# steps:
# - name: Checkout
# uses: actions/checkout@v2
# - name: Sed Config Base
# shell: bash
# run: |
# sed -i 's#VITE_PUBLIC_PATH\s*=.*#VITE_PUBLIC_PATH = /next/#g' ./.env.production
# sed -i "s#VITE_BUILD_COMPRESS\s*=.*#VITE_BUILD_COMPRESS = 'gzip'#g" ./.env.production
# cat ./.env.production
# - name: use Node.js 14
# uses: actions/setup-node@v2.1.2
# with:
# node-version: '14.x'
# - name: Get yarn cache
# id: yarn-cache
# run: echo "::set-output name=dir::$(yarn cache dir)"
# - name: Cache dependencies
# uses: actions/cache@v2
# with:
# path: ${{ steps.yarn-cache.outputs.dir }}
# key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
# restore-keys: |
# ${{ runner.os }}-yarn-
# - name: Build
# run: |
# yarn install
# yarn run build
# - name: Deploy
# uses: SamKirkland/FTP-Deploy-Action@2.0.0
# env:
# FTP_SERVER: ${{ secrets.FTP_SERVER }}
# FTP_USERNAME: ${{ secrets.FTP_USERNAME }}
# FTP_PASSWORD: ${{ secrets.FTP_PASSWORD }}
# METHOD: sftp
# PORT: ${{ secrets.FTP_PORT }}
# LOCAL_DIR: dist
# REMOTE_DIR: /srv/www/vben-admin
# ARGS: --delete --verbose --parallel=80
push-to-gh-pages:
if: "contains(github.event.head_commit.message, '[release]')"
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Sed Config Base
shell: bash
run: |
sed -i 's#VITE_PUBLIC_PATH\s*=.*#VITE_PUBLIC_PATH = /vue-vben-admin/#g' ./.env.production
sed -i "s#VITE_BUILD_COMPRESS\s*=.*#VITE_BUILD_COMPRESS = 'gzip'#g" ./.env.production
cat ./.env.production
- name: use Node.js 16
uses: actions/setup-node@v2.1.2
with:
node-version: '16.x'
- name: Get yarn cache
id: yarn-cache
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Cache dependencies
uses: actions/cache@v2
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Set SSH Environment
env:
DOCS_DEPLOY_KEY: ${{ secrets.ACTIONS_DEPLOY_KEY }}
run: |
mkdir -p ~/.ssh/
echo "$ACTIONS_DEPLOY_KEY" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan github.com > ~/.ssh/known_hosts
chmod 700 ~/.ssh && chmod 600 ~/.ssh/*
git config --local user.email "vbenadmin@163.com"
git config --local user.name "vbenAdmin"
- name: Delete gh-pages branch
run: |
git push origin --delete gh-pages
- name: Build
run: |
yarn install
yarn run build
touch dist/.nojekyll
cp dist/index.html dist/404.html
- name: Deploy
uses: peaceiris/actions-gh-pages@v2.5.0
env:
ACTIONS_DEPLOY_KEY: ${{secrets.ACTIONS_DEPLOY_KEY}}
PUBLISH_BRANCH: gh-pages
PUBLISH_DIR: ./dist
with:
forceOrphan: true

View File

@@ -1,56 +0,0 @@
name: schedule-push-to-ftp
# Timed deployment project
on:
push:
schedule:
- cron: '0 20 * * *'
jobs:
schedule-push-to-ftp:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Sed Config Base
shell: bash
run: |
sed -i 's#VITE_PUBLIC_PATH\s*=.*#VITE_PUBLIC_PATH = /next/#g' ./.env.production
sed -i "s#VITE_BUILD_COMPRESS\s*=.*#VITE_BUILD_COMPRESS = 'gzip'#g" ./.env.production
sed -i "s#VITE_DROP_CONSOLE\s*=.*#VITE_DROP_CONSOLE = true#g" ./.env.production
cat ./.env.production
- name: use Node.js 16
uses: actions/setup-node@v2.1.2
with:
node-version: '16.x'
- name: Get yarn cache
id: yarn-cache
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Cache dependencies
uses: actions/cache@v2
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Build
run: |
yarn install
yarn run build
- name: Deploy
uses: SamKirkland/FTP-Deploy-Action@2.0.0
env:
FTP_SERVER: ${{ secrets.FTP_SERVER }}
FTP_USERNAME: ${{ secrets.FTP_USERNAME }}
FTP_PASSWORD: ${{ secrets.FTP_PASSWORD }}
METHOD: sftp
PORT: ${{ secrets.FTP_PORT }}
LOCAL_DIR: dist
REMOTE_DIR: /srv/www/vben-admin
ARGS: --delete --verbose --parallel=80

View File

@@ -2,5 +2,5 @@ ports:
- port: 3344
onOpen: open-preview
tasks:
- init: yarn
command: yarn dev
- init: pnpm install
command: pnpm run dev

View File

@@ -1,8 +0,0 @@
module.exports = {
'*.{js,jsx,ts,tsx}': ['eslint --fix', 'prettier --write'],
'{!(package)*.json,*.code-snippets,.!(browserslist)*rc}': ['prettier --write--parser json'],
'package.json': ['prettier --write'],
'*.vue': ['eslint --fix', 'prettier --write', 'stylelint --fix'],
'*.{scss,less,styl,html}': ['stylelint --fix', 'prettier --write'],
'*.md': ['prettier --write'],
};

View File

@@ -1,6 +1,6 @@
{
"recommendations": [
"octref.vetur",
"johnsoncodehk.volar",
"dbaeumer.vscode-eslint",
"stylelint.vscode-stylelint",
"esbenp.prettier-vscode",

2
.vscode/launch.json vendored
View File

@@ -5,7 +5,7 @@
"type": "chrome",
"request": "launch",
"name": "Launch Chrome",
"url": "http://localhost:3100",
"url": "https://localhost:3100",
"webRoot": "${workspaceFolder}/src",
"sourceMaps": true
}

17
.vscode/settings.json vendored
View File

@@ -1,16 +1,10 @@
{
"typescript.tsdk": "./node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"volar.tsPlugin": true,
"volar.tsPluginStatus": false,
//===========================================
//============= Editor ======================
//===========================================
"npm.packageManager": "pnpm",
"editor.tabSize": 2,
"editor.defaultFormatter": "esbenp.prettier-vscode",
//===========================================
//============= files =======================
//===========================================
"files.eol": "\n",
"search.exclude": {
"**/node_modules": true,
@@ -61,7 +55,7 @@
"**/yarn.lock": true
},
"stylelint.enable": true,
"stylelint.packageManager": "yarn",
"stylelint.validate": ["css", "less", "postcss", "scss", "vue", "sass"],
"path-intellisense.mappings": {
"/@/": "${workspaceRoot}/src"
},
@@ -94,7 +88,8 @@
},
"[vue]": {
"editor.codeActionsOnSave": {
"source.fixAll.eslint": false
"source.fixAll.eslint": true,
"source.fixAll.stylelint": true
}
},
"i18n-ally.localesPaths": ["src/locales/lang"],
@@ -137,6 +132,8 @@
"lintstagedrc",
"brotli",
"tailwindcss",
"sider"
"sider",
"pnpm",
"antd"
]
}

View File

@@ -1,48 +0,0 @@
# test directories
__tests__
test
tests
powered-test
# asset directories
docs
doc
website
images
assets
# examples
example
examples
# code coverage directories
coverage
.nyc_output
# build scripts
Makefile
Gulpfile.js
Gruntfile.js
# configs
appveyor.yml
circle.yml
codeship-services.yml
codeship-steps.yml
wercker.yml
.tern-project
.gitattributes
.editorconfig
.*ignore
.eslintrc
.jshintrc
.flowconfig
.documentup.json
.yarn-metadata.json
.travis.yml
# misc
*.md
!istanbul-reports/lib/html/assets
!istanbul-api/node_modules/istanbul-reports/lib/html/assets

View File

@@ -1,4 +1,4 @@
## [2.7.2](https://github.com/anncwb/vue-vben-admin/compare/v2.7.1...v2.7.2) (2021-09-13)
## [2.8.0](https://github.com/anncwb/vue-vben-admin/compare/v2.7.2...v2.8.0) (2021-11-03)
### Bug Fixes

View File

@@ -70,20 +70,20 @@ git clone https://github.com/anncwb/vue-vben-admin.git
```bash
cd vue-vben-admin
yarn install
pnpm install
```
- run
```bash
yarn serve
pnpm serve
```
- build
```bash
yarn build
pnpm build
```
## Change Log

View File

@@ -70,20 +70,20 @@ git clone https://github.com/anncwb/vue-vben-admin.git
```bash
cd vue-vben-admin
yarn install
pnpm install
```
- 运行
```bash
yarn serve
pnpm serve
```
- 打包
```bash
yarn build
pnpm build
```
## 更新日志

View File

@@ -0,0 +1,63 @@
import path from 'path';
import { RollupOptions } from 'rollup';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import esbuild from 'rollup-plugin-esbuild';
import alias from '@rollup/plugin-alias';
import json from '@rollup/plugin-json';
export function getRollupOptions(): RollupOptions {
return {
input: path.join(__dirname, '../../electron-main/index.ts'),
output: {
file: path.join(__dirname, '../../dist/main/build.js'),
format: 'cjs',
name: 'ElectronMainBundle',
sourcemap: true,
},
plugins: [
nodeResolve({ preferBuiltins: true, browser: true }), // 消除碰到 node.js 模块时⚠警告
commonjs(),
json(),
esbuild({
// All options are optional
include: /\.[jt]sx?$/, // default, inferred from `loaders` option
exclude: /node_modules/, // default
// watch: process.argv.includes('--watch'), // rollup 中有配置
sourceMap: false, // default
minify: process.env.NODE_ENV === 'production',
target: 'es2017', // default, or 'es20XX', 'esnext'
jsxFactory: 'React.createElement',
jsxFragment: 'React.Fragment',
// Like @rollup/plugin-replace
define: {
__VERSION__: '"x.y.z"',
},
// Add extra loaders
loaders: {
// Add .json files support
// require @rollup/plugin-commonjs
'.json': 'json',
// Enable JSX in .js files too
'.js': 'jsx',
},
}),
alias({
entries: [{ find: '/@main/', replacement: path.join(__dirname, '../../electron-main') }],
}),
],
external: [
'crypto',
'assert',
'fs',
'util',
'os',
'events',
'child_process',
'http',
'https',
'path',
'electron',
],
};
}

View File

@@ -1,7 +1,7 @@
import path from 'path';
import fs from 'fs-extra';
import inquirer from 'inquirer';
import chalk from 'chalk';
import colors from 'picocolors';
import pkg from '../../../package.json';
async function generateIcon() {
@@ -64,7 +64,7 @@ async function generateIcon() {
}
fs.emptyDir(path.join(process.cwd(), 'node_modules/.vite'));
console.log(
`${chalk.cyan(`[${pkg.name}]`)}` + ' - Icon generated successfully:' + `[${prefixSet}]`,
`${colors.cyan(`[${pkg.name}]`)}` + ' - Icon generated successfully:' + `[${prefixSet}]`,
);
});
}

View File

@@ -3,7 +3,7 @@
*/
import { GLOB_CONFIG_FILE_NAME, OUTPUT_DIR } from '../constant';
import fs, { writeFileSync } from 'fs-extra';
import chalk from 'chalk';
import colors from 'picocolors';
import { getEnvConfig, getRootPath } from '../utils';
import { getConfigFileName } from '../getConfigFileName';
@@ -31,10 +31,10 @@ function createConfig(params: CreateConfigParams) {
fs.mkdirp(getRootPath(OUTPUT_DIR));
writeFileSync(getRootPath(`${OUTPUT_DIR}/${configFileName}`), configStr);
console.log(chalk.cyan(`✨ [${pkg.name}]`) + ` - configuration file is build successfully:`);
console.log(chalk.gray(OUTPUT_DIR + '/' + chalk.green(configFileName)) + '\n');
console.log(colors.cyan(`✨ [${pkg.name}]`) + ` - configuration file is build successfully:`);
console.log(colors.gray(OUTPUT_DIR + '/' + colors.green(configFileName)) + '\n');
} catch (error) {
console.log(chalk.red('configuration file configuration file failed to package:\n' + error));
console.log(colors.red('configuration file configuration file failed to package:\n' + error));
}
}

View File

@@ -0,0 +1,76 @@
import rollup, { OutputOptions } from 'rollup';
import chalk from 'chalk';
import ora from 'ora';
import waitOn from 'wait-on';
import net from 'net';
import { URL } from 'url';
import minimist from 'minimist';
import electronConnect from 'electron-connect';
import { getRollupOptions } from '../config/rollupElectronConfig';
const argv = minimist(process.argv.slice(2));
const TAG = '[compiler-electron]';
export function startCompilerElectron(port = 80) {
// 因为 vite 不会重定向到 index.html所以直接写 index.html 路由。
const ELECTRON_URL = `https://localhost:${port}/index.html`;
const spinner = ora(`${TAG} Electron build...`);
const electron = electronConnect.server.create({ stopOnClose: true });
const rollupOptions = getRollupOptions();
function watchFunc() {
// once here, all resources are available
const watcher = rollup.watch(rollupOptions);
watcher.on('change', (filename) => {
const log = chalk.green(`change -- ${filename}`);
console.log(TAG, log);
});
watcher.on('event', (ev) => {
if (ev.code === 'END') {
// init-未启动、started-第一次启动、restarted-重新启动
electron.electronState === 'init' ? electron.start() : electron.restart();
} else if (ev.code === 'ERROR') {
console.log(ev.error);
}
});
}
if (argv.watch) {
waitOn(
{
resources: [ELECTRON_URL],
timeout: 5000,
},
(err) => {
if (err) {
const { hostname } = new URL(ELECTRON_URL);
const serverSocket = net.connect(port, hostname, () => {
watchFunc();
});
serverSocket.on('error', (e) => {
console.log(err);
console.log(e);
process.exit(1);
});
} else {
watchFunc();
}
}
);
} else {
spinner.start();
rollup
.rollup(rollupOptions)
.then((build) => {
spinner.stop();
console.log(TAG, chalk.green('Electron build successed.'));
build.write(rollupOptions.output as OutputOptions);
})
.catch((error) => {
spinner.stop();
console.log(`\n${TAG} ${chalk.red('构建报错')}\n`, error, '\n');
});
}
}

View File

@@ -1,7 +1,7 @@
// #!/usr/bin/env node
import { runBuildConfig } from './buildConf';
import chalk from 'chalk';
import colors from 'picocolors';
import pkg from '../../package.json';
@@ -14,9 +14,9 @@ export const runBuild = async () => {
runBuildConfig();
}
console.log(`${chalk.cyan(`[${pkg.name}]`)}` + ' - build successfully!');
console.log(`${colors.cyan(`[${pkg.name}]`)}` + ' - build successfully!');
} catch (error) {
console.log(chalk.red('vite build error:\n' + error));
console.log(colors.red('vite build error:\n' + error));
process.exit(1);
}
};

View File

@@ -0,0 +1,22 @@
import { createServer } from 'vite';
import path from 'path';
import { startCompilerElectron } from './compilerElectron';
import minimist from 'minimist';
(async () => {
const argv = minimist(process.argv.slice(2));
console.log(argv);
const isDev = argv.env === 'development';
let port: number | undefined = undefined;
if (isDev) {
const server = await createServer({
root: path.resolve(__dirname, '../../'),
});
const app = await server.listen();
port = app.config.server.port;
process.env.PORT = `${port}`;
}
startCompilerElectron(port);
})();

View File

@@ -1,21 +0,0 @@
// TODO
import type { GetManualChunk } from 'rollup';
//
const vendorLibs: { match: string[]; output: string }[] = [
// {
// match: ['xlsx'],
// output: 'xlsx',
// },
];
// @ts-ignore
export const configManualChunk: GetManualChunk = (id: string) => {
if (/[\\/]node_modules[\\/]/.test(id)) {
const matchItem = vendorLibs.find((item) => {
const reg = new RegExp(`[\\/]node_modules[\\/]_?(${item.match.join('|')})(.*)`, 'ig');
return reg.test(id);
});
return matchItem ? matchItem.output : null;
}
};

View File

@@ -2,16 +2,16 @@
* Used to package and output gzip. Note that this does not work properly in Vite, the specific reason is still being investigated
* https://github.com/anncwb/vite-plugin-compression
*/
import type { Plugin } from 'vite';
import type { PluginOption } from 'vite';
import compressPlugin from 'vite-plugin-compression';
export function configCompressPlugin(
compress: 'gzip' | 'brotli' | 'none',
deleteOriginFile = false,
): Plugin | Plugin[] {
): PluginOption | PluginOption[] {
const compressList = compress.split(',');
const plugins: Plugin[] = [];
const plugins: PluginOption[] = [];
if (compressList.includes('gzip')) {
plugins.push(

View File

@@ -1,25 +0,0 @@
import type { Plugin } from 'vite';
/**
* TODO
* Temporarily solve the Vite circular dependency problem, and wait for a better solution to fix it later. I don't know what problems this writing will bring.
* @returns
*/
export function configHmrPlugin(): Plugin {
return {
name: 'singleHMR',
handleHotUpdate({ modules, file }) {
if (file.match(/xml$/)) return [];
modules.forEach((m) => {
if (!m.url.match(/\.(css|less)/)) {
m.importedModules = new Set();
m.importers = new Set();
}
});
return modules;
},
};
}

View File

@@ -2,8 +2,8 @@
* Plugin to minimize and use ejs template syntax in index.html.
* https://github.com/anncwb/vite-plugin-html
*/
import type { Plugin } from 'vite';
import html from 'vite-plugin-html';
import type { PluginOption } from 'vite';
import { createHtmlPlugin } from 'vite-plugin-html';
import pkg from '../../../package.json';
import { GLOB_CONFIG_FILE_NAME } from '../../constant';
@@ -16,7 +16,7 @@ export function configHtmlPlugin(env: ViteEnv, isBuild: boolean) {
return `${path || '/'}${GLOB_CONFIG_FILE_NAME}?v=${pkg.version}-${new Date().getTime()}`;
};
const htmlPlugin: Plugin[] = html({
const htmlPlugin: PluginOption[] = createHtmlPlugin({
minify: isBuild,
inject: {
// Inject data into ejs template

View File

@@ -1,9 +1,10 @@
import type { Plugin } from 'vite';
import { PluginOption } from 'vite';
import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
import legacy from '@vitejs/plugin-legacy';
import purgeIcons from 'vite-plugin-purge-icons';
import windiCSS from 'vite-plugin-windicss';
import VitePluginCertificate from 'vite-plugin-mkcert';
import vueSetupExtend from 'vite-plugin-vue-setup-extend';
import { configHtmlPlugin } from './html';
import { configPwaConfig } from './pwa';
@@ -14,7 +15,6 @@ import { configVisualizerConfig } from './visualizer';
import { configThemePlugin } from './theme';
import { configImageminPlugin } from './imagemin';
import { configSvgIconsPlugin } from './svgSprite';
import { configHmrPlugin } from './hmr';
export function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean) {
const {
@@ -25,21 +25,21 @@ export function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean) {
VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE,
} = viteEnv;
const vitePlugins: (Plugin | Plugin[])[] = [
const vitePlugins: (PluginOption | PluginOption[])[] = [
// have to
vue(),
// have to
vueJsx(),
// support name
vueSetupExtend(),
VitePluginCertificate({
source: 'coding',
}),
];
// vite-plugin-windicss
vitePlugins.push(windiCSS());
// TODO
!isBuild && vitePlugins.push(configHmrPlugin());
// @vitejs/plugin-legacy
VITE_LEGACY && isBuild && vitePlugins.push(legacy());
@@ -61,12 +61,12 @@ export function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean) {
// rollup-plugin-visualizer
vitePlugins.push(configVisualizerConfig());
//vite-plugin-theme
// vite-plugin-theme
vitePlugins.push(configThemePlugin(isBuild));
// The following plugins only work in the production environment
if (isBuild) {
//vite-plugin-imagemin
// vite-plugin-imagemin
VITE_USE_IMAGEMIN && vitePlugins.push(configImageminPlugin());
// rollup-plugin-gzip

View File

@@ -2,13 +2,13 @@
* Introduces component library styles on demand.
* https://github.com/anncwb/vite-plugin-style-import
*/
import styleImport from 'vite-plugin-style-import';
import { createStyleImportPlugin } from 'vite-plugin-style-import';
export function configStyleImportPlugin(isBuild: boolean) {
if (!isBuild) {
return [];
}
const styleImportPlugin = styleImport({
export function configStyleImportPlugin(_isBuild: boolean) {
// if (!isBuild) {
// return [];
// }
const styleImportPlugin = createStyleImportPlugin({
libs: [
{
libraryName: 'ant-design-vue',
@@ -19,6 +19,7 @@ export function configStyleImportPlugin(isBuild: boolean) {
'anchor-link',
'sub-menu',
'menu-item',
'menu-divider',
'menu-item-group',
'breadcrumb-item',
'breadcrumb-separator',

View File

@@ -3,11 +3,11 @@
* https://github.com/anncwb/vite-plugin-svg-icons
*/
import SvgIconsPlugin from 'vite-plugin-svg-icons';
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
import path from 'path';
export function configSvgIconsPlugin(isBuild: boolean) {
const svgIconsPlugin = SvgIconsPlugin({
const svgIconsPlugin = createSvgIconsPlugin({
iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
svgoOptions: isBuild,
// default

View File

@@ -2,7 +2,7 @@
* Vite plugin for website theme color switching
* https://github.com/anncwb/vite-plugin-theme
*/
import type { Plugin } from 'vite';
import type { PluginOption } from 'vite';
import path from 'path';
import {
viteThemePlugin,
@@ -14,7 +14,7 @@ import {
import { getThemeColors, generateColors } from '../../config/themeConfig';
import { generateModifyVars } from '../../generate/generateModifyVars';
export function configThemePlugin(isBuild: boolean): Plugin[] {
export function configThemePlugin(isBuild: boolean): PluginOption[] {
const colors = generateColors({
mixDarken,
mixLighten,
@@ -85,5 +85,5 @@ export function configThemePlugin(isBuild: boolean): Plugin[] {
}),
];
return plugin as unknown as Plugin[];
return plugin as unknown as PluginOption[];
}

64
electron-main/index.ts Normal file
View File

@@ -0,0 +1,64 @@
import { app, BrowserWindow, screen } from 'electron';
import is_dev from 'electron-is-dev';
import { join } from 'path';
let mainWindow: BrowserWindow | null = null;
class createWin {
constructor() {
const displayWorkAreaSize = screen.getAllDisplays()[0].workArea;
mainWindow = new BrowserWindow({
width: parseInt(`${displayWorkAreaSize.width * 0.85}`, 10),
height: parseInt(`${displayWorkAreaSize.height * 0.85}`, 10),
movable: true,
// frame: false,
show: false,
center: true,
resizable: true,
// transparent: true,
titleBarStyle: 'default',
webPreferences: {
devTools: true,
contextIsolation: false,
nodeIntegration: true,
enableRemoteModule: true,
},
backgroundColor: '#fff',
});
const URL = is_dev
? `https://localhost:${process.env.PORT}` // vite 启动的服务器地址
: `file://${join(__dirname, '../index.html')}`; // vite 构建后的静态文件地址
mainWindow.loadURL(URL);
mainWindow.on('ready-to-show', () => {
mainWindow.show();
});
}
}
app.whenReady().then(() => new createWin());
const isFirstInstance = app.requestSingleInstanceLock();
if (!isFirstInstance) {
app.quit();
} else {
app.on('second-instance', () => {
if (mainWindow) {
mainWindow.focus();
}
});
}
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
new createWin();
}
});

View File

@@ -8,7 +8,6 @@
name="viewport"
content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0"
/>
<title><%= title %></title>
<link rel="icon" href="/favicon.ico" />
</head>
@@ -30,7 +29,7 @@
}
html[data-theme='dark'] .app-loading .app-loading-title {
color: rgba(255, 255, 255, 0.85);
color: rgb(255 255 255 / 85%);
}
.app-loading {
@@ -48,7 +47,6 @@
top: 50%;
left: 50%;
display: flex;
-webkit-transform: translate3d(-50%, -50%, 0);
transform: translate3d(-50%, -50%, 0);
justify-content: center;
align-items: center;
@@ -66,7 +64,7 @@
display: flex;
margin-top: 30px;
font-size: 30px;
color: rgba(0, 0, 0, 0.85);
color: rgb(0 0 0 / 85%);
justify-content: center;
align-items: center;
}
@@ -97,7 +95,7 @@
height: 20px;
background-color: #0065cc;
border-radius: 100%;
opacity: 0.3;
opacity: 30%;
transform: scale(0.75);
animation: antSpinMove 1s infinite linear alternate;
transform-origin: 50% 50%;
@@ -111,43 +109,38 @@
.dot i:nth-child(2) {
top: 0;
right: 0;
-webkit-animation-delay: 0.4s;
animation-delay: 0.4s;
}
.dot i:nth-child(3) {
right: 0;
bottom: 0;
-webkit-animation-delay: 0.8s;
animation-delay: 0.8s;
}
.dot i:nth-child(4) {
bottom: 0;
left: 0;
-webkit-animation-delay: 1.2s;
animation-delay: 1.2s;
}
@keyframes antRotate {
to {
-webkit-transform: rotate(405deg);
transform: rotate(405deg);
}
}
@-webkit-keyframes antRotate {
@keyframes antRotate {
to {
-webkit-transform: rotate(405deg);
transform: rotate(405deg);
}
}
@keyframes antSpinMove {
to {
opacity: 1;
opacity: 100%;
}
}
@-webkit-keyframes antSpinMove {
@keyframes antSpinMove {
to {
opacity: 1;
opacity: 100%;
}
}
</style>

View File

@@ -1,36 +0,0 @@
export default {
preset: 'ts-jest',
roots: ['<rootDir>/tests/'],
clearMocks: true,
moduleDirectories: ['node_modules', 'src'],
moduleFileExtensions: ['js', 'ts', 'vue', 'tsx', 'jsx', 'json', 'node'],
modulePaths: ['<rootDir>/src', '<rootDir>/node_modules'],
testMatch: [
'**/tests/**/*.[jt]s?(x)',
'**/?(*.)+(spec|test).[tj]s?(x)',
'(/__tests__/.*|(\\.|/)(test|spec))\\.(js|ts)$',
],
testPathIgnorePatterns: [
'<rootDir>/tests/server/',
'<rootDir>/tests/__mocks__/',
'/node_modules/',
],
transform: {
'^.+\\.tsx?$': 'ts-jest',
},
transformIgnorePatterns: ['<rootDir>/tests/__mocks__/', '/node_modules/'],
// A map from regular expressions to module names that allow to stub out resources with a single module
moduleNameMapper: {
'\\.(vs|fs|vert|frag|glsl|jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
'<rootDir>/tests/__mocks__/fileMock.ts',
'\\.(sass|s?css|less)$': '<rootDir>/tests/__mocks__/styleMock.ts',
'\\?worker$': '<rootDir>/tests/__mocks__/workerMock.ts',
'^/@/(.*)$': '<rootDir>/src/$1',
},
testEnvironment: 'jsdom',
verbose: true,
collectCoverage: false,
coverageDirectory: 'coverage',
collectCoverageFrom: ['src/**/*.{js,ts,vue}'],
coveragePathIgnorePatterns: ['^.+\\.d\\.ts$'],
};

View File

@@ -111,4 +111,12 @@ export default [
return resultSuccess(undefined, { message: 'Token has been destroyed' });
},
},
{
url: '/basic-api/testRetry',
statusCode: 405,
method: 'get',
response: () => {
return resultError('Error!');
},
},
] as MockMethod[];

View File

@@ -1,18 +1,61 @@
{
"name": "vben-admin",
"version": "2.7.2",
"version": "2.8.0",
"author": {
"name": "vben",
"email": "anncwb@126.com",
"url": "https://github.com/anncwb"
},
"main": "dist/main/build.js",
"build": {
"appId": "xxx@gmail.com",
"electronDownload": {
"mirror": "https://npm.taobao.org/mirrors/electron/"
},
"files": [
"!node_modules",
"dist/**"
],
"asar": false,
"mac": {
"artifactName": "${productName}_setup_${version}.${ext}",
"target": [
"dmg"
]
},
"linux": {
"icon": "build/icons/512x512.png",
"target": [
"deb"
]
},
"win": {
"target": [
{
"target": "nsis",
"arch": [
"x64"
]
}
],
"artifactName": "${productName}_setup_${version}.${ext}"
},
"nsis": {
"oneClick": false,
"perMachine": false,
"allowToChangeInstallationDirectory": true,
"deleteAppDataOnUninstall": false
}
},
"scripts": {
"bootstrap": "yarn install",
"bootstrap": "pnpm install",
"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",
"build:no-cache": "pnpm clean:cache && npm run build",
"dev:app": "esno ./build/script/startElectron.ts --env=development --watch",
"build:app": "npm run build && esno ./build/script/startElectron.ts --env=production && electron-builder ",
"report": "cross-env REPORT=true npm run build",
"type:check": "vue-tsc --noEmit --skipLibCheck",
"preview": "npm run build && vite preview",
@@ -23,130 +66,147 @@
"lint:eslint": "eslint --cache --max-warnings 0 \"{src,mock}/**/*.{vue,ts,tsx}\" --fix",
"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:lint-staged": "lint-staged",
"test:unit": "jest",
"test:unit-coverage": "jest --coverage",
"test:gzip": "npx http-server dist --cors --gzip -c-1",
"test:br": "npx http-server dist --cors --brotli -c-1",
"reinstall": "rimraf yarn.lock && rimraf package.lock.json && rimraf node_modules && npm run bootstrap",
"reinstall": "rimraf pnpm-lock.yaml && rimraf package.lock.json && rimraf node_modules && npm run bootstrap",
"prepare": "husky install",
"gen:icon": "esno ./build/generate/icon/index.ts"
},
"dependencies": {
"@ant-design/colors": "^6.0.0",
"@ant-design/icons-vue": "^6.0.1",
"@iconify/iconify": "^2.0.4",
"@logicflow/core": "^0.7.2",
"@logicflow/extension": "^0.7.2",
"@vueuse/core": "^6.7.4",
"@vueuse/shared": "^6.7.4",
"@zxcvbn-ts/core": "^1.0.0-beta.0",
"ant-design-vue": "2.2.8",
"axios": "^0.24.0",
"codemirror": "^5.63.3",
"@ant-design/icons-vue": "^6.1.0",
"@logicflow/core": "^0.6.16",
"@logicflow/extension": "^0.6.16",
"@iconify/iconify": "^2.2.1",
"@vue/runtime-core": "^3.2.31",
"@vue/shared": "^3.2.31",
"@vueuse/core": "^6.3.3",
"@vueuse/shared": "^6.3.3",
"@zxcvbn-ts/core": "^2.0.1",
"ant-design-vue": "3.1.1",
"axios": "^0.21.4",
"codemirror": "^5.62.3",
"cropperjs": "^1.5.12",
"electron-is-dev": "^1.2.0",
"crypto-js": "^4.1.1",
"echarts": "^5.2.2",
"dayjs": "^1.11.0",
"echarts": "^5.2.0",
"intro.js": "^4.2.2",
"lodash-es": "^4.17.21",
"mockjs": "^1.1.0",
"moment": "^2.29.1",
"nprogress": "^0.2.0",
"path-to-regexp": "^6.2.0",
"pinia": "2.0.0",
"pinia": "2.0.12",
"print-js": "^1.6.0",
"qrcode": "^1.4.4",
"qs": "^6.10.1",
"qs": "^6.10.3",
"resize-observer-polyfill": "^1.5.1",
"showdown": "^1.9.1",
"sortablejs": "^1.14.0",
"tinymce": "^5.10.0",
"vditor": "^3.8.7",
"vue": "^3.2.21",
"tinymce": "^5.9.2",
"vditor": "^3.8.13",
"vue": "^3.2.31",
"vue-i18n": "^9.1.9",
"vue-json-pretty": "^2.0.4",
"vue-router": "^4.0.12",
"vue-json-pretty": "^2.0.6",
"vue-router": "^4.0.14",
"vue-types": "^4.1.1",
"xlsx": "^0.17.3"
"xlsx": "^0.18.5"
},
"devDependencies": {
"@commitlint/cli": "^14.1.0",
"@commitlint/config-conventional": "^14.1.0",
"@iconify/json": "^1.1.422",
"@commitlint/cli": "^13.1.0",
"@commitlint/config-conventional": "^13.1.0",
"@iconify/json": "^1.1.401",
"@purge-icons/generated": "^0.7.0",
"@types/codemirror": "^5.60.5",
"@rollup/plugin-alias": "^3.1.1",
"@rollup/plugin-commonjs": "^15.0.0",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^9.0.0",
"@types/codemirror": "^5.60.2",
"@types/crypto-js": "^4.0.2",
"@types/fs-extra": "^9.0.13",
"@types/inquirer": "^8.1.3",
"@types/fs-extra": "^9.0.12",
"@types/inquirer": "^8.1.1",
"@types/intro.js": "^3.0.2",
"@types/jest": "^27.0.2",
"@types/jest": "^27.0.1",
"@types/lodash-es": "^4.17.5",
"@types/mockjs": "^1.0.4",
"@types/node": "^16.11.6",
"@types/node": "^16.9.1",
"@types/nprogress": "^0.2.0",
"@types/qrcode": "^1.4.1",
"@types/qs": "^6.9.7",
"@types/showdown": "^1.9.4",
"@types/sortablejs": "^1.10.7",
"@typescript-eslint/eslint-plugin": "^5.3.0",
"@typescript-eslint/parser": "^5.3.0",
"@vitejs/plugin-legacy": "^1.6.2",
"@vitejs/plugin-vue": "^1.9.4",
"@vitejs/plugin-vue-jsx": "^1.2.0",
"@vue/compiler-sfc": "3.2.21",
"@vue/test-utils": "^2.0.0-rc.16",
"autoprefixer": "^10.4.0",
"@typescript-eslint/eslint-plugin": "^4.31.0",
"@typescript-eslint/parser": "^4.31.0",
"@vitejs/plugin-legacy": "^1.5.3",
"@vitejs/plugin-vue": "^1.6.2",
"@vitejs/plugin-vue-jsx": "^1.1.8",
"@vue/compiler-sfc": "3.2.11",
"@vue/test-utils": "^2.0.0-rc.14",
"autoprefixer": "^10.3.4",
"commitizen": "^4.2.4",
"conventional-changelog-cli": "^2.1.1",
"cross-env": "^7.0.3",
"dotenv": "^10.0.0",
"eslint": "^8.1.0",
"electron": "^18.0.0",
"electron-builder": "^22.8.0",
"electron-connect": "^0.6.3",
"electron-contextmenu-middleware": "^1.0.3",
"electron-input-menu": "^2.1.0",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-define-config": "^1.1.2",
"eslint-plugin-jest": "^25.2.2",
"eslint-define-config": "^1.0.9",
"eslint-plugin-jest": "^24.4.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^8.0.3",
"esno": "^0.10.1",
"eslint-plugin-vue": "^7.17.0",
"esno": "^0.9.1",
"fs-extra": "^10.0.0",
"husky": "^7.0.4",
"inquirer": "^8.2.0",
"jest": "^27.3.1",
"less": "^4.1.2",
"lint-staged": "11.2.6",
"http-server": "^13.0.1",
"husky": "^7.0.2",
"inquirer": "^8.1.2",
"is-ci": "^3.0.0",
"jest": "^27.2.0",
"less": "^4.1.1",
"lint-staged": "12.3.7",
"npm-run-all": "^4.1.5",
"postcss": "^8.3.11",
"postcss-html": "^1.2.0",
"postcss-less": "^5.0.0",
"prettier": "^2.4.1",
"picocolors": "^1.0.0",
"postcss": "^8.3.6",
"postcss-html": "^1.3.0",
"postcss-less": "^6.0.0",
"prettier": "^2.4.0",
"pretty-quick": "^3.1.1",
"rimraf": "^3.0.2",
"rollup-plugin-visualizer": "^5.5.2",
"stylelint": "^14.0.1",
"stylelint-config-html": "^1.0.0",
"stylelint-config-prettier": "^9.0.3",
"stylelint-config-standard": "^23.0.0",
"stylelint-order": "^5.0.0",
"ts-jest": "^27.0.7",
"ts-node": "^10.4.0",
"typescript": "^4.4.4",
"vite": "^2.6.13",
"rollup-plugin-esbuild": "^3.0.2",
"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",
"ts-jest": "^27.0.5",
"ts-node": "^10.2.1",
"typescript": "4.4.3",
"vite": "^2.9.1",
"vite-plugin-compression": "^0.3.5",
"vite-plugin-html": "^2.1.1",
"vite-plugin-imagemin": "^0.4.6",
"vite-plugin-html": "^3.2.0",
"vite-plugin-imagemin": "^0.4.5",
"vite-plugin-mkcert": "^1.6.0",
"vite-plugin-mock": "^2.9.6",
"vite-plugin-purge-icons": "^0.7.0",
"vite-plugin-pwa": "^0.11.3",
"vite-plugin-style-import": "^1.3.0",
"vite-plugin-svg-icons": "^1.0.5",
"vite-plugin-pwa": "^0.11.2",
"vite-plugin-style-import": "^2.0.0",
"vite-plugin-svg-icons": "^2.0.1",
"vite-plugin-theme": "^0.8.1",
"wait-on": "^5.2.1",
"vite-plugin-vue-setup-extend": "^0.1.0",
"vite-plugin-windicss": "^1.4.12",
"vue-eslint-parser": "^8.0.1",
"vue-tsc": "^0.28.10"
"vite-plugin-windicss": "^1.4.2",
"vue-eslint-parser": "^7.11.0",
"vue-tsc": "^0.3.0"
},
"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.56.3"
"rollup": "^2.56.3",
"gifsicle": "5.2.0"
},
"repository": {
"type": "git",

11963
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,7 @@
import { useTitle } from '/@/hooks/web/useTitle';
import { useLocale } from '/@/locales/useLocale';
import 'dayjs/locale/zh-cn';
// support Multi-language
const { getAntdLocale } = useLocale();

View File

@@ -14,6 +14,7 @@ export const demoListApi = (params: DemoParams) =>
url: Api.DEMO_LIST,
params,
headers: {
// @ts-ignore
ignoreCancelToken: true,
},
});

View File

@@ -8,6 +8,7 @@ enum Api {
Logout = '/logout',
GetUserInfo = '/getUserInfo',
GetPermCode = '/getPermCode',
TestRetry = '/testRetry',
}
/**
@@ -39,3 +40,16 @@ export function getPermCode() {
export function doLogout() {
return defHttp.get({ url: Api.Logout });
}
export function testRetry() {
return defHttp.get(
{ url: Api.TestRetry },
{
retryRequest: {
isOpenRetry: true,
count: 5,
waitTime: 1000,
},
},
);
}

View File

@@ -4,11 +4,11 @@
-->
<template>
<Dropdown
placement="bottomCenter"
placement="bottom"
:trigger="['click']"
:dropMenuList="localeList"
:selectedKeys="selectedKeys"
@menuEvent="handleMenuEvent"
@menu-event="handleMenuEvent"
overlayClassName="app-locale-picker-overlay"
>
<span class="cursor-pointer flex items-center">

View File

@@ -3,7 +3,6 @@
<div class="p-4 mb-2 bg-white">
<BasicForm @register="registerForm" />
</div>
{{ sliderProp.width }}
<div class="p-2 bg-white">
<List
:grid="{ gutter: 5, xs: 1, sm: 2, md: 4, lg: 4, xl: 6, xxl: grid }"
@@ -39,7 +38,7 @@
<Image :src="item.imgs[0]" />
</div>
</template>
<template class="ant-card-actions" #actions>
<template #actions>
<!-- <SettingOutlined key="setting" />-->
<EditOutlined key="edit" />
<Dropdown

View File

@@ -1,5 +1,5 @@
import { ref } from 'vue';
//每行个数
// 每行个数
export const grid = ref(12);
// slider属性
export const useSlider = (min = 6, max = 12) => {

View File

@@ -53,7 +53,7 @@
color: var(--comment);
text-align: right;
white-space: nowrap;
opacity: 60%;
opacity: 0.6;
}
.CodeMirror-guttermarker {
@@ -90,7 +90,7 @@
display: inline-block;
font-size: 0.8em;
content: '>';
opacity: 80%;
opacity: 0.8;
transform: rotate(90deg);
transition: transform 0.2s;
}

View File

@@ -1,6 +1,6 @@
<template>
<div :class="prefixCls">
<CollapseHeader v-bind="$props" :prefixCls="prefixCls" :show="show" @expand="handleExpand">
<CollapseHeader v-bind="props" :prefixCls="prefixCls" :show="show" @expand="handleExpand">
<template #title>
<slot name="title"></slot>
</template>
@@ -25,6 +25,7 @@
<script lang="ts" setup>
import type { PropType } from 'vue';
import { ref } from 'vue';
import { isNil } from 'lodash-es';
// component
import { Skeleton } from 'ant-design-vue';
import { CollapseTransition } from '/@/components/Transition';
@@ -66,13 +67,17 @@
/**
* @description: Handling development events
*/
function handleExpand() {
show.value = !show.value;
function handleExpand(val: boolean) {
show.value = isNil(val) ? !show.value : val;
if (props.triggerWindowResize) {
// 200 milliseconds here is because the expansion has animation,
useTimeoutFn(triggerWindowResize, 200);
}
}
defineExpose({
handleExpand,
});
</script>
<style lang="less">
@prefix-cls: ~'@{namespace}-collapse-container';

View File

@@ -60,9 +60,11 @@
const top = body.clientHeight < y + menuHeight ? y - menuHeight : y;
return {
...styles,
position: 'absolute',
width: `${width}px`,
left: `${left + 1}px`,
top: `${top + 1}px`,
zIndex: 9999,
};
});
@@ -124,15 +126,11 @@
}
const { items } = props;
return (
<Menu
inlineIndent={12}
mode="vertical"
class={prefixCls}
ref={wrapRef}
style={unref(getStyle)}
>
{renderMenuItem(items)}
</Menu>
<div class={prefixCls}>
<Menu inlineIndent={12} mode="vertical" ref={wrapRef} style={unref(getStyle)}>
{renderMenuItem(items)}
</Menu>
</div>
);
};
},
@@ -185,6 +183,9 @@
background-clip: padding-box;
user-select: none;
&__item {
margin: 0 !important;
}
.item-style();
.ant-divider {

View File

@@ -22,7 +22,7 @@
<CopperModal
@register="register"
@uploadSuccess="handleUploadSuccess"
@upload-success="handleUploadSuccess"
:uploadApi="uploadApi"
:src="sourceValue"
/>

View File

@@ -3,7 +3,7 @@
import type { DescriptionsProps } from 'ant-design-vue/es/descriptions/index';
import type { CSSProperties } from 'vue';
import type { CollapseContainerOptions } from '/@/components/Container/index';
import { defineComponent, computed, ref, unref } from 'vue';
import { defineComponent, computed, ref, unref, toRefs } from 'vue';
import { get } from 'lodash-es';
import { Descriptions } from 'ant-design-vue';
import { CollapseContainer } from '/@/components/Container/index';
@@ -121,6 +121,9 @@
return null;
}
const getField = get(_data, field);
if (getField && !toRefs(_data).hasOwnProperty(field)) {
return isFunction(render) ? render('', _data) : '';
}
return isFunction(render) ? render(getField, _data) : getField ?? '';
};

View File

@@ -94,7 +94,7 @@
opt.width = '100%';
}
const detailCls = `${prefixCls}__detail`;
opt.wrapClassName = wrapClassName ? `${wrapClassName} ${detailCls}` : detailCls;
opt.class = wrapClassName ? `${wrapClassName} ${detailCls}` : detailCls;
if (!getContainer) {
// TODO type error?

View File

@@ -128,13 +128,12 @@ export interface DrawerProps extends DrawerFooterProps {
* @type any (string | slot)
*/
title?: VNodeChild | JSX.Element;
/**
* The class name of the container of the Drawer dialog.
* @type string
*/
wrapClassName?: string;
class?: string;
/**
* Style of wrapper element which **contains mask** compare to `drawerStyle`
* @type object

View File

@@ -1,4 +1,4 @@
import xlsx from 'xlsx';
import * as xlsx from 'xlsx';
import type { WorkBook } from 'xlsx';
import type { JsonToSheet, AoAToSheet } from './typing';

View File

@@ -14,7 +14,7 @@
</template>
<script lang="ts">
import { defineComponent, ref, unref } from 'vue';
import XLSX from 'xlsx';
import * as XLSX from 'xlsx';
import { dateUtil } from '/@/utils/dateUtil';
import type { ExcelData } from './typing';

View File

@@ -9,6 +9,7 @@ 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 { default as ApiTree } from './src/components/ApiTree.vue';
export { default as ApiRadioGroup } from './src/components/ApiRadioGroup.vue';
export { default as ApiCascader } from './src/components/ApiCascader.vue';

View File

@@ -58,6 +58,7 @@
import { createFormContext } from './hooks/useFormContext';
import { useAutoFocus } from './hooks/useAutoFocus';
import { useModalContext } from '/@/components/Modal';
import { useDebounceFn } from '@vueuse/core';
import { basicProps } from './props';
import { useDesign } from '/@/hooks/web/useDesign';
@@ -66,7 +67,7 @@
name: 'BasicForm',
components: { FormItem, Form, Row, FormAction },
props: basicProps,
emits: ['advanced-change', 'reset', 'submit', 'register'],
emits: ['advanced-change', 'reset', 'submit', 'register', 'field-value-change'],
setup(props, { emit, attrs }) {
const formModel = reactive<Recordable>({});
const modalFn = useModalContext();
@@ -122,7 +123,7 @@
if (!Array.isArray(defaultValue)) {
schema.defaultValue = dateUtil(defaultValue);
} else {
const def: moment.Moment[] = [];
const def: any[] = [];
defaultValue.forEach((item) => {
def.push(dateUtil(item));
});
@@ -225,6 +226,14 @@
},
);
watch(
() => formModel,
useDebounceFn(() => {
unref(getProps).submitOnChange && handleSubmit();
}, 300),
{ deep: true },
);
async function setProps(formProps: Partial<FormProps>): Promise<void> {
propsRef.value = deepMerge(unref(propsRef) || {}, formProps);
}
@@ -235,6 +244,7 @@
if (!validateTrigger || validateTrigger === 'change') {
validateFields([key]).catch((_) => {});
}
emit('field-value-change', key, value);
}
function handleEnterPress(e: KeyboardEvent) {

View File

@@ -24,6 +24,7 @@ import {
import ApiRadioGroup from './components/ApiRadioGroup.vue';
import RadioButtonGroup from './components/RadioButtonGroup.vue';
import ApiSelect from './components/ApiSelect.vue';
import ApiTree from './components/ApiTree.vue';
import ApiTreeSelect from './components/ApiTreeSelect.vue';
import ApiCascader from './components/ApiCascader.vue';
import { BasicUpload } from '/@/components/Upload';
@@ -43,6 +44,7 @@ componentMap.set('AutoComplete', AutoComplete);
componentMap.set('Select', Select);
componentMap.set('ApiSelect', ApiSelect);
componentMap.set('ApiTree', ApiTree);
componentMap.set('TreeSelect', TreeSelect);
componentMap.set('ApiTreeSelect', ApiTreeSelect);
componentMap.set('ApiRadioGroup', ApiRadioGroup);

View File

@@ -26,7 +26,7 @@
import { get, omit } from 'lodash-es';
import { useRuleFormItem } from '/@/hooks/component/useFormItem';
import { LoadingOutlined } from '@ant-design/icons-vue';
import { useI18n } from '/@/hooks/web/useI18n';
interface Option {
value: string;
label: string;
@@ -76,7 +76,7 @@
const loading = ref<boolean>(false);
const emitData = ref<any[]>([]);
const isFirstLoad = ref(true);
const { t } = useI18n();
// Embedded in the form, just use the hook binding to perform form verification
const [state] = useRuleFormItem(props, 'value', 'change', emitData);
@@ -188,6 +188,7 @@
state,
options,
loading,
t,
handleChange,
loadData,
handleRenderDisplay,

View File

@@ -1,6 +1,6 @@
<template>
<Select
@dropdownVisibleChange="handleFetch"
@dropdown-visible-change="handleFetch"
v-bind="$attrs"
@change="handleChange"
:options="getOptions"
@@ -57,6 +57,7 @@
labelField: propTypes.string.def('label'),
valueField: propTypes.string.def('value'),
immediate: propTypes.bool.def(true),
alwaysLoad: propTypes.bool.def(false),
},
emits: ['options-change', 'change'],
setup(props, { emit }) {
@@ -87,7 +88,7 @@
});
watchEffect(() => {
props.immediate && fetch();
props.immediate && !props.alwaysLoad && fetch();
});
watch(
@@ -121,10 +122,14 @@
}
}
async function handleFetch() {
if (!props.immediate && unref(isFirstLoad)) {
await fetch();
isFirstLoad.value = false;
async function handleFetch(visible) {
if (visible) {
if (props.alwaysLoad) {
await fetch();
} else if (!props.immediate && unref(isFirstLoad)) {
await fetch();
isFirstLoad.value = false;
}
}
}

View File

@@ -0,0 +1,90 @@
<template>
<a-tree 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>
</template>
<script lang="ts">
import { computed, defineComponent, watch, ref, onMounted, unref } from 'vue';
import { Tree } 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: 'ApiTree',
components: { ATree: Tree, LoadingOutlined },
props: {
api: { type: Function as PropType<(arg?: Recordable) => Promise<Recordable>> },
params: { type: Object },
immediate: { type: Boolean, default: true },
resultField: propTypes.string.def(''),
afterFetch: { type: Function as PropType<Fn> },
},
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,
() => {
!unref(isFirstLoaded) && fetch();
},
{ deep: true },
);
watch(
() => props.immediate,
(v) => {
v && !isFirstLoaded.value && fetch();
},
);
onMounted(() => {
props.immediate && fetch();
});
async function fetch() {
const { api, afterFetch } = props;
if (!api || !isFunction(api)) return;
loading.value = true;
treeData.value = [];
let result;
try {
result = await api(props.params);
} catch (e) {
console.error(e);
}
if (afterFetch && isFunction(afterFetch)) {
result = afterFetch(result);
}
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,17 +1,16 @@
<script lang="tsx">
import type { PropType, Ref } from 'vue';
import type { FormActionType, FormProps } from '../types/form';
import type { FormSchema } from '../types/form';
import { computed, defineComponent, toRefs, unref } from 'vue';
import type { FormActionType, FormProps, FormSchema } from '../types/form';
import type { ValidationRule } from 'ant-design-vue/lib/form/Form';
import type { TableActionType } from '/@/components/Table';
import { defineComponent, computed, unref, toRefs } from 'vue';
import { Form, Col, Divider } from 'ant-design-vue';
import { Col, Divider, Form } from 'ant-design-vue';
import { componentMap } from '../componentMap';
import { BasicHelp } from '/@/components/Basic';
import { isBoolean, isFunction, isNull } from '/@/utils/is';
import { getSlot } from '/@/utils/helper/tsxHelper';
import { createPlaceholderMessage, setComponentRuleType } from '../helper';
import { upperFirst, cloneDeep } from 'lodash-es';
import { cloneDeep, upperFirst } from 'lodash-es';
import { useItemLabelWidth } from '../hooks/useLabelWidth';
import { useI18n } from '/@/hooks/web/useI18n';
@@ -178,8 +177,21 @@
const getRequired = isFunction(required) ? required(unref(getValues)) : required;
if ((!rules || rules.length === 0) && getRequired) {
rules = [{ required: getRequired, validator }];
/*
* 1、若设置了required属性又没有其他的rules就创建一个验证规则
* 2、若设置了required属性又存在其他的rules则只rules中不存在required属性时才添加验证required的规则
* 也就是说rules中的required优先级大于required
*/
if (getRequired) {
if (!rules || rules.length === 0) {
rules = [{ required: getRequired, validator }];
} else {
const requiredIndex: number = rules.findIndex((rule) => Reflect.has(rule, 'required'));
if (requiredIndex === -1) {
rules.push({ required: getRequired, validator });
}
}
}
const requiredRuleIndex: number = rules.findIndex(

View File

@@ -70,3 +70,5 @@ export function handleInputNumberValue(component?: ComponentType, val?: any) {
* 时间字段
*/
export const dateItemType = genType();
export const defaultValueComponents = ['Input', 'InputPassword', 'InputSearch', 'InputTextArea'];

View File

@@ -1,10 +1,10 @@
import type { ComputedRef, Ref } from 'vue';
import type { FormProps, FormSchema, FormActionType } from '../types/form';
import type { NamePath } from 'ant-design-vue/lib/form/interface';
import { unref, toRaw } from 'vue';
import { isArray, isFunction, isObject, isString } from '/@/utils/is';
import { unref, toRaw, nextTick } from 'vue';
import { isArray, isFunction, isNullOrUnDef, isObject, isString } from '/@/utils/is';
import { deepMerge } from '/@/utils';
import { dateItemType, handleInputNumberValue } from '../helper';
import { dateItemType, handleInputNumberValue, defaultValueComponents } from '../helper';
import { dateUtil } from '/@/utils/dateUtil';
import { cloneDeep, uniqBy } from 'lodash-es';
import { error } from '/@/utils/log';
@@ -37,9 +37,12 @@ export function useFormEvents({
if (!formEl) return;
Object.keys(formModel).forEach((key) => {
formModel[key] = defaultValueRef.value[key];
const schema = unref(getSchema).find((item) => item.field === key);
const isInput = schema?.component && defaultValueComponents.includes(schema.component);
formModel[key] = isInput ? defaultValueRef.value[key] || '' : defaultValueRef.value[key];
});
clearValidate();
nextTick(() => clearValidate());
emit('reset', toRaw(formModel));
submitOnReset && handleSubmit();
}
@@ -125,18 +128,18 @@ export function useFormEvents({
const schemaList: FormSchema[] = cloneDeep(unref(getSchema));
const index = schemaList.findIndex((schema) => schema.field === prefixField);
const hasInList = schemaList.some((item) => item.field === prefixField || schema.field);
if (!hasInList) return;
if (!prefixField || index === -1 || first) {
first ? schemaList.unshift(schema) : schemaList.push(schema);
schemaRef.value = schemaList;
_setDefaultValue(schema);
return;
}
if (index !== -1) {
schemaList.splice(index + 1, 0, schema);
}
_setDefaultValue(schema);
schemaRef.value = schemaList;
}
@@ -192,9 +195,34 @@ export function useFormEvents({
}
});
});
_setDefaultValue(schema);
schemaRef.value = uniqBy(schema, 'field');
}
function _setDefaultValue(data: FormSchema | FormSchema[]) {
let schemas: FormSchema[] = [];
if (isObject(data)) {
schemas.push(data as FormSchema);
}
if (isArray(data)) {
schemas = [...data];
}
const obj: Recordable = {};
schemas.forEach((item) => {
if (
item.component != 'Divider' &&
Reflect.has(item, 'field') &&
item.field &&
!isNullOrUnDef(item.defaultValue)
) {
obj[item.field] = item.defaultValue;
}
});
setFieldsValue(obj);
}
function getFieldsValue(): Recordable {
const formEl = unref(formElRef);
if (!formEl) return {};

View File

@@ -11,6 +11,43 @@ interface UseFormValuesContext {
getProps: ComputedRef<FormProps>;
formModel: Recordable;
}
/**
* @desription deconstruct array-link key. This method will mutate the target.
*/
function tryDeconstructArray(key: string, value: any, target: Recordable) {
const pattern = /^\[(.+)\]$/;
if (pattern.test(key)) {
const match = key.match(pattern);
if (match && match[1]) {
const keys = match[1].split(',');
value = Array.isArray(value) ? value : [value];
keys.forEach((k, index) => {
set(target, k.trim(), value[index]);
});
return true;
}
}
}
/**
* @desription deconstruct object-link key. This method will mutate the target.
*/
function tryDeconstructObject(key: string, value: any, target: Recordable) {
const pattern = /^\{(.+)\}$/;
if (pattern.test(key)) {
const match = key.match(pattern);
if (match && match[1]) {
const keys = match[1].split(',');
value = isObject(value) ? value : {};
keys.forEach((k) => {
set(target, k.trim(), value[k.trim()]);
});
return true;
}
}
}
export function useFormValues({
defaultValueRef,
getSchema,
@@ -33,14 +70,18 @@ export function useFormValues({
if (isObject(value)) {
value = transformDateFunc?.(value);
}
if (isArray(value) && value[0]?._isAMomentObject && value[1]?._isAMomentObject) {
if (isArray(value) && value[0]?.format && value[1]?.format) {
value = value.map((item) => transformDateFunc?.(item));
}
// Remove spaces
if (isString(value)) {
value = value.trim();
}
set(res, key, value);
if (!tryDeconstructArray(key, value, res) && !tryDeconstructObject(key, value, res)) {
// 没有解构成功的,按原样赋值
set(res, key, value);
}
}
return handleRangeTimeValue(res);
}
@@ -77,7 +118,10 @@ export function useFormValues({
const { defaultValue } = item;
if (!isNullOrUnDef(defaultValue)) {
obj[item.field] = defaultValue;
formModel[item.field] = defaultValue;
if (formModel[item.field] === undefined) {
formModel[item.field] = defaultValue;
}
}
});
defaultValueRef.value = obj;

View File

@@ -1,7 +1,6 @@
import type { Ref } from 'vue';
import type { FormProps, FormSchema } from '../types/form';
import { computed, unref } from 'vue';
import type { FormProps, FormSchema } from '../types/form';
import { isNumber } from '/@/utils/is';
export function useItemLabelWidth(schemaItemRef: Ref<FormSchema>, propsRef: Ref<FormProps>) {
@@ -14,6 +13,7 @@ export function useItemLabelWidth(schemaItemRef: Ref<FormSchema>, propsRef: Ref<
labelWidth: globalLabelWidth,
labelCol: globalLabelCol,
wrapperCol: globWrapperCol,
layout,
} = unref(propsRef);
// If labelWidth is set globally, all items setting
@@ -33,7 +33,10 @@ export function useItemLabelWidth(schemaItemRef: Ref<FormSchema>, propsRef: Ref<
return {
labelCol: { style: { width }, ...col },
wrapperCol: { style: { width: `calc(100% - ${width})` }, ...wrapCol },
wrapperCol: {
style: { width: layout === 'vertical' ? '100%' : `calc(100% - ${width})` },
...wrapCol,
},
};
});
}

View File

@@ -40,6 +40,7 @@ export const basicProps = {
// 在INPUT组件上单击回车时是否自动提交
autoSubmitOnEnter: propTypes.bool.def(false),
submitOnReset: propTypes.bool,
submitOnChange: propTypes.bool,
size: propTypes.oneOf(['default', 'small', 'large']).def('default'),
// 禁用表单
disabled: propTypes.bool,
@@ -53,7 +54,7 @@ export const basicProps = {
transformDateFunc: {
type: Function as PropType<Fn>,
default: (date: any) => {
return date._isAMomentObject ? date?.format('YYYY-MM-DD HH:mm:ss') : date;
return date?.format?.('YYYY-MM-DD HH:mm:ss') ?? date;
},
},
rulesMessageJoinLabel: propTypes.bool.def(true),

View File

@@ -49,17 +49,20 @@ export type RegisterFn = (formInstance: FormActionType) => void;
export type UseFormReturnType = [RegisterFn, FormActionType];
export interface FormProps {
name?: string;
layout?: 'vertical' | 'inline' | 'horizontal';
// Form value
model?: Recordable;
// The width of all items in the entire form
labelWidth?: number | string;
//alignment
// alignment
labelAlign?: 'left' | 'right';
//Row configuration for the entire form
// Row configuration for the entire form
rowProps?: RowProps;
// Submit form on reset
submitOnReset?: boolean;
// Submit form on form changing
submitOnChange?: boolean;
// Col configuration for the entire form
labelCol?: Partial<ColEx>;
// Col configuration for the entire form

View File

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

View File

@@ -31,18 +31,7 @@
v-for="icon in getPaginationList"
:key="icon"
:class="currentSelect === icon ? 'border border-primary' : ''"
class="
p-2
w-1/8
cursor-pointer
mr-1
mt-1
flex
justify-center
items-center
border border-solid
hover:border-primary
"
class="p-2 w-1/8 cursor-pointer mr-1 mt-1 flex justify-center items-center border border-solid hover:border-primary"
@click="handleClick(icon)"
:title="icon"
>

View File

@@ -4,7 +4,7 @@
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { computed, defineProps } from 'vue';
import showdown from 'showdown';
const converter = new showdown.Converter();

View File

@@ -6,7 +6,7 @@
:openKeys="getOpenKeys"
:inlineIndent="inlineIndent"
:theme="theme"
@openChange="handleOpenChange"
@open-change="handleOpenChange"
:class="getMenuClass"
@click="handleMenuClick"
:subMenuOpenDelay="0.2"

View File

@@ -10,7 +10,7 @@ export default defineComponent({
inheritAttrs: false,
props: basicProps,
emits: ['cancel'],
setup(props, { slots }) {
setup(props, { slots, emit }) {
const { visible, draggable, destroyOnClose } = toRefs(props);
const attrs = useAttrs();
useModalDragMove({
@@ -19,8 +19,12 @@ export default defineComponent({
draggable,
});
const onCancel = (e: Event) => {
emit('cancel', e);
};
return () => {
const propsData = { ...unref(attrs), ...props } as Recordable;
const propsData = { ...unref(attrs), ...props, onCancel } as Recordable;
return <Modal {...propsData}>{extendSlots(slots)}</Modal>;
};
},

View File

@@ -111,16 +111,19 @@
.ant-modal-confirm .ant-modal-body {
padding: 24px !important;
}
@media screen and (max-height: 600px) {
.ant-modal {
top: 60px;
}
}
@media screen and (max-height: 540px) {
.ant-modal {
top: 30px;
}
}
@media screen and (max-height: 480px) {
.ant-modal {
top: 10px;

View File

@@ -5,7 +5,7 @@
:title="title"
v-bind="omit($attrs, 'class')"
ref="headerRef"
v-if="content || $slots.headerContent || title || getHeaderSlots.length"
v-if="getShowHeader"
>
<template #default>
<template v-if="content">
@@ -99,6 +99,10 @@
];
});
const getShowHeader = computed(
() => props.content || slots?.headerContent || props.title || getHeaderSlots.value.length,
);
const getShowFooter = computed(() => slots?.leftFooter || slots?.rightFooter);
const getHeaderSlots = computed(() => {
@@ -150,6 +154,7 @@
getClass,
getHeaderSlots,
prefixCls,
getShowHeader,
getShowFooter,
omit,
getContentClass,

View File

@@ -21,7 +21,7 @@
:overlayClassName="`${prefixCls}-menu-popover`"
v-else
:visible="getIsOpend"
@visibleChange="handleVisibleChange"
@visible-change="handleVisibleChange"
:overlayStyle="getOverlayStyle"
:align="{ offset: [0, 0] }"
>

View File

@@ -13,8 +13,8 @@
bottom: 0;
display: block;
width: 2px;
background-color: @primary-color;
content: '';
background-color: @primary-color;
}
}
@@ -45,8 +45,8 @@
position: absolute;
top: 50%;
right: 18px;
transform: translateY(-50%) rotate(-90deg);
transition: transform @transition-time @ease-in-out;
transform: translateY(-50%) rotate(-90deg);
}
}
@@ -128,12 +128,12 @@
position: relative;
z-index: 1;
display: flex;
align-items: center;
font-size: @font-size-base;
color: inherit;
list-style: none;
cursor: pointer;
outline: none;
align-items: center;
&:hover,
&:active {
@@ -178,8 +178,8 @@
&-vertical &-submenu-collapse {
.@{submenu-popup-prefix-cls} {
display: flex;
justify-content: center;
align-items: center;
justify-content: center;
}
.@{menu-prefix-cls}-submenu-collapsed-show-tit {
flex-direction: column;
@@ -244,8 +244,8 @@
left: 0;
width: 3px;
height: 100%;
background-color: @primary-color;
content: '';
background-color: @primary-color;
}
}
}
@@ -276,8 +276,8 @@
left: 0;
width: 3px;
height: 100%;
background-color: @primary-color;
content: '';
background-color: @primary-color;
}
.@{menu-prefix-cls}-submenu-collapse {

View File

@@ -2,6 +2,7 @@ export { default as BasicTable } from './src/BasicTable.vue';
export { default as TableAction } from './src/components/TableAction.vue';
export { default as EditTableHeaderIcon } from './src/components/EditTableHeaderIcon.vue';
export { default as TableImg } from './src/components/TableImg.vue';
export * from './src/types/table';
export * from './src/types/pagination';
export * from './src/types/tableAction';

View File

@@ -1,6 +1,7 @@
<template>
<div ref="wrapRef" :class="getWrapperClass">
<BasicForm
ref="formRef"
submitOnReset
v-bind="getFormProps"
v-if="getBindValues.useSearchForm"
@@ -24,10 +25,12 @@
<template #[item]="data" v-for="item in Object.keys($slots)" :key="item">
<slot :name="item" v-bind="data || {}"></slot>
</template>
<template #[`header-${column.dataIndex}`] v-for="column in columns" :key="column.dataIndex">
<template #headerCell="{ column }">
<HeaderCell :column="column" />
</template>
<!-- <template #[`header-${column.dataIndex}`] v-for="(column, index) in columns" :key="index">-->
<!-- <HeaderCell :column="column" />-->
<!-- </template>-->
</Table>
</div>
</template>
@@ -43,7 +46,6 @@
import { Table } from 'ant-design-vue';
import { BasicForm, useForm } from '/@/components/Form/index';
import { PageWrapperFixedHeightKey } from '/@/components/Page';
import expandIcon from './components/ExpandIcon';
import HeaderCell from './components/HeaderCell.vue';
import { InnerHandlers } from './types/table';
@@ -53,6 +55,7 @@
import { useLoading } from './hooks/useLoading';
import { useRowSelection } from './hooks/useRowSelection';
import { useTableScroll } from './hooks/useTableScroll';
import { useTableScrollTo } from './hooks/useScrollTo';
import { useCustomRow } from './hooks/useCustomRow';
import { useTableStyle } from './hooks/useTableStyle';
import { useTableHeader } from './hooks/useTableHeader';
@@ -97,6 +100,7 @@
const tableData = ref<Recordable[]>([]);
const wrapRef = ref(null);
const formRef = ref(null);
const innerPropsRef = ref<Partial<BasicTableProps>>();
const { prefixCls } = useDesign('basic-table');
@@ -185,8 +189,12 @@
getColumnsRef,
getRowSelectionRef,
getDataSourceRef,
wrapRef,
formRef,
);
const { scrollTo } = useTableScrollTo(tableElRef, getDataSourceRef);
const { customRow } = useCustomRow(getProps, {
setSelectedRowKeys,
getSelectRowKeys,
@@ -197,7 +205,11 @@
const { getRowClassName } = useTableStyle(getProps, prefixCls);
const { getExpandOption, expandAll, collapseAll } = useTableExpand(getProps, tableData, emit);
const { getExpandOption, expandAll, expandRows, collapseAll } = useTableExpand(
getProps,
tableData,
emit,
);
const handlers: InnerHandlers = {
onColumnsChange: (data: ColumnChangeParam[]) => {
@@ -222,10 +234,8 @@
const getBindValues = computed(() => {
const dataSource = unref(getDataSourceRef);
let propsData: Recordable = {
// ...(dataSource.length === 0 ? { getPopupContainer: () => document.body } : {}),
...attrs,
customRow,
expandIcon: slots.expandIcon ? null : expandIcon(),
...unref(getProps),
...unref(getHeaderProps),
scroll: unref(getScrollRef),
@@ -300,7 +310,9 @@
getShowPagination,
setCacheColumnsByField,
expandAll,
expandRows,
collapseAll,
scrollTo,
getSize: () => {
return unref(getBindValues).size as SizeType;
},
@@ -312,6 +324,7 @@
emit('register', tableAction, formActions);
return {
formRef,
tableElRef,
getBindValues,
getLoading,
@@ -346,6 +359,7 @@
.@{prefix-cls} {
max-width: 100%;
height: 100%;
&-row__striped {
td {

View File

@@ -7,6 +7,7 @@ import {
Switch,
DatePicker,
TimePicker,
AutoComplete,
} from 'ant-design-vue';
import type { ComponentType } from './types/componentType';
import { ApiSelect, ApiTreeSelect } from '/@/components/Form';
@@ -17,6 +18,7 @@ componentMap.set('Input', Input);
componentMap.set('InputNumber', InputNumber);
componentMap.set('Select', Select);
componentMap.set('ApiSelect', ApiSelect);
componentMap.set('AutoComplete', AutoComplete);
componentMap.set('ApiTreeSelect', ApiTreeSelect);
componentMap.set('Switch', Switch);
componentMap.set('Checkbox', Checkbox);

View File

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

View File

@@ -29,7 +29,7 @@
const { prefixCls } = useDesign('basic-table-header-cell');
const getIsEdit = computed(() => !!props.column?.edit);
const getTitle = computed(() => props.column?.customTitle);
const getTitle = computed(() => props.column?.customTitle || props.column?.title);
const getHelpMessage = computed(() => props.column?.helpMessage);
return { prefixCls, getIsEdit, getTitle, getHelpMessage };

View File

@@ -104,21 +104,20 @@
});
const getDropdownList = computed((): any[] => {
return (toRaw(props.dropDownActions) || [])
.filter((action) => {
return hasPermission(action.auth) && isIfShow(action);
})
.map((action, index) => {
const { label, popConfirm } = action;
return {
...action,
...popConfirm,
onConfirm: popConfirm?.confirm,
onCancel: popConfirm?.cancel,
text: label,
divider: index < props.dropDownActions.length - 1 ? props.divider : false,
};
});
const list = (toRaw(props.dropDownActions) || []).filter((action) => {
return hasPermission(action.auth) && isIfShow(action);
});
return list.map((action, index) => {
const { label, popConfirm } = action;
return {
...action,
...popConfirm,
onConfirm: popConfirm?.confirm,
onCancel: popConfirm?.cancel,
text: label,
divider: index < list.length - 1 ? props.divider : false,
};
});
});
const getAlign = computed(() => {

View File

@@ -1,40 +1,4 @@
<template>
<div :class="prefixCls">
<div
v-show="!isEdit"
:class="{ [`${prefixCls}__normal`]: true, 'ellipsis-cell': column.ellipsis }"
@click="handleEdit"
>
<div class="cell-content" :title="column.ellipsis ? getValues ?? '' : ''">
{{ getValues ? getValues : '&nbsp;' }}
</div>
<FormOutlined :class="`${prefixCls}__normal-icon`" v-if="!column.editRow" />
</div>
<a-spin v-if="isEdit" :spinning="spinning">
<div :class="`${prefixCls}__wrapper`" v-click-outside="onClickOutside">
<CellComponent
v-bind="getComponentProps"
:component="getComponent"
:style="getWrapperStyle"
:popoverVisible="getRuleVisible"
:rule="getRule"
:ruleMessage="ruleMessage"
:class="getWrapperClass"
ref="elRef"
@change="handleChange"
@options-change="handleOptionsChange"
@pressEnter="handleEnter"
/>
<div :class="`${prefixCls}__action`" v-if="!getRowEditable">
<CheckOutlined :class="[`${prefixCls}__icon`, 'mx-2']" @click="handleSubmitClick" />
<CloseOutlined :class="`${prefixCls}__icon `" @click="handleCancel" />
</div>
</div>
</a-spin>
</div>
</template>
<script lang="ts">
<script lang="tsx">
import type { CSSProperties, PropType } from 'vue';
import { computed, defineComponent, nextTick, ref, toRaw, unref, watchEffect } from 'vue';
import type { BasicColumn } from '../../types/table';
@@ -56,7 +20,7 @@
export default defineComponent({
name: 'EditableCell',
components: { FormOutlined, CloseOutlined, CheckOutlined, CellComponent, ASpin: Spin },
components: { FormOutlined, CloseOutlined, CheckOutlined, CellComponent, Spin },
directives: {
clickOutside,
},
@@ -100,13 +64,6 @@
});
const getComponentProps = computed(() => {
const compProps = props.column?.editComponentProps ?? {};
const component = unref(getComponent);
const apiSelectProps: Recordable = {};
if (component === 'ApiSelect') {
apiSelectProps.cache = true;
}
const isCheckValue = unref(getIsCheckComp);
const valueField = isCheckValue ? 'checked' : 'value';
@@ -114,19 +71,30 @@
const value = isCheckValue ? (isNumber(val) && isBoolean(val) ? val : !!val) : val;
let compProps = props.column?.editComponentProps ?? {};
const { record, column, index } = props;
if (isFunction(compProps)) {
compProps = compProps({ text: val, record, column, index }) ?? {};
}
const component = unref(getComponent);
const apiSelectProps: Recordable = {};
if (component === 'ApiSelect') {
apiSelectProps.cache = true;
}
return {
size: 'small',
getPopupContainer: () => unref(table?.wrapRef.value) ?? document.body,
getCalendarContainer: () => unref(table?.wrapRef.value) ?? document.body,
placeholder: createPlaceholderMessage(unref(getComponent)),
...apiSelectProps,
...omit(compProps, 'onChange'),
[valueField]: value,
};
} as any;
});
const getValues = computed(() => {
const { editComponentProps, editValueMap } = props.column;
const { editValueMap } = props.column;
const value = unref(currentValueRef);
@@ -139,7 +107,8 @@
return value;
}
const options: LabelValueOptions = editComponentProps?.options ?? (unref(optionsRef) || []);
const options: LabelValueOptions =
unref(getComponentProps)?.options ?? (unref(optionsRef) || []);
const option = options.find((item) => `${item.value}` === `${value}`);
return option?.label ?? value;
@@ -190,14 +159,16 @@
const component = unref(getComponent);
if (!e) {
currentValueRef.value = e;
} else if (e?.target && Reflect.has(e.target, 'value')) {
currentValueRef.value = (e as ChangeEvent).target.value;
} else if (component === 'Checkbox') {
currentValueRef.value = (e as ChangeEvent).target.checked;
} else if (component === 'Switch') {
currentValueRef.value = e;
} else if (e?.target && Reflect.has(e.target, 'value')) {
currentValueRef.value = (e as ChangeEvent).target.value;
} else if (isString(e) || isBoolean(e) || isNumber(e)) {
currentValueRef.value = e;
}
const onChange = props.column?.editComponentProps?.onChange;
const onChange = unref(getComponentProps)?.onChange;
if (onChange && isFunction(onChange)) onChange(...arguments);
table.emit?.('edit-change', {
@@ -265,7 +236,7 @@
result = await beforeEditSubmit({
record: pick(record, keys),
index,
key: key as string,
key: dataKey as string,
value,
});
} catch (e) {
@@ -281,7 +252,7 @@
set(record, dataKey, value);
//const record = await table.updateTableData(index, dataKey, value);
needEmit && table.emit?.('edit-end', { record, index, key, value });
needEmit && table.emit?.('edit-end', { record, index, key: dataKey, value });
isEdit.value = false;
}
@@ -322,7 +293,7 @@
// only ApiSelect or TreeSelect
function handleOptionsChange(options: LabelValueOptions) {
const { replaceFields } = props.column?.editComponentProps ?? {};
const { replaceFields } = unref(getComponentProps);
const component = unref(getComponent);
if (component === 'ApiTreeSelect') {
const { title = 'title', value = 'value', children = 'children' } = replaceFields || {};
@@ -355,7 +326,7 @@
if (props.column.dataIndex) {
if (!props.record.editValueRefs) props.record.editValueRefs = {};
props.record.editValueRefs[props.column.dataIndex] = currentValueRef;
props.record.editValueRefs[props.column.dataIndex as any] = currentValueRef;
}
/* eslint-disable */
props.record.onCancelEdit = () => {
@@ -398,6 +369,59 @@
spinning,
};
},
render() {
return (
<div class={this.prefixCls}>
<div
v-show={!this.isEdit}
class={{ [`${this.prefixCls}__normal`]: true, 'ellipsis-cell': this.column.ellipsis }}
onClick={this.handleEdit}
>
<div class="cell-content" title={this.column.ellipsis ? this.getValues ?? '' : ''}>
{this.column.editRender
? this.column.editRender({
text: this.value,
record: this.record as Recordable,
column: this.column,
index: this.index,
})
: this.getValues
? this.getValues
: '\u00A0'}
</div>
{!this.column.editRow && <FormOutlined class={`${this.prefixCls}__normal-icon`} />}
</div>
{this.isEdit && (
<Spin spinning={this.spinning}>
<div class={`${this.prefixCls}__wrapper`} v-click-outside={this.onClickOutside}>
<CellComponent
{...this.getComponentProps}
component={this.getComponent}
style={this.getWrapperStyle}
popoverVisible={this.getRuleVisible}
rule={this.getRule}
ruleMessage={this.ruleMessage}
class={this.getWrapperClass}
ref="elRef"
onChange={this.handleChange}
onOptionsChange={this.handleOptionsChange}
onPressEnter={this.handleEnter}
/>
{!this.getRowEditable && (
<div class={`${this.prefixCls}__action`}>
<CheckOutlined
class={[`${this.prefixCls}__icon`, 'mx-2']}
onClick={this.handleSubmitClick}
/>
<CloseOutlined class={`${this.prefixCls}__icon `} onClick={this.handleCancel} />
</div>
)}
</div>
</Spin>
)}
</div>
);
},
});
</script>
<style lang="less">

View File

@@ -7,7 +7,7 @@ const { t } = useI18n();
* @description: 生成placeholder
*/
export function createPlaceholderMessage(component: ComponentType) {
if (component.includes('Input')) {
if (component.includes('Input') || component.includes('AutoComplete')) {
return t('common.inputText');
}
if (component.includes('Picker')) {

View File

@@ -6,7 +6,7 @@
<Popover
placement="bottomLeft"
trigger="click"
@visibleChange="handleVisibleChange"
@visible-change="handleVisibleChange"
:overlayClassName="`${prefixCls}__cloumn-list`"
:getPopupContainer="getPopupContainer"
>
@@ -43,7 +43,7 @@
<CheckboxGroup v-model:value="checkedList" @change="onChange" ref="columnListRef">
<template v-for="item in plainOptions" :key="item.value">
<div :class="`${prefixCls}__check-item`" v-if="!('ifShow' in item && !item.ifShow)">
<DragOutlined class="table-coulmn-drag-icon" />
<DragOutlined class="table-column-drag-icon" />
<Checkbox :value="item.value">
{{ item.label }}
</Checkbox>
@@ -117,13 +117,16 @@
import { useI18n } from '/@/hooks/web/useI18n';
import { useTableContext } from '../../hooks/useTableContext';
import { useDesign } from '/@/hooks/web/useDesign';
import { useSortable } from '/@/hooks/web/useSortable';
// import { useSortable } from '/@/hooks/web/useSortable';
import { isFunction, isNullAndUnDef } from '/@/utils/is';
import { getPopupContainer as getParentContainer } from '/@/utils';
import { omit } from 'lodash-es';
import { cloneDeep, omit } from 'lodash-es';
import Sortablejs from 'sortablejs';
import type Sortable from 'sortablejs';
interface State {
checkAll: boolean;
isInit?: boolean;
checkedList: string[];
defaultCheckList: string[];
}
@@ -157,7 +160,7 @@
let inited = false;
const cachePlainOptions = ref<Options[]>([]);
const plainOptions = ref<Options[]>([]);
const plainOptions = ref<Options[] | any>([]);
const plainSortOptions = ref<Options[]>([]);
@@ -180,7 +183,7 @@
watchEffect(() => {
const columns = table.getColumns();
if (columns.length) {
if (columns.length && !state.isInit) {
init();
}
});
@@ -233,6 +236,7 @@
}
});
}
state.isInit = true;
state.checkedList = checkList;
}
@@ -250,16 +254,15 @@
const indeterminate = computed(() => {
const len = plainOptions.value.length;
let checkdedLen = state.checkedList.length;
unref(checkIndex) && checkdedLen--;
return checkdedLen > 0 && checkdedLen < len;
let checkedLen = state.checkedList.length;
unref(checkIndex) && checkedLen--;
return checkedLen > 0 && checkedLen < len;
});
// Trigger when check/uncheck a column
function onChange(checkedList: string[]) {
const len = plainOptions.value.length;
const len = plainSortOptions.value.length;
state.checkAll = checkedList.length === len;
const sortList = unref(plainSortOptions).map((item) => item.value);
checkedList.sort((prev, next) => {
return sortList.indexOf(prev) - sortList.indexOf(next);
@@ -267,6 +270,8 @@
setColumns(checkedList);
}
let sortable: Sortable;
let sortableOrder: string[] = [];
// reset columns
function reset() {
state.checkedList = [...state.defaultCheckList];
@@ -274,6 +279,7 @@
plainOptions.value = unref(cachePlainOptions);
plainSortOptions.value = unref(cachePlainOptions);
setColumns(table.getCacheColumns());
sortable.sort(sortableOrder);
}
// Open the pop-up window for drag and drop initialization
@@ -285,15 +291,18 @@
const el = columnListEl.$el as any;
if (!el) return;
// Drag and drop sort
const { initSortable } = useSortable(el, {
handle: '.table-coulmn-drag-icon ',
sortable = Sortablejs.create(unref(el), {
animation: 500,
delay: 400,
delayOnTouchOnly: true,
handle: '.table-column-drag-icon ',
onEnd: (evt) => {
const { oldIndex, newIndex } = evt;
if (isNullAndUnDef(oldIndex) || isNullAndUnDef(newIndex) || oldIndex === newIndex) {
return;
}
// Sort column
const columns = getColumns();
const columns = cloneDeep(plainSortOptions.value);
if (oldIndex > newIndex) {
columns.splice(newIndex, 0, columns[oldIndex]);
@@ -304,11 +313,11 @@
}
plainSortOptions.value = columns;
plainOptions.value = columns;
setColumns(columns);
},
});
initSortable();
// 记录原始order 序列
sortableOrder = sortable.toArray();
inited = true;
});
}
@@ -341,13 +350,13 @@
if (isFixed && !item.width) {
item.width = 100;
}
table.setCacheColumnsByField?.(item.dataIndex, { fixed: isFixed });
table.setCacheColumnsByField?.(item.dataIndex as string, { fixed: isFixed });
setColumns(columns);
}
function setColumns(columns: BasicColumn[] | string[]) {
table.setColumns(columns);
const data: ColumnChangeParam[] = unref(plainOptions).map((col) => {
const data: ColumnChangeParam[] = unref(plainSortOptions).map((col) => {
const visible =
columns.findIndex(
(c: BasicColumn | string) =>
@@ -390,7 +399,7 @@
<style lang="less">
@prefix-cls: ~'@{namespace}-basic-column-setting';
.table-coulmn-drag-icon {
.table-column-drag-icon {
margin: 0 5px;
cursor: move;
}

View File

@@ -4,7 +4,7 @@
<span>{{ t('component.table.settingDens') }}</span>
</template>
<Dropdown placement="bottomCenter" :trigger="['click']" :getPopupContainer="getPopupContainer">
<Dropdown placement="bottom" :trigger="['click']" :getPopupContainer="getPopupContainer">
<ColumnHeightOutlined />
<template #overlay>
<Menu @click="handleTitleClick" selectable v-model:selectedKeys="selectedKeysRef">

View File

@@ -152,10 +152,10 @@ export function useColumns(
return hasPermission(column.auth) && isIfShow(column);
})
.map((column) => {
const { slots, dataIndex, customRender, format, edit, editRow, flag } = column;
const { slots, customRender, format, edit, editRow, flag } = column;
if (!slots || !slots?.title) {
column.slots = { title: `header-${dataIndex}`, ...(slots || {}) };
// column.slots = { title: `header-${dataIndex}`, ...(slots || {}) };
column.customTitle = column.title;
Reflect.deleteProperty(column, 'title');
}
@@ -197,7 +197,7 @@ export function useColumns(
* set columns
* @param columnList keycolumn
*/
function setColumns(columnList: Partial<BasicColumn>[] | string[]) {
function setColumns(columnList: Partial<BasicColumn>[] | (string | string[])[]) {
const columns = cloneDeep(columnList);
if (!isArray(columns)) return;
@@ -210,31 +210,23 @@ export function useColumns(
const cacheKeys = cacheColumns.map((item) => item.dataIndex);
if (!isString(firstColumn)) {
if (!isString(firstColumn) && !isArray(firstColumn)) {
columnsRef.value = columns as BasicColumn[];
} else {
const columnKeys = columns as string[];
const columnKeys = (columns as (string | string[])[]).map((m) => m.toString());
const newColumns: BasicColumn[] = [];
cacheColumns.forEach((item) => {
if (columnKeys.includes(item.dataIndex! || (item.key as string))) {
newColumns.push({
...item,
defaultHidden: false,
});
} else {
newColumns.push({
...item,
defaultHidden: true,
});
}
newColumns.push({
...item,
defaultHidden: !columnKeys.includes(item.dataIndex?.toString() || (item.key as string)),
});
});
// Sort according to another array
if (!isEqual(cacheKeys, columns)) {
newColumns.sort((prev, next) => {
return (
cacheKeys.indexOf(prev.dataIndex as string) -
cacheKeys.indexOf(next.dataIndex as string)
columnKeys.indexOf(prev.dataIndex?.toString() as string) -
columnKeys.indexOf(next.dataIndex?.toString() as string)
);
});
}

View File

@@ -14,7 +14,7 @@ import {
import { useTimeoutFn } from '/@/hooks/core/useTimeout';
import { buildUUID } from '/@/utils/uuid';
import { isFunction, isBoolean } from '/@/utils/is';
import { get, cloneDeep } from 'lodash-es';
import { get, cloneDeep, merge } from 'lodash-es';
import { FETCH_SETTING, ROW_KEY, PAGE_SIZE } from '../const';
interface ActionType {
@@ -196,11 +196,10 @@ export function useDataSource(
}
function insertTableDataRecord(record: Recordable, index: number): Recordable | undefined {
if (!dataSourceRef.value || dataSourceRef.value.length == 0) return;
// if (!dataSourceRef.value || dataSourceRef.value.length == 0) return;
index = index ?? dataSourceRef.value?.length;
unref(dataSourceRef).splice(index, 0, record);
unref(propsRef).dataSource?.splice(index, 0, record);
return unref(propsRef).dataSource;
return unref(dataSourceRef);
}
function findTableDataRecord(rowKey: string | number) {
@@ -272,17 +271,17 @@ export function useDataSource(
const { sortInfo = {}, filterInfo } = searchState;
let params: Recordable = {
...pageParams,
...(useSearchForm ? getFieldsValue() : {}),
...searchInfo,
...(opt?.searchInfo ?? {}),
...defSort,
...sortInfo,
...filterInfo,
...(opt?.sortInfo ?? {}),
...(opt?.filterInfo ?? {}),
};
let params: Recordable = merge(
pageParams,
useSearchForm ? getFieldsValue() : {},
searchInfo,
opt?.searchInfo ?? {},
defSort,
sortInfo,
filterInfo,
opt?.sortInfo ?? {},
opt?.filterInfo ?? {},
);
if (beforeFetch && isFunction(beforeFetch)) {
params = (await beforeFetch(params)) || params;
}
@@ -293,7 +292,7 @@ export function useDataSource(
const isArrayResult = Array.isArray(res);
let resultItems: Recordable[] = isArrayResult ? res : get(res, listField);
const resultTotal: number = isArrayResult ? 0 : get(res, totalField);
const resultTotal: number = isArrayResult ? res.length : get(res, totalField);
// 假如数据变少导致总页数变少并小于当前选中页码通过getPaginationRef获取到的页码是不正确的需获取正确的页码再次执行
if (resultTotal) {

View File

@@ -21,11 +21,8 @@ export function useRowSelection(
return {
selectedRowKeys: unref(selectedRowKeysRef),
hideDefaultSelections: false,
onChange: (selectedRowKeys: string[]) => {
setSelectedRowKeys(selectedRowKeys);
// selectedRowKeysRef.value = selectedRowKeys;
// selectedRowRef.value = selectedRows;
},
...omit(rowSelection, ['onChange']),
};

View File

@@ -0,0 +1,55 @@
import type { ComputedRef, Ref } from 'vue';
import { nextTick, unref } from 'vue';
import { warn } from '/@/utils/log';
export function useTableScrollTo(
tableElRef: Ref<ComponentRef>,
getDataSourceRef: ComputedRef<Recordable[]>,
) {
let bodyEl: HTMLElement | null;
async function findTargetRowToScroll(targetRowData: Recordable) {
const { id } = targetRowData;
const targetRowEl: HTMLElement | null | undefined = bodyEl?.querySelector(
`[data-row-key="${id}"]`,
);
//Add a delay to get new dataSource
await nextTick();
bodyEl?.scrollTo({
top: targetRowEl?.offsetTop ?? 0,
behavior: 'smooth',
});
}
function scrollTo(pos: string): void {
const table = unref(tableElRef);
if (!table) return;
const tableEl: Element = table.$el;
if (!tableEl) return;
if (!bodyEl) {
bodyEl = tableEl.querySelector('.ant-table-body');
if (!bodyEl) return;
}
const dataSource = unref(getDataSourceRef);
if (!dataSource) return;
// judge pos type
if (pos === 'top') {
findTargetRowToScroll(dataSource[0]);
} else if (pos === 'bottom') {
findTargetRowToScroll(dataSource[dataSource.length - 1]);
} else {
const targetRowData = dataSource.find((data) => data.id === pos);
if (targetRowData) {
findTargetRowToScroll(targetRowData);
} else {
warn(`id: ${pos} doesn't exist`);
}
}
}
return { scrollTo };
}

View File

@@ -152,9 +152,15 @@ export function useTable(tableProps?: Props): [
expandAll: () => {
getTableInstance().expandAll();
},
expandRows: (keys: string[]) => {
getTableInstance().expandRows(keys);
},
collapseAll: () => {
getTableInstance().collapseAll();
},
scrollTo: (pos: string) => {
getTableInstance().scrollTo(pos);
},
};
return [register, methods];

View File

@@ -37,6 +37,13 @@ export function useTableExpand(
expandedRowKeys.value = keys;
}
function expandRows(keys: string[]) {
// use row ID expands the specified table row
const { isTreeTable } = unref(propsRef);
if (!isTreeTable) return;
expandedRowKeys.value = [...expandedRowKeys.value, ...keys];
}
function getAllKeys(data?: Recordable[]) {
const keys: string[] = [];
const { childrenColumnName } = unref(propsRef);
@@ -54,5 +61,5 @@ export function useTableExpand(
expandedRowKeys.value = [];
}
return { getExpandOption, expandAll, collapseAll };
return { getExpandOption, expandAll, expandRows, collapseAll };
}

View File

@@ -8,7 +8,7 @@ export function useTableFooter(
propsRef: ComputedRef<BasicTableProps>,
scrollRef: ComputedRef<{
x: string | number | true;
y: Nullable<number>;
y: string | number | null;
scrollToFirstRowOnChange: boolean;
}>,
tableElRef: Ref<ComponentRef>,

View File

@@ -1,6 +1,6 @@
import type { BasicTableProps, TableRowSelection, BasicColumn } from '../types/table';
import type { Ref, ComputedRef } from 'vue';
import { computed, unref, ref, nextTick, watch } from 'vue';
import { Ref, ComputedRef, ref } from 'vue';
import { computed, unref, nextTick, watch } from 'vue';
import { getViewportOffset } from '/@/utils/domUtils';
import { isBoolean } from '/@/utils/is';
import { useWindowSizeFn } from '/@/hooks/event/useWindowSizeFn';
@@ -12,11 +12,12 @@ export function useTableScroll(
propsRef: ComputedRef<BasicTableProps>,
tableElRef: Ref<ComponentRef>,
columnsRef: ComputedRef<BasicColumn[]>,
rowSelectionRef: ComputedRef<TableRowSelection<any> | null>,
rowSelectionRef: ComputedRef<TableRowSelection | null>,
getDataSourceRef: ComputedRef<Recordable[]>,
wrapRef: Ref<HTMLElement | null>,
formRef: Ref<ComponentRef>,
) {
const tableHeightRef: Ref<Nullable<number>> = ref(null);
const tableHeightRef: Ref<Nullable<number | string>> = ref(167);
const modalFn = useModalContext();
// Greater than animation time 280
@@ -43,8 +44,8 @@ export function useTableScroll(
});
}
function setHeight(heigh: number) {
tableHeightRef.value = heigh;
function setHeight(height: number) {
tableHeightRef.value = height;
// Solve the problem of modal adaptive height calculation when the form is placed in the modal
modalFn?.redoModalHeight?.();
}
@@ -55,7 +56,8 @@ export function useTableScroll(
let bodyEl: HTMLElement | null;
async function calcTableHeight() {
const { resizeHeightOffset, pagination, maxHeight } = unref(propsRef);
const { resizeHeightOffset, pagination, maxHeight, isCanResizeParent, useSearchForm } =
unref(propsRef);
const tableData = unref(getDataSourceRef);
const table = unref(tableElRef);
@@ -91,17 +93,14 @@ export function useTableScroll(
if (!unref(getCanResize) || tableData.length === 0) return;
await nextTick();
//Add a delay to get the correct bottomIncludeBody paginationHeight footerHeight headerHeight
// Add a delay to get the correct bottomIncludeBody paginationHeight footerHeight headerHeight
const headEl = tableEl.querySelector('.ant-table-thead ');
if (!headEl) return;
// Table height from bottom
const { bottomIncludeBody } = getViewportOffset(headEl);
// Table height from bottom height-custom offset
const paddingHeight = 32;
let paddingHeight = 32;
// Pager height
let paginationHeight = 2;
if (!isBoolean(pagination)) {
@@ -132,6 +131,35 @@ export function useTableScroll(
headerHeight = (headEl as HTMLElement).offsetHeight;
}
let bottomIncludeBody = 0;
if (unref(wrapRef) && isCanResizeParent) {
const tablePadding = 12;
const formMargin = 16;
let paginationMargin = 10;
const wrapHeight = unref(wrapRef)?.offsetHeight ?? 0;
let formHeight = unref(formRef)?.$el.offsetHeight ?? 0;
if (formHeight) {
formHeight += formMargin;
}
if (isBoolean(pagination) && !pagination) {
paginationMargin = 0;
}
if (isBoolean(useSearchForm) && !useSearchForm) {
paddingHeight = 0;
}
const headerCellHeight =
(tableEl.querySelector('.ant-table-title') as HTMLElement)?.offsetHeight ?? 0;
console.log(wrapHeight - formHeight - headerCellHeight - tablePadding - paginationMargin);
bottomIncludeBody =
wrapHeight - formHeight - headerCellHeight - tablePadding - paginationMargin;
} else {
// Table height from bottom
bottomIncludeBody = getViewportOffset(headEl).bottomIncludeBody;
}
let height =
bottomIncludeBody -
(resizeHeightOffset || 0) -
@@ -139,7 +167,6 @@ export function useTableScroll(
paginationHeight -
footerHeight -
headerHeight;
height = (height > maxHeight! ? (maxHeight as number) : height) ?? height;
setHeight(height);

View File

@@ -10,14 +10,15 @@ import type {
SizeType,
} from './types/table';
import type { FormProps } from '/@/components/Form';
import { DEFAULT_FILTER_FN, DEFAULT_SORT_FN, FETCH_SETTING, DEFAULT_SIZE } from './const';
import { propTypes } from '/@/utils/propTypes';
export const basicProps = {
clickToRowSelect: propTypes.bool.def(true),
isTreeTable: propTypes.bool.def(false),
clickToRowSelect: { type: Boolean, default: true },
isTreeTable: Boolean,
tableSetting: propTypes.shape<TableSetting>({}),
inset: propTypes.bool,
inset: Boolean,
sortFn: {
type: Function as PropType<(sortInfo: SorterResult) => any>,
default: DEFAULT_SORT_FN,
@@ -26,10 +27,10 @@ export const basicProps = {
type: Function as PropType<(data: Partial<Recordable<string[]>>) => any>,
default: DEFAULT_FILTER_FN,
},
showTableSetting: propTypes.bool,
autoCreateKey: propTypes.bool.def(true),
striped: propTypes.bool.def(true),
showSummary: propTypes.bool,
showTableSetting: Boolean,
autoCreateKey: { type: Boolean, default: true },
striped: { type: Boolean, default: true },
showSummary: Boolean,
summaryFunc: {
type: [Function, Array] as PropType<(...arg: any[]) => any[]>,
default: null,
@@ -39,7 +40,7 @@ export const basicProps = {
default: null,
},
indentSize: propTypes.number.def(24),
canColDrag: propTypes.bool.def(true),
canColDrag: { type: Boolean, default: true },
api: {
type: Function as PropType<(...arg: any[]) => Promise<any>>,
default: null,
@@ -63,8 +64,8 @@ export const basicProps = {
},
},
// 立即请求接口
immediate: propTypes.bool.def(true),
emptyDataIsShowTable: propTypes.bool.def(true),
immediate: { type: Boolean, default: true },
emptyDataIsShowTable: { type: Boolean, default: true },
// 额外的请求参数
searchInfo: {
type: Object as PropType<Recordable>,
@@ -86,7 +87,7 @@ export const basicProps = {
type: [Array] as PropType<BasicColumn[]>,
default: () => [],
},
showIndexColumn: propTypes.bool.def(true),
showIndexColumn: { type: Boolean, default: true },
indexColumnProps: {
type: Object as PropType<BasicColumn>,
default: null,
@@ -95,8 +96,9 @@ export const basicProps = {
type: Object as PropType<BasicColumn>,
default: null,
},
ellipsis: propTypes.bool.def(true),
canResize: propTypes.bool.def(true),
ellipsis: { type: Boolean, default: true },
isCanResizeParent: { type: Boolean, default: false },
canResize: { type: Boolean, default: true },
clearSelectOnPageChange: propTypes.bool,
resizeHeightOffset: propTypes.number.def(0),
rowSelection: {

View File

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

View File

@@ -1,10 +1,8 @@
import type { VNodeChild } from 'vue';
import type { PaginationProps } from './pagination';
import type { FormProps } from '/@/components/Form';
import type {
ColumnProps,
TableRowSelection as ITableRowSelection,
} from 'ant-design-vue/lib/table/interface';
import type { TableRowSelection as ITableRowSelection } from 'ant-design-vue/lib/table/interface';
import type { ColumnProps } from 'ant-design-vue/lib/table';
import { ComponentType } from './componentType';
import { VueNode } from '/@/utils/propTypes';
@@ -89,7 +87,9 @@ export interface TableActionType {
getSelectRows: <T = Recordable>() => T[];
clearSelectedRowKeys: () => void;
expandAll: () => void;
expandRows: (keys: string[]) => void;
collapseAll: () => void;
scrollTo: (pos: string) => void; // pos: id | "top" | "bottom"
getSelectRowKeys: () => string[];
deleteSelectRowByKey: (key: string) => void;
setPagination: (info: Partial<PaginationProps>) => void;
@@ -191,6 +191,8 @@ export interface BasicTableProps<T = any> {
actionColumn?: BasicColumn;
// 文本超过宽度是否显示。。。
ellipsis?: boolean;
// 是否继承父级高度(父级高度-表单高度-padding高度
isCanResizeParent?: boolean;
// 是否可以自适应高度
canResize?: boolean;
// 自适应高度偏移, 计算结果-偏移量
@@ -410,7 +412,7 @@ export type CellFormat =
| Map<string | number, any>;
// @ts-ignore
export interface BasicColumn extends ColumnProps {
export interface BasicColumn extends ColumnProps<Recordable> {
children?: BasicColumn[];
filters?: {
text: string;
@@ -439,7 +441,14 @@ export interface BasicColumn extends ColumnProps {
editRow?: boolean;
editable?: boolean;
editComponent?: ComponentType;
editComponentProps?: Recordable;
editComponentProps?:
| ((opt: {
text: string | number | boolean | Recordable;
record: Recordable;
column: BasicColumn;
index: number;
}) => Recordable)
| Recordable;
editRule?: boolean | ((text: string, record: Recordable) => Promise<string>);
editValueMap?: (value: any) => string;
onEditRow?: () => void;
@@ -447,6 +456,13 @@ export interface BasicColumn extends ColumnProps {
auth?: RoleEnum | RoleEnum[] | string | string[];
// 业务控制是否显示
ifShow?: boolean | ((column: BasicColumn) => boolean);
// 自定义修改后显示的内容
editRender?: (opt: {
text: string | number | boolean | Recordable;
record: Recordable;
column: BasicColumn;
index: number;
}) => VNodeChild | JSX.Element;
}
export type ColumnChangeParam = {

View File

@@ -23,4 +23,17 @@ export interface PopConfirm {
confirm: Fn;
cancel?: Fn;
icon?: string;
placement?:
| 'top'
| 'left'
| 'right'
| 'bottom'
| 'topLeft'
| 'topRight'
| 'leftTop'
| 'leftBottom'
| 'rightTop'
| 'rightBottom'
| 'bottomLeft'
| 'bottomRight';
}

View File

@@ -1,5 +1,6 @@
import BasicTree from './src/Tree.vue';
import './style';
export { BasicTree };
export type { ContextMenuItem } from '/@/hooks/web/useContextMenu';
export * from './src/typing';
export * from './src/tree';

View File

@@ -1,5 +1,6 @@
<script lang="tsx">
import type { ReplaceFields, Keys, CheckKeys, TreeActionType, TreeItem } from './typing';
import type { CSSProperties } from 'vue';
import type { FieldNames, TreeState, TreeItem, KeyType, CheckKeys, TreeActionType } from './tree';
import {
defineComponent,
@@ -10,48 +11,31 @@
watchEffect,
toRaw,
watch,
CSSProperties,
onMounted,
} from 'vue';
import { Tree, Empty } from 'ant-design-vue';
import { TreeIcon } from './TreeIcon';
import TreeHeader from './TreeHeader.vue';
import { Tree, Spin, Empty } from 'ant-design-vue';
import { TreeIcon } from './TreeIcon';
import { ScrollContainer } from '/@/components/Container';
import { omit, get, difference } from 'lodash-es';
import { omit, get, difference, cloneDeep } from 'lodash-es';
import { isArray, isBoolean, isEmpty, isFunction } from '/@/utils/is';
import { extendSlots, getSlot } from '/@/utils/helper/tsxHelper';
import { filter, treeToList } from '/@/utils/helper/treeHelper';
import { filter, treeToList, eachTree } from '/@/utils/helper/treeHelper';
import { useTree } from './useTree';
import { useContextMenu } from '/@/hooks/web/useContextMenu';
import { useDesign } from '/@/hooks/web/useDesign';
import { basicProps } from './props';
import { CreateContextOptions } from '/@/components/ContextMenu';
import { treeEmits, treeProps } from './tree';
import { createBEM } from '/@/utils/bem';
import { CheckEvent } from './typing';
interface State {
expandedKeys: Keys;
selectedKeys: Keys;
checkedKeys: CheckKeys;
checkStrictly: boolean;
}
export default defineComponent({
name: 'BasicTree',
inheritAttrs: false,
props: basicProps,
emits: [
'update:expandedKeys',
'update:selectedKeys',
'update:value',
'change',
'check',
'update:searchValue',
],
props: treeProps,
emits: treeEmits,
setup(props, { attrs, slots, emit, expose }) {
const state = reactive<State>({
const [bem] = createBEM('tree');
const state = reactive<TreeState>({
checkStrictly: props.checkStrictly,
expandedKeys: props.expandedKeys || [],
selectedKeys: props.selectedKeys || [],
@@ -67,15 +51,14 @@
const treeDataRef = ref<TreeItem[]>([]);
const [createContextMenu] = useContextMenu();
const { prefixCls } = useDesign('basic-tree');
const getReplaceFields = computed((): Required<ReplaceFields> => {
const { replaceFields } = props;
const getFieldNames = computed((): Required<FieldNames> => {
const { fieldNames } = props;
return {
children: 'children',
title: 'title',
key: 'key',
...replaceFields,
...fieldNames,
};
});
@@ -88,19 +71,19 @@
selectedKeys: state.selectedKeys,
checkedKeys: state.checkedKeys,
checkStrictly: state.checkStrictly,
replaceFields: unref(getReplaceFields),
'onUpdate:expandedKeys': (v: Keys) => {
filedNames: unref(getFieldNames),
'onUpdate:expandedKeys': (v: KeyType[]) => {
state.expandedKeys = v;
emit('update:expandedKeys', v);
},
'onUpdate:selectedKeys': (v: Keys) => {
'onUpdate:selectedKeys': (v: KeyType[]) => {
state.selectedKeys = v;
emit('update:selectedKeys', v);
},
onCheck: (v: CheckKeys, e: CheckEvent) => {
let currentValue = toRaw(state.checkedKeys) as Keys;
onCheck: (v: CheckKeys, e) => {
let currentValue = toRaw(state.checkedKeys) as KeyType[];
if (isArray(currentValue) && searchState.startSearch) {
const { key } = unref(getReplaceFields);
const { key } = unref(getFieldNames);
currentValue = difference(currentValue, getChildrenKeys(e.node.$attrs.node[key]));
if (e.checked) {
currentValue.push(e.node.$attrs.node[key]);
@@ -136,7 +119,7 @@
getAllKeys,
getChildrenKeys,
getEnabledKeys,
} = useTree(treeDataRef, getReplaceFields);
} = useTree(treeDataRef, getFieldNames);
function getIcon(params: Recordable, icon?: string) {
if (!icon) {
@@ -165,14 +148,14 @@
createContextMenu(contextMenuOptions);
}
function setExpandedKeys(keys: Keys) {
function setExpandedKeys(keys: KeyType[]) {
state.expandedKeys = keys;
}
function getExpandedKeys() {
return state.expandedKeys;
}
function setSelectedKeys(keys: Keys) {
function setSelectedKeys(keys: KeyType[]) {
state.selectedKeys = keys;
}
@@ -189,11 +172,11 @@
}
function checkAll(checkAll: boolean) {
state.checkedKeys = checkAll ? getEnabledKeys() : ([] as Keys);
state.checkedKeys = checkAll ? getEnabledKeys() : ([] as KeyType[]);
}
function expandAll(expandAll: boolean) {
state.expandedKeys = expandAll ? getAllKeys() : ([] as Keys);
state.expandedKeys = expandAll ? getAllKeys() : ([] as KeyType[]);
}
function onStrictlyChange(strictly: boolean) {
@@ -231,21 +214,21 @@
const { filterFn, checkable, expandOnSearch, checkOnSearch, selectedOnSearch } =
unref(props);
searchState.startSearch = true;
const { title: titleField, key: keyField } = unref(getReplaceFields);
const { title: titleField, key: keyField } = unref(getFieldNames);
const matchedKeys: string[] = [];
searchState.searchData = filter(
unref(treeDataRef),
(node) => {
const result = filterFn
? filterFn(searchValue, node, unref(getReplaceFields))
? filterFn(searchValue, node, unref(getFieldNames))
: node[titleField]?.includes(searchValue) ?? false;
if (result) {
matchedKeys.push(node[keyField]);
}
return result;
},
unref(getReplaceFields),
unref(getFieldNames),
);
if (expandOnSearch) {
@@ -321,15 +304,6 @@
},
);
// watchEffect(() => {
// console.log('======================');
// console.log(props.value);
// console.log('======================');
// if (props.value) {
// state.checkedKeys = props.value;
// }
// });
watchEffect(() => {
state.checkStrictly = props.checkStrictly;
});
@@ -358,8 +332,6 @@
},
};
expose(instance);
function renderAction(node: TreeItem) {
const { actionList } = props;
if (!actionList || actionList.length === 0) return;
@@ -374,29 +346,25 @@
if (!nodeShow) return null;
return (
<span key={index} class={`${prefixCls}__action`}>
<span key={index} class={bem('action')}>
{item.render(node)}
</span>
);
});
}
function renderTreeNode({ data, level }: { data: TreeItem[] | undefined; level: number }) {
if (!data) {
return null;
}
const searchText = searchState.searchText;
const { highlight } = unref(props);
return data.map((item) => {
const treeData = computed(() => {
const data = cloneDeep(getTreeData.value);
eachTree(data, (item, _parent) => {
const searchText = searchState.searchText;
const { highlight } = unref(props);
const {
title: titleField,
key: keyField,
children: childrenField,
} = unref(getReplaceFields);
} = unref(getFieldNames);
const propsData = omit(item, 'title');
const icon = getIcon({ ...item, level }, item.icon);
const children = get(item, childrenField) || [];
const icon = getIcon(item, item.icon);
const title = get(item, titleField);
const searchIdx = searchText ? title.indexOf(searchText) : -1;
@@ -405,7 +373,7 @@
const highlightStyle = `color: ${isBoolean(highlight) ? '#f50' : highlight}`;
const titleDom = isHighlight ? (
<span class={unref(getBindValues)?.blockNode ? `${prefixCls}__content` : ''}>
<span class={unref(getBindValues)?.blockNode ? `${bem('content')}` : ''}>
<span>{title.substr(0, searchIdx)}</span>
<span style={highlightStyle}>{searchText}</span>
<span>{title.substr(searchIdx + (searchText as string).length)}</span>
@@ -413,41 +381,35 @@
) : (
title
);
return (
<Tree.TreeNode {...propsData} node={toRaw(item)} key={get(item, keyField)}>
{{
title: () => (
<span
class={`${prefixCls}-title pl-2`}
onClick={handleClickNode.bind(null, item[keyField], item[childrenField])}
>
{item.slots?.title ? (
getSlot(slots, item.slots?.title, item)
) : (
<>
{icon && <TreeIcon icon={icon} />}
{titleDom}
{/*{get(item, titleField)}*/}
<span class={`${prefixCls}__actions`}>
{renderAction({ ...item, level })}
</span>
</>
)}
</span>
),
default: () => renderTreeNode({ data: children, level: level + 1 }),
}}
</Tree.TreeNode>
item[titleField] = (
<span
class={`${bem('title')} pl-2`}
onClick={handleClickNode.bind(null, item[keyField], item[childrenField])}
>
{slots?.title ? (
getSlot(slots, 'title', item)
) : (
<>
{icon && <TreeIcon icon={icon} />}
{titleDom}
<span class={bem('actions')}>{renderAction(item)}</span>
</>
)}
</span>
);
return item;
});
}
return data;
});
expose(instance);
return () => {
const { title, helpMessage, toolbar, search, checkable } = props;
const showTitle = title || toolbar || search || slots.headerTitle;
const scrollStyle: CSSProperties = { height: 'calc(100% - 38px)' };
return (
<div class={[prefixCls, 'h-full', attrs.class]}>
<div class={[bem(), 'h-full', attrs.class]}>
{showTitle && (
<TreeHeader
checkable={checkable}
@@ -464,67 +426,19 @@
{extendSlots(slots)}
</TreeHeader>
)}
<ScrollContainer style={scrollStyle} v-show={!unref(getNotFound)}>
<Tree {...unref(getBindValues)} showIcon={false}>
{{
// switcherIcon: () => <DownOutlined />,
default: () => renderTreeNode({ data: unref(getTreeData), level: 1 }),
...extendSlots(slots),
}}
</Tree>
</ScrollContainer>
<Empty v-show={unref(getNotFound)} image={Empty.PRESENTED_IMAGE_SIMPLE} class="!mt-4" />
<Spin spinning={unref(props.loading)} tip="加载中...">
<ScrollContainer style={scrollStyle} v-show={!unref(getNotFound)}>
<Tree {...unref(getBindValues)} showIcon={false} treeData={treeData.value} />
</ScrollContainer>
<Empty
v-show={unref(getNotFound)}
image={Empty.PRESENTED_IMAGE_SIMPLE}
class="!mt-4"
/>
</Spin>
</div>
);
};
},
});
</script>
<style lang="less">
@prefix-cls: ~'@{namespace}-basic-tree';
.@{prefix-cls} {
background-color: @component-background;
.ant-tree-node-content-wrapper {
position: relative;
.ant-tree-title {
position: absolute;
left: 0;
width: 100%;
}
}
&-title {
position: relative;
display: flex;
align-items: center;
width: 100%;
padding-right: 10px;
&:hover {
.@{prefix-cls}__action {
visibility: visible;
}
}
}
&__content {
overflow: hidden;
}
&__actions {
position: absolute;
top: 2px;
right: 3px;
display: flex;
}
&__action {
margin-left: 4px;
visibility: hidden;
}
}
</style>

View File

@@ -1,12 +1,11 @@
<template>
<div class="flex px-2 py-1.5 items-center basic-tree-header">
<slot name="headerTitle" v-if="$slots.headerTitle"></slot>
<BasicTitle :helpMessage="helpMessage" v-if="!$slots.headerTitle && title">
<div :class="bem()" class="flex px-2 py-1.5 items-center">
<slot name="headerTitle" v-if="slots.headerTitle"></slot>
<BasicTitle :helpMessage="helpMessage" v-if="!slots.headerTitle && title">
{{ title }}
</BasicTitle>
<div
class="flex flex-1 justify-self-stretch items-center cursor-pointer"
class="flex items-center flex-1 cursor-pointer justify-self-stretch"
v-if="search || toolbar"
>
<div :class="getInputSearchCls" v-if="search">
@@ -33,151 +32,139 @@
</div>
</div>
</template>
<script lang="ts">
import { PropType } from 'vue';
import { defineComponent, computed, ref, watch } from 'vue';
import { Dropdown, Menu, Input } from 'ant-design-vue';
<script lang="ts" setup>
import { computed, ref, watch, useSlots } from 'vue';
import { Dropdown, Menu, MenuItem, MenuDivider, InputSearch } from 'ant-design-vue';
import { Icon } from '/@/components/Icon';
import { BasicTitle } from '/@/components/Basic';
import { propTypes } from '/@/utils/propTypes';
import { useI18n } from '/@/hooks/web/useI18n';
import { useDebounceFn } from '@vueuse/core';
import { createBEM } from '/@/utils/bem';
import { ToolbarEnum } from './tree';
enum ToolbarEnum {
SELECT_ALL,
UN_SELECT_ALL,
EXPAND_ALL,
UN_EXPAND_ALL,
CHECK_STRICTLY,
CHECK_UN_STRICTLY,
}
const searchValue = ref('');
interface MenuInfo {
key: ToolbarEnum;
}
export default defineComponent({
name: 'BasicTreeHeader',
components: {
BasicTitle,
Icon,
Dropdown,
Menu,
MenuItem: Menu.Item,
MenuDivider: Menu.Divider,
InputSearch: Input.Search,
const [bem] = createBEM('tree-header');
const props = defineProps({
helpMessage: {
type: [String, Array] as PropType<string | string[]>,
default: '',
},
props: {
helpMessage: {
type: [String, Array] as PropType<string | string[]>,
default: '',
title: {
type: String,
default: '',
},
toolbar: {
type: Boolean,
default: false,
},
checkable: {
type: Boolean,
default: false,
},
search: {
type: Boolean,
default: false,
},
searchText: {
type: String,
default: '',
},
checkAll: {
type: Function,
default: undefined,
},
expandAll: {
type: Function,
default: undefined,
},
} as const);
const emit = defineEmits(['strictly-change', 'search']);
const slots = useSlots();
const { t } = useI18n();
const getInputSearchCls = computed(() => {
const titleExists = slots.headerTitle || props.title;
return [
'mr-1',
'w-full',
{
['ml-5']: titleExists,
},
title: propTypes.string,
toolbar: propTypes.bool,
checkable: propTypes.bool,
search: propTypes.bool,
checkAll: propTypes.func,
expandAll: propTypes.func,
searchText: propTypes.string,
},
emits: ['strictly-change', 'search'],
setup(props, { emit, slots }) {
const { t } = useI18n();
const searchValue = ref('');
];
});
const getInputSearchCls = computed(() => {
const titleExists = slots.headerTitle || props.title;
return [
'mr-1',
'w-full',
// titleExists ? 'w-2/3' : 'w-full',
{
['ml-5']: titleExists,
},
];
});
const toolbarList = computed(() => {
const { checkable } = props;
const defaultToolbarList = [
{ label: t('component.tree.expandAll'), value: ToolbarEnum.EXPAND_ALL },
{
label: t('component.tree.unExpandAll'),
value: ToolbarEnum.UN_EXPAND_ALL,
divider: checkable,
},
];
const toolbarList = computed(() => {
const { checkable } = props;
const defaultToolbarList = [
{ label: t('component.tree.expandAll'), value: ToolbarEnum.EXPAND_ALL },
return checkable
? [
{ label: t('component.tree.selectAll'), value: ToolbarEnum.SELECT_ALL },
{
label: t('component.tree.unExpandAll'),
value: ToolbarEnum.UN_EXPAND_ALL,
label: t('component.tree.unSelectAll'),
value: ToolbarEnum.UN_SELECT_ALL,
divider: checkable,
},
];
return checkable
? [
{ label: t('component.tree.selectAll'), value: ToolbarEnum.SELECT_ALL },
{
label: t('component.tree.unSelectAll'),
value: ToolbarEnum.UN_SELECT_ALL,
divider: checkable,
},
...defaultToolbarList,
{ label: t('component.tree.checkStrictly'), value: ToolbarEnum.CHECK_STRICTLY },
{ label: t('component.tree.checkUnStrictly'), value: ToolbarEnum.CHECK_UN_STRICTLY },
]
: defaultToolbarList;
});
function handleMenuClick(e: MenuInfo) {
const { key } = e;
switch (key) {
case ToolbarEnum.SELECT_ALL:
props.checkAll?.(true);
break;
case ToolbarEnum.UN_SELECT_ALL:
props.checkAll?.(false);
break;
case ToolbarEnum.EXPAND_ALL:
props.expandAll?.(true);
break;
case ToolbarEnum.UN_EXPAND_ALL:
props.expandAll?.(false);
break;
case ToolbarEnum.CHECK_STRICTLY:
emit('strictly-change', false);
break;
case ToolbarEnum.CHECK_UN_STRICTLY:
emit('strictly-change', true);
break;
}
}
function emitChange(value?: string): void {
emit('search', value);
}
const debounceEmitChange = useDebounceFn(emitChange, 200);
watch(
() => searchValue.value,
(v) => {
debounceEmitChange(v);
},
);
watch(
() => props.searchText,
(v) => {
if (v !== searchValue.value) {
searchValue.value = v;
}
},
);
// function handleSearch(e: ChangeEvent): void {
// debounceEmitChange(e.target.value);
// }
return { t, toolbarList, handleMenuClick, searchValue, getInputSearchCls };
},
...defaultToolbarList,
{ label: t('component.tree.checkStrictly'), value: ToolbarEnum.CHECK_STRICTLY },
{ label: t('component.tree.checkUnStrictly'), value: ToolbarEnum.CHECK_UN_STRICTLY },
]
: defaultToolbarList;
});
</script>
<style lang="less" scoped>
.basic-tree-header {
border-bottom: 1px solid @border-color-base;
function handleMenuClick(e: { key: ToolbarEnum }) {
const { key } = e;
switch (key) {
case ToolbarEnum.SELECT_ALL:
props.checkAll?.(true);
break;
case ToolbarEnum.UN_SELECT_ALL:
props.checkAll?.(false);
break;
case ToolbarEnum.EXPAND_ALL:
props.expandAll?.(true);
break;
case ToolbarEnum.UN_EXPAND_ALL:
props.expandAll?.(false);
break;
case ToolbarEnum.CHECK_STRICTLY:
emit('strictly-change', false);
break;
case ToolbarEnum.CHECK_UN_STRICTLY:
emit('strictly-change', true);
break;
}
}
</style>
function emitChange(value?: string): void {
emit('search', value);
}
const debounceEmitChange = useDebounceFn(emitChange, 200);
watch(
() => searchValue.value,
(v) => {
debounceEmitChange(v);
},
);
watch(
() => props.searchText,
(v) => {
if (v !== searchValue.value) {
searchValue.value = v;
}
},
);
</script>

View File

@@ -1,14 +1,10 @@
import type { VNode, FunctionalComponent } from 'vue';
import { h } from 'vue';
import { isString } from '/@/utils/is';
import { isString } from '@vue/shared';
import { Icon } from '/@/components/Icon';
export interface ComponentProps {
icon: VNode | string;
}
export const TreeIcon: FunctionalComponent = ({ icon }: ComponentProps) => {
export const TreeIcon: FunctionalComponent = ({ icon }: { icon: VNode | string }) => {
if (!icon) return null;
if (isString(icon)) {
return h(Icon, { icon, class: 'mr-1' });

View File

@@ -1,108 +0,0 @@
import type { PropType } from 'vue';
import type {
ReplaceFields,
ActionItem,
Keys,
CheckKeys,
ContextMenuOptions,
TreeItem,
} from './typing';
import type { ContextMenuItem } from '/@/hooks/web/useContextMenu';
import type { TreeDataItem } from 'ant-design-vue/es/tree/Tree';
import { propTypes } from '/@/utils/propTypes';
export const basicProps = {
value: {
type: [Object, Array] as PropType<Keys | CheckKeys>,
},
renderIcon: {
type: Function as PropType<(params: Recordable) => string>,
},
helpMessage: {
type: [String, Array] as PropType<string | string[]>,
default: '',
},
title: propTypes.string,
toolbar: propTypes.bool,
search: propTypes.bool,
searchValue: propTypes.string,
checkStrictly: propTypes.bool,
clickRowToExpand: propTypes.bool.def(true),
checkable: propTypes.bool.def(false),
defaultExpandLevel: {
type: [String, Number] as PropType<string | number>,
default: '',
},
defaultExpandAll: propTypes.bool.def(false),
replaceFields: {
type: Object as PropType<ReplaceFields>,
},
treeData: {
type: Array as PropType<TreeDataItem[]>,
},
actionList: {
type: Array as PropType<ActionItem[]>,
default: () => [],
},
expandedKeys: {
type: Array as PropType<Keys>,
default: () => [],
},
selectedKeys: {
type: Array as PropType<Keys>,
default: () => [],
},
checkedKeys: {
type: Array as PropType<CheckKeys>,
default: () => [],
},
beforeRightClick: {
type: Function as PropType<(...arg: any) => ContextMenuItem[] | ContextMenuOptions>,
default: null,
},
rightMenuList: {
type: Array as PropType<ContextMenuItem[]>,
},
// 自定义数据过滤判断方法(注: 不是整个过滤方法而是内置过滤的判断方法用于增强原本仅能通过title进行过滤的方式)
filterFn: {
type: Function as PropType<
(searchValue: any, node: TreeItem, replaceFields: ReplaceFields) => boolean
>,
default: null,
},
// 高亮搜索值仅高亮具体匹配值通过title值为true时使用默认色值值为#xxx时使用此值替代且高亮开启
highlight: {
type: [Boolean, String] as PropType<Boolean | String>,
default: false,
},
// 搜索完成时自动展开结果
expandOnSearch: propTypes.bool.def(false),
// 搜索完成自动选中所有结果,当且仅当 checkable===true 时生效
checkOnSearch: propTypes.bool.def(false),
// 搜索完成自动select所有结果
selectedOnSearch: propTypes.bool.def(false),
};
export const treeNodeProps = {
actionList: {
type: Array as PropType<ActionItem[]>,
default: () => [],
},
replaceFields: {
type: Object as PropType<ReplaceFields>,
},
treeData: {
type: Array as PropType<TreeDataItem[]>,
default: () => [],
},
};

View File

@@ -0,0 +1,188 @@
import type { ExtractPropTypes } from 'vue';
import type { TreeDataItem } from 'ant-design-vue/es/tree/Tree';
import { buildProps } from '/@/utils/props';
export enum ToolbarEnum {
SELECT_ALL,
UN_SELECT_ALL,
EXPAND_ALL,
UN_EXPAND_ALL,
CHECK_STRICTLY,
CHECK_UN_STRICTLY,
}
export const treeEmits = [
'update:expandedKeys',
'update:selectedKeys',
'update:value',
'change',
'check',
'update:searchValue',
];
export interface TreeState {
expandedKeys: KeyType[];
selectedKeys: KeyType[];
checkedKeys: CheckKeys;
checkStrictly: boolean;
}
export interface FieldNames {
children?: string;
title?: string;
key?: string;
}
export type KeyType = string | number;
export type CheckKeys =
| KeyType[]
| { checked: string[] | number[]; halfChecked: string[] | number[] };
export const treeProps = buildProps({
value: {
type: [Object, Array] as PropType<KeyType[] | CheckKeys>,
},
renderIcon: {
type: Function as PropType<(params: Recordable) => string>,
},
helpMessage: {
type: [String, Array] as PropType<string | string[]>,
default: '',
},
title: {
type: String,
default: '',
},
toolbar: Boolean,
search: Boolean,
searchValue: {
type: String,
default: '',
},
checkStrictly: Boolean,
clickRowToExpand: {
type: Boolean,
default: false,
},
checkable: Boolean,
defaultExpandLevel: {
type: [String, Number] as PropType<string | number>,
default: '',
},
defaultExpandAll: Boolean,
fieldNames: {
type: Object as PropType<FieldNames>,
},
treeData: {
type: Array as PropType<TreeDataItem[]>,
},
actionList: {
type: Array as PropType<TreeActionItem[]>,
default: () => [],
},
expandedKeys: {
type: Array as PropType<KeyType[]>,
default: () => [],
},
selectedKeys: {
type: Array as PropType<KeyType[]>,
default: () => [],
},
checkedKeys: {
type: Array as PropType<CheckKeys>,
default: () => [],
},
beforeRightClick: {
type: Function as PropType<(...arg: any) => ContextMenuItem[] | ContextMenuOptions>,
default: undefined,
},
rightMenuList: {
type: Array as PropType<ContextMenuItem[]>,
},
// 自定义数据过滤判断方法(注: 不是整个过滤方法而是内置过滤的判断方法用于增强原本仅能通过title进行过滤的方式)
filterFn: {
type: Function as PropType<
(searchValue: any, node: TreeItem, fieldNames: FieldNames) => boolean
>,
default: undefined,
},
// 高亮搜索值仅高亮具体匹配值通过title值为true时使用默认色值值为#xxx时使用此值替代且高亮开启
highlight: {
type: [Boolean, String] as PropType<Boolean | String>,
default: false,
},
// 搜索完成时自动展开结果
expandOnSearch: Boolean,
// 搜索完成自动选中所有结果,当且仅当 checkable===true 时生效
checkOnSearch: Boolean,
// 搜索完成自动select所有结果
selectedOnSearch: Boolean,
loading: {
type: Boolean,
default: false,
},
});
export type TreeProps = ExtractPropTypes<typeof treeProps>;
export interface ContextMenuItem {
label: string;
icon?: string;
disabled?: boolean;
handler?: Fn;
divider?: boolean;
children?: ContextMenuItem[];
}
export interface ContextMenuOptions {
icon?: string;
styles?: any;
items?: ContextMenuItem[];
}
export interface TreeItem extends TreeDataItem {
icon?: any;
}
export interface TreeActionItem {
render: (record: Recordable) => any;
show?: boolean | ((record: Recordable) => boolean);
}
export interface InsertNodeParams {
parentKey: string | null;
node: TreeDataItem;
list?: TreeDataItem[];
push?: 'push' | 'unshift';
}
export interface TreeActionType {
checkAll: (checkAll: boolean) => void;
expandAll: (expandAll: boolean) => void;
setExpandedKeys: (keys: KeyType[]) => void;
getExpandedKeys: () => KeyType[];
setSelectedKeys: (keys: KeyType[]) => void;
getSelectedKeys: () => KeyType[];
setCheckedKeys: (keys: CheckKeys) => void;
getCheckedKeys: () => CheckKeys;
filterByLevel: (level: number) => void;
insertNodeByKey: (opt: InsertNodeParams) => void;
insertNodesByKey: (opt: InsertNodeParams) => void;
deleteNodeByKey: (key: string) => void;
updateNodeByKey: (key: string, node: Omit<TreeDataItem, 'key'>) => void;
setSearchValue: (value: string) => void;
getSearchValue: () => string;
}

View File

@@ -1,54 +0,0 @@
import type { TreeDataItem, CheckEvent as CheckEventOrigin } from 'ant-design-vue/es/tree/Tree';
import { ContextMenuItem } from '/@/hooks/web/useContextMenu';
export interface ActionItem {
render: (record: Recordable) => any;
show?: boolean | ((record: Recordable) => boolean);
}
export interface TreeItem extends TreeDataItem {
icon?: any;
}
export interface ReplaceFields {
children?: string;
title?: string;
key?: string;
}
export type Keys = (string | number)[];
export type CheckKeys =
| (string | number)[]
| { checked: (string | number)[]; halfChecked: (string | number)[] };
export interface TreeActionType {
checkAll: (checkAll: boolean) => void;
expandAll: (expandAll: boolean) => void;
setExpandedKeys: (keys: Keys) => void;
getExpandedKeys: () => Keys;
setSelectedKeys: (keys: Keys) => void;
getSelectedKeys: () => Keys;
setCheckedKeys: (keys: CheckKeys) => void;
getCheckedKeys: () => CheckKeys;
filterByLevel: (level: number) => void;
insertNodeByKey: (opt: InsertNodeParams) => void;
insertNodesByKey: (opt: InsertNodeParams) => void;
deleteNodeByKey: (key: string) => void;
updateNodeByKey: (key: string, node: Omit<TreeDataItem, 'key'>) => void;
setSearchValue: (value: string) => void;
getSearchValue: () => string;
}
export interface InsertNodeParams {
parentKey: string | null;
node: TreeDataItem;
list?: TreeDataItem[];
push?: 'push' | 'unshift';
}
export interface ContextMenuOptions {
icon?: string;
styles?: any;
items?: ContextMenuItem[];
}
export type CheckEvent = CheckEventOrigin;

View File

@@ -1,4 +1,4 @@
import type { InsertNodeParams, Keys, ReplaceFields } from './typing';
import type { InsertNodeParams, KeyType, FieldNames } from './tree';
import type { Ref, ComputedRef } from 'vue';
import type { TreeDataItem } from 'ant-design-vue/es/tree/Tree';
@@ -6,14 +6,11 @@ import { cloneDeep } from 'lodash-es';
import { unref } from 'vue';
import { forEach } from '/@/utils/helper/treeHelper';
export function useTree(
treeDataRef: Ref<TreeDataItem[]>,
getReplaceFields: ComputedRef<ReplaceFields>,
) {
export function useTree(treeDataRef: Ref<TreeDataItem[]>, getFieldNames: ComputedRef<FieldNames>) {
function getAllKeys(list?: TreeDataItem[]) {
const keys: string[] = [];
const treeData = list || unref(treeDataRef);
const { key: keyField, children: childrenField } = unref(getReplaceFields);
const { key: keyField, children: childrenField } = unref(getFieldNames);
if (!childrenField || !keyField) return keys;
for (let index = 0; index < treeData.length; index++) {
@@ -24,13 +21,14 @@ export function useTree(
keys.push(...(getAllKeys(children) as string[]));
}
}
return keys as Keys;
return keys as KeyType[];
}
// get keys that can be checked and selected
function getEnabledKeys(list?: TreeDataItem[]) {
const keys: string[] = [];
const treeData = list || unref(treeDataRef);
const { key: keyField, children: childrenField } = unref(getReplaceFields);
const { key: keyField, children: childrenField } = unref(getFieldNames);
if (!childrenField || !keyField) return keys;
for (let index = 0; index < treeData.length; index++) {
@@ -41,13 +39,13 @@ export function useTree(
keys.push(...(getEnabledKeys(children) as string[]));
}
}
return keys as Keys;
return keys as KeyType[];
}
function getChildrenKeys(nodeKey: string | number, list?: TreeDataItem[]): Keys {
const keys: Keys = [];
function getChildrenKeys(nodeKey: string | number, list?: TreeDataItem[]) {
const keys: KeyType[] = [];
const treeData = list || unref(treeDataRef);
const { key: keyField, children: childrenField } = unref(getReplaceFields);
const { key: keyField, children: childrenField } = unref(getFieldNames);
if (!childrenField || !keyField) return keys;
for (let index = 0; index < treeData.length; index++) {
const node = treeData[index];
@@ -63,14 +61,14 @@ export function useTree(
}
}
}
return keys as Keys;
return keys as KeyType[];
}
// Update node
function updateNodeByKey(key: string, node: TreeDataItem, list?: TreeDataItem[]) {
if (!key) return;
const treeData = list || unref(treeDataRef);
const { key: keyField, children: childrenField } = unref(getReplaceFields);
const { key: keyField, children: childrenField } = unref(getFieldNames);
if (!childrenField || !keyField) return;
@@ -97,7 +95,7 @@ export function useTree(
for (let index = 0; index < data.length; index++) {
const item = data[index];
const { key: keyField, children: childrenField } = unref(getReplaceFields);
const { key: keyField, children: childrenField } = unref(getFieldNames);
const key = keyField ? item[keyField] : '';
const children = childrenField ? item[childrenField] : [];
res.push(key);
@@ -119,7 +117,7 @@ export function useTree(
treeDataRef.value = treeData;
return;
}
const { key: keyField, children: childrenField } = unref(getReplaceFields);
const { key: keyField, children: childrenField } = unref(getFieldNames);
if (!childrenField || !keyField) return;
forEach(treeData, (treeItem) => {
@@ -144,7 +142,7 @@ export function useTree(
treeData[push](list[i]);
}
} else {
const { key: keyField, children: childrenField } = unref(getReplaceFields);
const { key: keyField, children: childrenField } = unref(getFieldNames);
if (!childrenField || !keyField) return;
forEach(treeData, (treeItem) => {
@@ -163,7 +161,7 @@ export function useTree(
function deleteNodeByKey(key: string, list?: TreeDataItem[]) {
if (!key) return;
const treeData = list || unref(treeDataRef);
const { key: keyField, children: childrenField } = unref(getReplaceFields);
const { key: keyField, children: childrenField } = unref(getFieldNames);
if (!childrenField || !keyField) return;
for (let index = 0; index < treeData.length; index++) {

Some files were not shown because too many files have changed in this diff Show More