initial commit
15
.editorconfig
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset=utf-8
|
||||||
|
end_of_line=lf
|
||||||
|
insert_final_newline=false
|
||||||
|
indent_style=space
|
||||||
|
indent_size=2
|
||||||
|
|
||||||
|
[*.yml]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
1
.env.development
Normal file
@ -0,0 +1 @@
|
|||||||
|
VITE_USE_MOCK=true
|
1
.env.production
Normal file
@ -0,0 +1 @@
|
|||||||
|
VITE_USE_MOCK=true
|
28
.eslintignore
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
|
||||||
|
*.sh
|
||||||
|
node_modules
|
||||||
|
lib
|
||||||
|
*.md
|
||||||
|
*.scss
|
||||||
|
*.woff
|
||||||
|
*.ttf
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
/dist/
|
||||||
|
/mock/
|
||||||
|
/public
|
||||||
|
/docs
|
||||||
|
.vscode
|
||||||
|
.local
|
||||||
|
/bin
|
||||||
|
/build
|
||||||
|
/config
|
||||||
|
Dockerfile
|
||||||
|
vue.config.js
|
||||||
|
commit-lint.js
|
||||||
|
/src/assets/iconfont/
|
||||||
|
/types/shims
|
||||||
|
/src/types/shims
|
||||||
|
postcss.config.js
|
||||||
|
stylelint.config.js
|
||||||
|
commitlint.config.js
|
62
.eslintrc.js
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
module.exports = {
|
||||||
|
parser: 'vue-eslint-parser',
|
||||||
|
parserOptions: {
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
sourceType: 'module',
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true,
|
||||||
|
jsx: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
extends: [
|
||||||
|
'plugin:vue/vue3-recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'prettier/@typescript-eslint',
|
||||||
|
'plugin:prettier/recommended',
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/ban-ts-ignore': 'off',
|
||||||
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'@typescript-eslint/no-var-requires': 'off',
|
||||||
|
'@typescript-eslint/no-empty-function': 'off',
|
||||||
|
'vue/custom-event-name-casing': 'off',
|
||||||
|
'no-use-before-define': 'off',
|
||||||
|
// 'no-use-before-define': [
|
||||||
|
// 'error',
|
||||||
|
// {
|
||||||
|
// functions: false,
|
||||||
|
// classes: true,
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
'@typescript-eslint/no-use-before-define': 'off',
|
||||||
|
// '@typescript-eslint/no-use-before-define': [
|
||||||
|
// 'error',
|
||||||
|
// {
|
||||||
|
// functions: false,
|
||||||
|
// classes: true,
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
'@typescript-eslint/ban-ts-comment': 'off',
|
||||||
|
'@typescript-eslint/ban-types': 'off',
|
||||||
|
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||||
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
|
'@typescript-eslint/no-unused-vars': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
argsIgnorePattern: '^h$',
|
||||||
|
varsIgnorePattern: '^h$',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'no-unused-vars': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
argsIgnorePattern: '^h$',
|
||||||
|
varsIgnorePattern: '^h$',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'space-before-function-paren': 'off',
|
||||||
|
},
|
||||||
|
};
|
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
*.local
|
||||||
|
.npmrc
|
||||||
|
.cache
|
20
.ls-lint.yml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
ls:
|
||||||
|
src/*:
|
||||||
|
.js: kebab-case | PascalCase
|
||||||
|
.vue: PascalCase | regex:^index
|
||||||
|
.ts: camelCase | PascalCase
|
||||||
|
.d.ts: kebab-case
|
||||||
|
.mock.ts: kebab-case
|
||||||
|
.data.ts: camelCase | kebab-case
|
||||||
|
.test-d.ts: kebab-case
|
||||||
|
.less: kebab-case | PascalCase
|
||||||
|
.spec.ts: camelCase | PascalCase
|
||||||
|
|
||||||
|
ignore:
|
||||||
|
- node_modules
|
||||||
|
- .git
|
||||||
|
- .circleci
|
||||||
|
- .github
|
||||||
|
- .vscode
|
||||||
|
- dist
|
||||||
|
- .local
|
7
.prettierignore
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
/dist/*
|
||||||
|
.local
|
||||||
|
.output.js
|
||||||
|
/node_modules/**
|
||||||
|
|
||||||
|
**/*.svg
|
||||||
|
**/*.sh
|
21
CHANGELOG.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# 2.0.0 (2020-09-28)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- add 37afeff
|
||||||
|
- add menu ab58829
|
||||||
|
- add menu aeb75e7
|
||||||
|
- add split menu 6b2b7bd
|
||||||
|
- add split menu 2e7cb0b
|
||||||
|
- add split menu 58fa70e
|
||||||
|
- add split menu aa87a2c
|
||||||
|
- auth d36878e
|
||||||
|
- header be36cc2
|
||||||
|
- prettier 3f1db50
|
||||||
|
|
||||||
|
### Performance Improvements
|
||||||
|
|
||||||
|
- form 2f94a5d
|
||||||
|
- loading 788fd64
|
||||||
|
- lockpage 92d6b7e
|
||||||
|
- menu ae6ace8
|
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020-present, Vben
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
26
build/config/glob/lessModifyVars.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* less global variable
|
||||||
|
*/
|
||||||
|
const primaryColor = '#018ffb';
|
||||||
|
//{
|
||||||
|
const modifyVars = {
|
||||||
|
'primary-color': primaryColor, // Global dominant color
|
||||||
|
'info-color': primaryColor, // Default color
|
||||||
|
'success-color': '#55D187', // Success color
|
||||||
|
'error-color': '#ED6F6F', // False color
|
||||||
|
'warning-color': '#EFBD47', // Warning color
|
||||||
|
'link-color': primaryColor, // Link color
|
||||||
|
'disabled-color': '#C2C2CC', // Failure color
|
||||||
|
'heading-color': '#2C3A61', // Title color
|
||||||
|
'text-color': '#2C3A61', // Main text color
|
||||||
|
'text-color-secondary ': '#606266', // Subtext color
|
||||||
|
'background-color-base': '#F0F2F5', // background color
|
||||||
|
'font-size-base': '14px', // Main font size
|
||||||
|
'box-shadow-base': '0 2px 8px rgba(0, 0, 0, 0.15)', // Floating shadow
|
||||||
|
'border-color-base': '#cececd', // Border color,
|
||||||
|
'border-color-split': '#cececd', // Border color,
|
||||||
|
'border-radius-base': '2px', // Component/float fillet
|
||||||
|
};
|
||||||
|
//}
|
||||||
|
|
||||||
|
export { modifyVars, primaryColor };
|
10
build/config/vite/env.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import moment from 'moment';
|
||||||
|
// @ts-ignore
|
||||||
|
import pkg from '../../../package.json';
|
||||||
|
export function setupBasicEnv() {
|
||||||
|
// version
|
||||||
|
process.env.VITE_VERSION = (pkg as any).version;
|
||||||
|
// build time
|
||||||
|
process.env.VITE_APP_BUILD_TIME = moment().format('YYYY-MM-DD HH:mm:ss');
|
||||||
|
process.env.VITE_BUILD_SHORT_TIME = moment().format('MMDDHHmmss');
|
||||||
|
}
|
15
build/config/vite/proxy.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
type ProxyItem = [string, string];
|
||||||
|
|
||||||
|
type ProxyList = ProxyItem[];
|
||||||
|
|
||||||
|
export function createProxy(list: ProxyList) {
|
||||||
|
const ret: any = {};
|
||||||
|
for (const [prefix, target] of list) {
|
||||||
|
ret[prefix] = {
|
||||||
|
target: target,
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: (path: string) => path.replace(new RegExp(`^${prefix}`), ''),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
22
build/gzip/index.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
// Build gzip after packaging
|
||||||
|
// import { readFile, writeFile } from 'fs';
|
||||||
|
import viteConfig from '../../vite.config';
|
||||||
|
import {
|
||||||
|
// basename,
|
||||||
|
join,
|
||||||
|
} from 'path';
|
||||||
|
// import { promisify } from 'util';
|
||||||
|
// import { gzip, ZlibOptions } from 'zlib';
|
||||||
|
import { readAllFile } from '../utils';
|
||||||
|
|
||||||
|
// const readFilePromise = promisify(readFile);
|
||||||
|
// const writeFilePromise = promisify(writeFile);
|
||||||
|
|
||||||
|
// function createGzip() {}
|
||||||
|
|
||||||
|
const FILE_REG = /\.(js|mjs|json|css|html)$/;
|
||||||
|
|
||||||
|
const OUT_DIR = viteConfig.outDir || 'dist';
|
||||||
|
|
||||||
|
// TODO 待开发
|
||||||
|
const files = readAllFile(join(process.cwd(), OUT_DIR), FILE_REG);
|
56
build/gzip/types.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import type { ZlibOptions } from 'zlib';
|
||||||
|
|
||||||
|
export type StringMappingOption = (originalString: string) => string;
|
||||||
|
export type CustomCompressionOption = (
|
||||||
|
content: string | Buffer
|
||||||
|
) => string | Buffer | Promise<string | Buffer>;
|
||||||
|
|
||||||
|
export interface GzipPluginOptions {
|
||||||
|
/**
|
||||||
|
* Control which of the output files to compress
|
||||||
|
*
|
||||||
|
* Defaults to `/\.(js|mjs|json|css|html)$/`
|
||||||
|
*/
|
||||||
|
filter?: RegExp | ((fileName: string) => boolean);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GZIP compression options, see https://nodejs.org/api/zlib.html#zlib_class_options
|
||||||
|
*/
|
||||||
|
gzipOptions?: ZlibOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specified the minimum size in Bytes for a file to get compressed.
|
||||||
|
* Files that are smaller than this threshold will not be compressed.
|
||||||
|
* This does not apply to the files specified through `additionalFiles`!
|
||||||
|
*/
|
||||||
|
minSize?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This option allows you to compress additional files outside of the main rollup bundling process.
|
||||||
|
* The processing is delayed to make sure the files are written on disk; the delay is controlled
|
||||||
|
* through `additionalFilesDelay`.
|
||||||
|
*/
|
||||||
|
additionalFiles?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This options sets a delay (ms) before the plugin compresses the files specified through `additionalFiles`.
|
||||||
|
* Increase this value if your artifacts take a long time to generate.
|
||||||
|
*
|
||||||
|
* Defaults to `2000`
|
||||||
|
*/
|
||||||
|
additionalFilesDelay?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a custom compression algorithm. The function can either return the compressed contents synchronously,
|
||||||
|
* or otherwise return a promise for asynchronous processing.
|
||||||
|
*/
|
||||||
|
customCompression?: CustomCompressionOption;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a custom file name convention for the compressed files. Can be a suffix string or a function
|
||||||
|
* returning the file name.
|
||||||
|
*
|
||||||
|
* Defaults to `.gz`
|
||||||
|
*/
|
||||||
|
fileName?: string | StringMappingOption;
|
||||||
|
}
|
39
build/script/changelog.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
// #!/usr/bin/env node
|
||||||
|
|
||||||
|
import { sh } from 'tasksfile';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
|
||||||
|
const createChangeLog = async () => {
|
||||||
|
try {
|
||||||
|
let cmd = `conventional-changelog -p angular -i CHANGELOG.md -s -r 0 `;
|
||||||
|
// let cmd = `conventional-changelog -p angular -i CHANGELOG.md -s -r 0 `;
|
||||||
|
// if (shell.which('git')) {
|
||||||
|
// cmd += '&& git add CHANGELOG.md';
|
||||||
|
// }
|
||||||
|
await sh(cmd, {
|
||||||
|
async: true,
|
||||||
|
nopipe: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await sh('prettier --write **/CHANGELOG.md ', {
|
||||||
|
async: true,
|
||||||
|
nopipe: true,
|
||||||
|
});
|
||||||
|
console.log(
|
||||||
|
chalk.blue.bold('**************** ') +
|
||||||
|
chalk.green.bold('CHANGE_LOG generated successfully!') +
|
||||||
|
chalk.blue.bold(' ****************')
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(
|
||||||
|
chalk.blue.red('**************** ') +
|
||||||
|
chalk.green.red('CHANGE_LOG generated error\n' + error) +
|
||||||
|
chalk.blue.red(' ****************')
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
createChangeLog();
|
||||||
|
module.exports = {
|
||||||
|
createChangeLog,
|
||||||
|
};
|
10
build/script/postinstall.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { exec, which } from 'shelljs';
|
||||||
|
|
||||||
|
function ignoreCaseGit() {
|
||||||
|
try {
|
||||||
|
if (which('git')) {
|
||||||
|
exec('git config core.ignorecase false ');
|
||||||
|
}
|
||||||
|
} catch (error) {}
|
||||||
|
}
|
||||||
|
ignoreCaseGit();
|
67
build/script/preserve.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
// 是否需要更新依赖,防止package.json更新了依赖,其他人获取代码后没有install
|
||||||
|
|
||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs-extra';
|
||||||
|
import { isEqual } from 'lodash';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import { sh } from 'tasksfile';
|
||||||
|
|
||||||
|
const resolve = (dir: string) => {
|
||||||
|
return path.resolve(process.cwd(), dir);
|
||||||
|
};
|
||||||
|
|
||||||
|
let NEED_INSTALL = false;
|
||||||
|
|
||||||
|
fs.mkdirp(resolve('build/.cache'));
|
||||||
|
function checkPkgUpdate() {
|
||||||
|
const pkg = require('../../package.json');
|
||||||
|
const { dependencies, devDependencies } = pkg;
|
||||||
|
const depsFile = resolve('build/.cache/deps.json');
|
||||||
|
if (!fs.pathExistsSync(depsFile)) {
|
||||||
|
NEED_INSTALL = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const depsJson = require('../.cache/deps.json');
|
||||||
|
|
||||||
|
if (!isEqual(depsJson, { dependencies, devDependencies })) {
|
||||||
|
NEED_INSTALL = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
checkPkgUpdate();
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
if (NEED_INSTALL) {
|
||||||
|
console.log(
|
||||||
|
chalk.blue.bold('**************** ') +
|
||||||
|
chalk.red.bold('检测到依赖变化,正在安装依赖(Tip: 项目首次运行也会执行)!') +
|
||||||
|
chalk.blue.bold(' ****************')
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
// 从代码执行貌似不会自动读取.npmrc 所以手动加上源地址
|
||||||
|
// await run('yarn install --registry=https://registry.npm.taobao.org ', {
|
||||||
|
await sh('yarn install ', {
|
||||||
|
async: true,
|
||||||
|
nopipe: true,
|
||||||
|
});
|
||||||
|
console.log(
|
||||||
|
chalk.blue.bold('**************** ') +
|
||||||
|
chalk.green.bold('依赖安装成功,正在运行!') +
|
||||||
|
chalk.blue.bold(' ****************')
|
||||||
|
);
|
||||||
|
|
||||||
|
const pkg = require('../../package.json');
|
||||||
|
const { dependencies, devDependencies } = pkg;
|
||||||
|
const depsFile = resolve('build/.cache/deps.json');
|
||||||
|
const deps = { dependencies, devDependencies };
|
||||||
|
if (!fs.pathExistsSync(depsFile)) {
|
||||||
|
fs.writeFileSync(depsFile, JSON.stringify(deps));
|
||||||
|
} else {
|
||||||
|
const depsFile = resolve('build/.cache/deps.json');
|
||||||
|
const depsJson = require('../.cache/deps.json');
|
||||||
|
if (!isEqual(depsJson, deps)) {
|
||||||
|
fs.writeFileSync(depsFile, JSON.stringify(deps));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {}
|
||||||
|
}
|
||||||
|
})();
|
70
build/script/preview.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import chalk from 'chalk';
|
||||||
|
import Koa from 'koa';
|
||||||
|
import inquirer from 'inquirer';
|
||||||
|
import { sh } from 'tasksfile';
|
||||||
|
import staticServer from 'koa-static';
|
||||||
|
import portfinder from 'portfinder';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
import viteConfig from '../../vite.config';
|
||||||
|
import { getIPAddress } from '../utils';
|
||||||
|
|
||||||
|
const BUILD = 1;
|
||||||
|
const NO_BUILD = 2;
|
||||||
|
|
||||||
|
// 启动服务器
|
||||||
|
const startApp = () => {
|
||||||
|
const port = 9680;
|
||||||
|
portfinder.basePort = port;
|
||||||
|
const app = new Koa();
|
||||||
|
// const connect = require('connect');
|
||||||
|
// const serveStatic = require('serve-static');
|
||||||
|
// const app = connect();
|
||||||
|
|
||||||
|
app.use(staticServer(resolve(process.cwd(), viteConfig.outDir || 'dist')));
|
||||||
|
|
||||||
|
portfinder.getPort(async (err, port) => {
|
||||||
|
if (err) {
|
||||||
|
throw err;
|
||||||
|
} else {
|
||||||
|
// const publicPath = process.env.BASE_URL;
|
||||||
|
app.listen(port, function () {
|
||||||
|
const empty = ' ';
|
||||||
|
const common = `The preview program is already running:
|
||||||
|
- LOCAL: http://localhost:${port}/
|
||||||
|
- NETWORK: http://${getIPAddress()}:${port}/
|
||||||
|
`;
|
||||||
|
console.log(chalk.cyan('\n' + empty + common));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const preview = async () => {
|
||||||
|
const prompt = inquirer.prompt({
|
||||||
|
type: 'list',
|
||||||
|
message: 'Please select a preview method',
|
||||||
|
name: 'type',
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
name: 'Preview after packaging',
|
||||||
|
value: BUILD,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: `No packaging, preview directly (need to have dist file after packaging)`,
|
||||||
|
value: NO_BUILD,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const { type } = await prompt;
|
||||||
|
if (type === BUILD) {
|
||||||
|
await sh('npm run build', {
|
||||||
|
async: true,
|
||||||
|
nopipe: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
startApp();
|
||||||
|
};
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
preview();
|
||||||
|
})();
|
18
build/tsconfig.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "esnext",
|
||||||
|
"module": "commonjs",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"jsx": "react",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"lib": ["dom", "esnext"],
|
||||||
|
"incremental": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
}
|
||||||
|
}
|
87
build/utils.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import { networkInterfaces } from 'os';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
|
||||||
|
export const isFunction = (arg: unknown): arg is (...args: any[]) => any =>
|
||||||
|
typeof arg === 'function';
|
||||||
|
export const isRegExp = (arg: unknown): arg is RegExp =>
|
||||||
|
Object.prototype.toString.call(arg) === '[object RegExp]';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Read all files in the specified folder, filter through regular rules, and return file path array
|
||||||
|
* @param root Specify the folder path
|
||||||
|
* [@param] reg Regular expression for filtering files, optional parameters
|
||||||
|
* Note: It can also be deformed to check whether the file path conforms to regular rules. The path can be a folder or a file. The path that does not exist is also fault-tolerant.
|
||||||
|
*/
|
||||||
|
export function readAllFile(root: string, reg: RegExp) {
|
||||||
|
let resultArr: string[] = [];
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(root)) {
|
||||||
|
const stat = fs.lstatSync(root);
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
// dir
|
||||||
|
const files = fs.readdirSync(root);
|
||||||
|
files.forEach(function (file) {
|
||||||
|
const t = readAllFile(root + '/' + file, reg);
|
||||||
|
resultArr = resultArr.concat(t);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (reg !== undefined) {
|
||||||
|
if (isFunction(reg.test) && reg.test(root)) {
|
||||||
|
resultArr.push(root);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resultArr.push(root);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {}
|
||||||
|
|
||||||
|
return resultArr;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getIPAddress() {
|
||||||
|
let interfaces = networkInterfaces();
|
||||||
|
for (let devName in interfaces) {
|
||||||
|
let iFace = interfaces[devName];
|
||||||
|
if (!iFace) return;
|
||||||
|
for (let i = 0; i < iFace.length; i++) {
|
||||||
|
let alias = iFace[i];
|
||||||
|
if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) {
|
||||||
|
return alias.address;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isDevFn(): boolean {
|
||||||
|
return process.env.NODE_ENV === 'development';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isProdFn(): boolean {
|
||||||
|
return process.env.NODE_ENV === 'production';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isReportMode(): boolean {
|
||||||
|
return process.env.REPORT === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadEnv() {
|
||||||
|
const env = process.env.NODE_ENV;
|
||||||
|
const ret: any = {};
|
||||||
|
const envList = [`.env.${env}.local`, `.env.${env}`, '.env.local', '.env', ,];
|
||||||
|
envList.forEach((e) => {
|
||||||
|
dotenv.config({
|
||||||
|
path: e,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const envName of Object.keys(process.env)) {
|
||||||
|
const realName = (process.env as any)[envName].replace(/\\n/g, '\n');
|
||||||
|
ret[envName] = realName;
|
||||||
|
process.env[envName] = realName;
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
55
commitlint.config.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
module.exports = {
|
||||||
|
ignores: [(commit) => commit.includes('init')],
|
||||||
|
extends: ['@commitlint/config-conventional'],
|
||||||
|
parserPreset: {
|
||||||
|
parserOpts: {
|
||||||
|
headerPattern: /^(\w*|[\u4e00-\u9fa5]*)(?:[\(\(](.*)[\)\)])?[\:\:] (.*)/,
|
||||||
|
headerCorrespondence: ['type', 'scope', 'subject'],
|
||||||
|
referenceActions: [
|
||||||
|
'close',
|
||||||
|
'closes',
|
||||||
|
'closed',
|
||||||
|
'fix',
|
||||||
|
'fixes',
|
||||||
|
'fixed',
|
||||||
|
'resolve',
|
||||||
|
'resolves',
|
||||||
|
'resolved',
|
||||||
|
],
|
||||||
|
issuePrefixes: ['#'],
|
||||||
|
noteKeywords: ['BREAKING CHANGE', '不兼容变更'],
|
||||||
|
fieldPattern: /^-(.*?)-$/,
|
||||||
|
revertPattern: /^Revert\s"([\s\S]*)"\s*This reverts commit (\w*)\./,
|
||||||
|
revertCorrespondence: ['header', 'hash'],
|
||||||
|
warn() {},
|
||||||
|
mergePattern: null,
|
||||||
|
mergeCorrespondence: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'body-leading-blank': [2, 'always'],
|
||||||
|
'footer-leading-blank': [1, 'always'],
|
||||||
|
'header-max-length': [2, 'always', 108],
|
||||||
|
'subject-empty': [2, 'never'],
|
||||||
|
'type-empty': [2, 'never'],
|
||||||
|
'type-enum': [
|
||||||
|
2,
|
||||||
|
'always',
|
||||||
|
[
|
||||||
|
'feat',
|
||||||
|
'fix',
|
||||||
|
'perf',
|
||||||
|
'style',
|
||||||
|
'docs',
|
||||||
|
'test',
|
||||||
|
'refactor',
|
||||||
|
'build',
|
||||||
|
'ci',
|
||||||
|
'chore',
|
||||||
|
'revert',
|
||||||
|
'wip',
|
||||||
|
'workflow',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
13
index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vue Vben admin 2.0</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
8
lint-staged.config.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
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': ['prettier --write', 'stylelint --fix', 'git add .'],
|
||||||
|
'*.{scss,less,styl,css,html}': ['stylelint --fix', 'prettier --write', 'git add .'],
|
||||||
|
'*.md': ['prettier --write'],
|
||||||
|
};
|
7
mock/_createProductionServer.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer';
|
||||||
|
import userMock from './sys/user';
|
||||||
|
import menuMock from './sys/menu';
|
||||||
|
|
||||||
|
export function setupProdMockServer() {
|
||||||
|
createProdMockServer([...userMock, ...menuMock]);
|
||||||
|
}
|
38
mock/_util.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
// Interface data format used to return a unified format
|
||||||
|
|
||||||
|
export function resultSuccess<T = any>(result: T, { message = 'ok' } = {}) {
|
||||||
|
return {
|
||||||
|
code: 0,
|
||||||
|
result,
|
||||||
|
message,
|
||||||
|
type: 'success',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resultPageSuccess<T = any>(items: T[], total: number, { message = 'ok' } = {}) {
|
||||||
|
return {
|
||||||
|
code: 0,
|
||||||
|
result: {
|
||||||
|
items,
|
||||||
|
total,
|
||||||
|
},
|
||||||
|
message,
|
||||||
|
type: 'success',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resultError(message = 'Request failed', { code = -1, result = null } = {}) {
|
||||||
|
return {
|
||||||
|
code,
|
||||||
|
result,
|
||||||
|
message,
|
||||||
|
type: 'error',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pagination<T = any>(pageNo: number, pageSize: number, array: T[]): T[] {
|
||||||
|
let offset = (pageNo - 1) * pageSize;
|
||||||
|
return offset + pageSize >= array.length
|
||||||
|
? array.slice(offset, array.length)
|
||||||
|
: array.slice(offset, offset + pageSize);
|
||||||
|
}
|
132
mock/sys/menu.ts
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import { resultSuccess } from '../_util';
|
||||||
|
import { MockMethod } from 'vite-plugin-mock';
|
||||||
|
|
||||||
|
const dashboardRoute = {
|
||||||
|
layout: {
|
||||||
|
path: '/dashboard',
|
||||||
|
name: 'Dashboard',
|
||||||
|
component: 'PAGE_LAYOUT',
|
||||||
|
redirect: '/dashboard/welcome',
|
||||||
|
meta: {
|
||||||
|
icon: 'ant-design:home-outlined',
|
||||||
|
title: 'Dashboard',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/welcome',
|
||||||
|
name: 'Welcome',
|
||||||
|
component: '/dashboard/welcome/index.vue',
|
||||||
|
meta: {
|
||||||
|
title: '欢迎页',
|
||||||
|
affix: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const frontRoute = {
|
||||||
|
path: '/front',
|
||||||
|
name: 'PermissionFrontDemo',
|
||||||
|
meta: {
|
||||||
|
title: '基于前端权限',
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'page',
|
||||||
|
component: '/demo/permission/front/index.vue',
|
||||||
|
meta: {
|
||||||
|
title: '页面权限',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'btn',
|
||||||
|
component: '/demo/permission/front/Btn.vue',
|
||||||
|
meta: {
|
||||||
|
title: '按钮权限',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'auth-pageA',
|
||||||
|
component: '/demo/permission/front/AuthPageA.vue',
|
||||||
|
meta: {
|
||||||
|
title: '权限测试页A',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'auth-pageB',
|
||||||
|
component: '/demo/permission/front/AuthPageB.vue',
|
||||||
|
meta: {
|
||||||
|
title: '权限测试页B',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const backRoute = {
|
||||||
|
path: '/back',
|
||||||
|
name: 'PermissionBackDemo',
|
||||||
|
meta: {
|
||||||
|
title: '基于后台权限',
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'page',
|
||||||
|
component: 'demo/permission/back/index.vue',
|
||||||
|
meta: {
|
||||||
|
title: '页面权限',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'btn',
|
||||||
|
component: '/demo/permission/back/Btn.vue',
|
||||||
|
meta: {
|
||||||
|
title: '按钮权限',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const authRoute = {
|
||||||
|
layout: {
|
||||||
|
path: '/permission',
|
||||||
|
name: 'Permission',
|
||||||
|
component: 'PAGE_LAYOUT',
|
||||||
|
redirect: '/permission/front/page',
|
||||||
|
meta: {
|
||||||
|
icon: 'ant-design:home-outlined',
|
||||||
|
title: '权限管理',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
routes: [frontRoute, backRoute],
|
||||||
|
};
|
||||||
|
|
||||||
|
const authRoute1 = {
|
||||||
|
layout: {
|
||||||
|
path: '/permission',
|
||||||
|
name: 'Permission',
|
||||||
|
component: 'PAGE_LAYOUT',
|
||||||
|
redirect: '/permission/front/page',
|
||||||
|
meta: {
|
||||||
|
icon: 'ant-design:home-outlined',
|
||||||
|
title: '权限管理',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
routes: [backRoute],
|
||||||
|
};
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
url: '/api/getMenuListById',
|
||||||
|
timeout: 1000,
|
||||||
|
method: 'get',
|
||||||
|
response: ({ query }) => {
|
||||||
|
const { id } = query;
|
||||||
|
if (!id || id === '1') {
|
||||||
|
return resultSuccess([dashboardRoute, authRoute]);
|
||||||
|
}
|
||||||
|
if (id === '2') {
|
||||||
|
return resultSuccess([dashboardRoute, authRoute1]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as MockMethod[];
|
90
mock/sys/user.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import { MockMethod } from 'vite-plugin-mock';
|
||||||
|
import { resultError, resultSuccess } from '../_util';
|
||||||
|
|
||||||
|
function createFakeUserList() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
userId: '1',
|
||||||
|
username: 'vben',
|
||||||
|
realName: 'Vben',
|
||||||
|
desc: 'manager',
|
||||||
|
password: '123456',
|
||||||
|
token: 'fakeToken1',
|
||||||
|
role: {
|
||||||
|
roleName: 'Super Admin',
|
||||||
|
value: 'super',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userId: '2',
|
||||||
|
username: 'test',
|
||||||
|
password: '123456',
|
||||||
|
realName: 'test user',
|
||||||
|
desc: 'tester',
|
||||||
|
token: 'fakeToken2',
|
||||||
|
role: {
|
||||||
|
roleName: 'Tester',
|
||||||
|
value: 'test',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const fakeCodeList: any = {
|
||||||
|
'1': ['1000', '3000', '5000'],
|
||||||
|
|
||||||
|
'2': ['2000', '4000', '6000'],
|
||||||
|
};
|
||||||
|
export default [
|
||||||
|
// mock user login
|
||||||
|
{
|
||||||
|
url: '/api/login',
|
||||||
|
timeout: 1000,
|
||||||
|
method: 'post',
|
||||||
|
response: ({ body }) => {
|
||||||
|
const { username, password } = body;
|
||||||
|
const checkUser = createFakeUserList().find(
|
||||||
|
(item) => item.username === username && password === item.password
|
||||||
|
);
|
||||||
|
if (!checkUser) {
|
||||||
|
return resultError('Incorrect account or password!');
|
||||||
|
}
|
||||||
|
const { userId, username: _username, token, realName, desc, role } = checkUser;
|
||||||
|
return resultSuccess({
|
||||||
|
role,
|
||||||
|
userId,
|
||||||
|
username: _username,
|
||||||
|
token,
|
||||||
|
realName,
|
||||||
|
desc,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: '/api/getUserInfoById',
|
||||||
|
timeout: 200,
|
||||||
|
method: 'get',
|
||||||
|
response: ({ query }) => {
|
||||||
|
const { userId } = query;
|
||||||
|
const checkUser = createFakeUserList().find((item) => item.userId === userId);
|
||||||
|
if (!checkUser) {
|
||||||
|
return resultError('The corresponding user information was not obtained!');
|
||||||
|
}
|
||||||
|
return resultSuccess(checkUser);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: '/api/getPermCodeByUserId',
|
||||||
|
timeout: 200,
|
||||||
|
method: 'get',
|
||||||
|
response: ({ query }) => {
|
||||||
|
const { userId } = query;
|
||||||
|
if (!userId) {
|
||||||
|
return resultError('userId is not null!');
|
||||||
|
}
|
||||||
|
const codeList = fakeCodeList[userId];
|
||||||
|
|
||||||
|
return resultSuccess(codeList);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as MockMethod[];
|
104
package.json
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
{
|
||||||
|
"name": "vben-admin-2.0",
|
||||||
|
"version": "2.0.0-beta.1",
|
||||||
|
"scripts": {
|
||||||
|
"bootstrap": "yarn install",
|
||||||
|
"serve": "ts-node --project ./build/tsconfig.json ./build/script/preserve && cross-env NODE_ENV=development vite",
|
||||||
|
"build": "cross-env NODE_ENV=production vite build ",
|
||||||
|
"report": "cross-env REPORT=true yarn build ",
|
||||||
|
"build:no-cache": "yarn clean:cache && yarn build",
|
||||||
|
"preview": "ts-node --project ./build/tsconfig.json ./build/script/preview",
|
||||||
|
"log": "ts-node --project ./build/tsconfig.json ./build/script/changelog",
|
||||||
|
"gen:gz": "ts-node --project build/tsconfig.build.json ./build/gzip/index.ts ",
|
||||||
|
"clean:cache": "npx rimraf node_modules/.cache/ && npx rimraf node_modules/.vite_opt_cache",
|
||||||
|
"clean:lib": "npx rimraf node_modules",
|
||||||
|
"ls-lint": "npx ls-lint",
|
||||||
|
"lint:eslint": "eslint --fix --ext \"src/**/*.{vue,less,css,scss}\"",
|
||||||
|
"lint:prettier": "prettier --write --loglevel warn \"src/**/*.{js,json,tsx,css,less,scss,vue,html,md}\"",
|
||||||
|
"lint:stylelint": "stylelint --fix \"**/*.{vue,less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/",
|
||||||
|
"reinstall": "npx rimraf node_modules && npx rimraf yarn.lock && npx rimraf package.lock.json && yarn run bootstrap",
|
||||||
|
"postinstall": "ts-node --project ./build/tsconfig.json ./build/script/postinstall"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@iconify/iconify": "^2.0.0-rc.1",
|
||||||
|
"ant-design-vue": "^2.0.0-beta.10",
|
||||||
|
"axios": "^0.20.0",
|
||||||
|
"lodash-es": "^4.17.15",
|
||||||
|
"mockjs": "^1.1.0",
|
||||||
|
"nprogress": "^0.2.0",
|
||||||
|
"path-to-regexp": "^6.1.0",
|
||||||
|
"qrcode": "^1.4.4",
|
||||||
|
"vue": "^3.0.0",
|
||||||
|
"vue-i18n": "^9.0.0-beta.3",
|
||||||
|
"vue-router": "^4.0.0-beta.12",
|
||||||
|
"vuex": "^4.0.0-beta.4",
|
||||||
|
"vuex-module-decorators": "^1.0.1",
|
||||||
|
"zxcvbn": "^4.4.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@commitlint/cli": "^11.0.0",
|
||||||
|
"@commitlint/config-conventional": "^11.0.0",
|
||||||
|
"@iconify/json": "^1.1.233",
|
||||||
|
"@ls-lint/ls-lint": "^1.9.2",
|
||||||
|
"@purge-icons/generated": "^0.4.1",
|
||||||
|
"@types/fs-extra": "^9.0.1",
|
||||||
|
"@types/inquirer": "^7.3.1",
|
||||||
|
"@types/koa-static": "^4.0.1",
|
||||||
|
"@types/lodash-es": "^4.17.3",
|
||||||
|
"@types/mockjs": "^1.0.3",
|
||||||
|
"@types/nprogress": "^0.2.0",
|
||||||
|
"@types/qrcode": "^1.3.5",
|
||||||
|
"@types/rollup-plugin-visualizer": "^2.6.0",
|
||||||
|
"@types/shelljs": "^0.8.8",
|
||||||
|
"@types/zxcvbn": "^4.4.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^4.2.0",
|
||||||
|
"@typescript-eslint/parser": "^4.2.0",
|
||||||
|
"@vue/compiler-sfc": "^3.0.0",
|
||||||
|
"autoprefixer": "^9.8.6",
|
||||||
|
"babel-plugin-import": "^1.13.0",
|
||||||
|
"commitizen": "^4.2.1",
|
||||||
|
"conventional-changelog-cli": "^2.1.0",
|
||||||
|
"cross-env": "^7.0.2",
|
||||||
|
"dotenv": "^8.2.0",
|
||||||
|
"eslint": "^7.10.0",
|
||||||
|
"eslint-config-prettier": "^6.12.0",
|
||||||
|
"eslint-plugin-prettier": "^3.1.4",
|
||||||
|
"eslint-plugin-vue": "^7.0.0-beta.4",
|
||||||
|
"fs-extra": "^9.0.1",
|
||||||
|
"husky": "^4.3.0",
|
||||||
|
"inquirer": "^7.3.3",
|
||||||
|
"koa-static": "^5.0.0",
|
||||||
|
"less": "^3.12.2",
|
||||||
|
"lint-staged": "^10.4.0",
|
||||||
|
"ora": "^5.1.0",
|
||||||
|
"portfinder": "^1.0.28",
|
||||||
|
"postcss-import": "^12.0.1",
|
||||||
|
"prettier": "^2.1.2",
|
||||||
|
"rimraf": "^3.0.2",
|
||||||
|
"rollup-plugin-analyzer": "^3.3.0",
|
||||||
|
"rollup-plugin-visualizer": "^4.1.1",
|
||||||
|
"shelljs": "^0.8.4",
|
||||||
|
"stylelint": "^13.7.2",
|
||||||
|
"stylelint-config-prettier": "^8.0.2",
|
||||||
|
"stylelint-config-standard": "^20.0.0",
|
||||||
|
"stylelint-order": "^4.1.0",
|
||||||
|
"tailwindcss": "^1.8.10",
|
||||||
|
"tasksfile": "^5.1.1",
|
||||||
|
"ts-node": "^9.0.0",
|
||||||
|
"typescript": "^4.0.3",
|
||||||
|
"vite": "^1.0.0-rc.4",
|
||||||
|
"vite-jsx": "^1.0.5",
|
||||||
|
"vite-plugin-mock": "^1.0.2",
|
||||||
|
"vite-plugin-purge-icons": "^0.4.1",
|
||||||
|
"vue-eslint-parser": "^7.1.0"
|
||||||
|
},
|
||||||
|
"husky": {
|
||||||
|
"hooks": {
|
||||||
|
"pre-commit": "ls-lint && lint-staged",
|
||||||
|
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
}
|
4
postcss.config.js
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
const path = require('path');
|
||||||
|
module.exports = {
|
||||||
|
plugins: [require('tailwindcss'), require('autoprefixer'), require('postcss-import')],
|
||||||
|
};
|
28
prettier.config.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
module.exports = {
|
||||||
|
printWidth: 100,
|
||||||
|
tabWidth: 2,
|
||||||
|
useTabs: false,
|
||||||
|
semi: true,
|
||||||
|
vueIndentScriptAndStyle: true,
|
||||||
|
singleQuote: true,
|
||||||
|
quoteProps: 'as-needed',
|
||||||
|
bracketSpacing: true,
|
||||||
|
trailingComma: 'es5',
|
||||||
|
jsxBracketSameLine: false,
|
||||||
|
jsxSingleQuote: false,
|
||||||
|
arrowParens: 'always',
|
||||||
|
insertPragma: false,
|
||||||
|
requirePragma: false,
|
||||||
|
proseWrap: 'never',
|
||||||
|
htmlWhitespaceSensitivity: 'strict',
|
||||||
|
endOfLine: 'lf',
|
||||||
|
rangeStart: 0,
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: '*.md',
|
||||||
|
options: {
|
||||||
|
tabWidth: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
BIN
public/favicon.ico
Normal file
After Width: | Height: | Size: 894 B |
42
src/App.vue
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<template>
|
||||||
|
<ConfigProvider
|
||||||
|
:locale="zhCN"
|
||||||
|
:renderEmpty="renderEmpty"
|
||||||
|
:transformCellText="transformCellText"
|
||||||
|
v-bind="lockOn"
|
||||||
|
>
|
||||||
|
<router-view />
|
||||||
|
</ConfigProvider>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import { ConfigProvider } from 'ant-design-vue';
|
||||||
|
import { createBreakpointListen } from '/@/hooks/event/useBreakpoint';
|
||||||
|
|
||||||
|
import zhCN from 'ant-design-vue/es/locale/zh_CN';
|
||||||
|
import moment from 'moment';
|
||||||
|
import 'moment/locale/zh-cn';
|
||||||
|
|
||||||
|
import { useConfigProvider, useInitAppConfigStore, useListenerNetWork } from './useApp';
|
||||||
|
import { useLockPage } from '/@/hooks/web/useLockPage';
|
||||||
|
moment.locale('zh-cn');
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'App',
|
||||||
|
components: { ConfigProvider },
|
||||||
|
setup() {
|
||||||
|
useInitAppConfigStore();
|
||||||
|
useListenerNetWork();
|
||||||
|
createBreakpointListen();
|
||||||
|
const { renderEmpty, transformCellText } = useConfigProvider();
|
||||||
|
const { on: lockOn } = useLockPage();
|
||||||
|
|
||||||
|
return {
|
||||||
|
renderEmpty,
|
||||||
|
transformCellText,
|
||||||
|
zhCN,
|
||||||
|
lockOn,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
18
src/api/sys/menu.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { defHttp } from '/@/utils/http/axios';
|
||||||
|
|
||||||
|
import { getMenuListByIdParams, getMenuListByIdParamsResultModel } from './model/menuModel';
|
||||||
|
|
||||||
|
enum Api {
|
||||||
|
GetMenuListById = '/getMenuListById',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 根据id获取用户菜单
|
||||||
|
*/
|
||||||
|
export function getMenuListById(params: getMenuListByIdParams) {
|
||||||
|
return defHttp.request<getMenuListByIdParamsResultModel>({
|
||||||
|
url: Api.GetMenuListById,
|
||||||
|
method: 'GET',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
23
src/api/sys/model/menuModel.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { RouteMeta } from '/@/router/types';
|
||||||
|
export interface RouteItem {
|
||||||
|
path: string;
|
||||||
|
component: any;
|
||||||
|
meta: RouteMeta;
|
||||||
|
name?: string;
|
||||||
|
alias?: string | string[];
|
||||||
|
redirect?: string;
|
||||||
|
caseSensitive?: boolean;
|
||||||
|
children?: RouteItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 获取菜单接口
|
||||||
|
*/
|
||||||
|
export interface getMenuListByIdParams {
|
||||||
|
id: number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 获取菜单返回值
|
||||||
|
*/
|
||||||
|
export type getMenuListByIdParamsResultModel = RouteItem[];
|
43
src/api/sys/model/userModel.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* @description: Login interface parameters
|
||||||
|
*/
|
||||||
|
export interface LoginParams {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: Get user information
|
||||||
|
*/
|
||||||
|
export interface GetUserInfoByUserIdParams {
|
||||||
|
userId: string | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RoleInfo {
|
||||||
|
roleName: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: Login interface return value
|
||||||
|
*/
|
||||||
|
export interface LoginResultModel {
|
||||||
|
userId: string | number;
|
||||||
|
token: string;
|
||||||
|
role: RoleInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: Get user information return value
|
||||||
|
*/
|
||||||
|
export interface GetUserInfoByUserIdModel {
|
||||||
|
role: RoleInfo;
|
||||||
|
// 用户id
|
||||||
|
userId: string | number;
|
||||||
|
// 用户名
|
||||||
|
username: string;
|
||||||
|
// 真实名字
|
||||||
|
realName: string;
|
||||||
|
// 介绍
|
||||||
|
desc?: string;
|
||||||
|
}
|
48
src/api/sys/user.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { defHttp } from '/@/utils/http/axios';
|
||||||
|
import {
|
||||||
|
LoginParams,
|
||||||
|
LoginResultModel,
|
||||||
|
GetUserInfoByUserIdParams,
|
||||||
|
GetUserInfoByUserIdModel,
|
||||||
|
} from './model/userModel';
|
||||||
|
|
||||||
|
enum Api {
|
||||||
|
Login = '/login',
|
||||||
|
GetUserInfoById = '/getUserInfoById',
|
||||||
|
GetPermCodeByUserId = '/getPermCodeByUserId',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: user login api
|
||||||
|
*/
|
||||||
|
export function loginApi(params: LoginParams) {
|
||||||
|
return defHttp.request<LoginResultModel>(
|
||||||
|
{
|
||||||
|
url: Api.Login,
|
||||||
|
method: 'POST',
|
||||||
|
params,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
errorMessageMode: 'modal',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: getUserInfoById
|
||||||
|
*/
|
||||||
|
export function getUserInfoById(params: GetUserInfoByUserIdParams) {
|
||||||
|
return defHttp.request<GetUserInfoByUserIdModel>({
|
||||||
|
url: Api.GetUserInfoById,
|
||||||
|
method: 'GET',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPermCodeByUserId(params: GetUserInfoByUserIdParams) {
|
||||||
|
return defHttp.request<string[]>({
|
||||||
|
url: Api.GetPermCodeByUserId,
|
||||||
|
method: 'GET',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
BIN
src/assets/images/dashboard/wokb/approve.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
src/assets/images/dashboard/wokb/attendance.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
src/assets/images/dashboard/wokb/datashow1.png
Normal file
After Width: | Height: | Size: 5.0 KiB |
BIN
src/assets/images/dashboard/wokb/datashow2.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
src/assets/images/dashboard/wokb/datashow3.png
Normal file
After Width: | Height: | Size: 4.9 KiB |
BIN
src/assets/images/dashboard/wokb/datashow4.png
Normal file
After Width: | Height: | Size: 5.5 KiB |
BIN
src/assets/images/dashboard/wokb/leave.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
src/assets/images/dashboard/wokb/meal.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
src/assets/images/dashboard/wokb/overtime.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
src/assets/images/dashboard/wokb/performance.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
src/assets/images/dashboard/wokb/stamp.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
src/assets/images/dashboard/wokb/travel.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
src/assets/images/dashboard/wokb/wokb.png
Normal file
After Width: | Height: | Size: 190 KiB |
BIN
src/assets/images/exception/404.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
src/assets/images/exception/500.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
src/assets/images/exception/net-work.png
Normal file
After Width: | Height: | Size: 9.6 KiB |
BIN
src/assets/images/header.jpg
Normal file
After Width: | Height: | Size: 1.0 MiB |
39
src/assets/images/layout/menu-mix.svg
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="52px" height="45px" viewBox="0 0 52 45" version="1.1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<defs>
|
||||||
|
<filter x="-9.4%" y="-6.2%" width="118.8%" height="122.5%" filterUnits="objectBoundingBox" id="filter-1">
|
||||||
|
<feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
|
||||||
|
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
||||||
|
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
|
||||||
|
<feMergeNode in="SourceGraphic"></feMergeNode>
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
<rect id="path-2" x="0" y="0" width="48" height="40" rx="4"></rect>
|
||||||
|
<filter x="-4.2%" y="-2.5%" width="108.3%" height="110.0%" filterUnits="objectBoundingBox" id="filter-4">
|
||||||
|
<feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
|
||||||
|
<feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
||||||
|
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<g id="配置面板" width="48" height="40" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g id="setting-copy-2" width="48" height="40" transform="translate(-1190.000000, -136.000000)">
|
||||||
|
<g id="Group-8" width="48" height="40" transform="translate(1167.000000, 0.000000)">
|
||||||
|
<g id="Group-5-Copy-5" filter="url(#filter-1)" transform="translate(25.000000, 137.000000)">
|
||||||
|
<mask id="mask-3" fill="white">
|
||||||
|
<use xlink:href="#path-2"></use>
|
||||||
|
</mask>
|
||||||
|
<g id="Rectangle-18">
|
||||||
|
<use fill="black" fill-opacity="1" filter="url(#filter-4)" xlink:href="#path-2"></use>
|
||||||
|
<use fill="#F0F2F5" fill-rule="evenodd" xlink:href="#path-2"></use>
|
||||||
|
</g>
|
||||||
|
<rect id="Rectangle-18" fill="#fff" mask="url(#mask-3)" x="0" y="0" width="16" height="40"></rect>
|
||||||
|
<rect id="Rectangle-11" fill="#303648" mask="url(#mask-3)" x="0" y="0" width="48" height="10"></rect>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.5 KiB |
39
src/assets/images/layout/menu-sidebar.svg
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="52px" height="45px" viewBox="0 0 52 45" version="1.1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<defs>
|
||||||
|
<filter x="-9.4%" y="-6.2%" width="118.8%" height="122.5%" filterUnits="objectBoundingBox" id="filter-1">
|
||||||
|
<feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
|
||||||
|
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
||||||
|
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
|
||||||
|
<feMergeNode in="SourceGraphic"></feMergeNode>
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
<rect id="path-2" x="0" y="0" width="48" height="40" rx="4"></rect>
|
||||||
|
<filter x="-4.2%" y="-2.5%" width="108.3%" height="110.0%" filterUnits="objectBoundingBox" id="filter-4">
|
||||||
|
<feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
|
||||||
|
<feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
||||||
|
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<g width="48" height="40" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g id="setting-copy-2" width="48" height="40" transform="translate(-1190.000000, -136.000000)">
|
||||||
|
<g id="Group-8" width="48" height="40" transform="translate(1167.000000, 0.000000)">
|
||||||
|
<g id="Group-5-Copy-5" filter="url(#filter-1)" transform="translate(25.000000, 137.000000)">
|
||||||
|
<mask id="mask-3" fill="white">
|
||||||
|
<use xlink:href="#path-2"></use>
|
||||||
|
</mask>
|
||||||
|
<g id="Rectangle-18">
|
||||||
|
<use fill="black" fill-opacity="1" filter="url(#filter-4)" xlink:href="#path-2"></use>
|
||||||
|
<use fill="#F0F2F5" fill-rule="evenodd" xlink:href="#path-2"></use>
|
||||||
|
</g>
|
||||||
|
<rect id="Rectangle-11" fill="#FFFFFF" mask="url(#mask-3)" x="0" y="0" width="48" height="10"></rect>
|
||||||
|
<rect id="Rectangle-18" fill="#303648" mask="url(#mask-3)" x="0" y="0" width="16" height="40"></rect>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.3 KiB |
39
src/assets/images/layout/menu-top.svg
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="52px" height="45px" viewBox="0 0 52 45" version="1.1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
|
||||||
|
<defs>
|
||||||
|
<filter x="-9.4%" y="-6.2%" width="118.8%" height="122.5%" filterUnits="objectBoundingBox" id="filter-1">
|
||||||
|
<feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
|
||||||
|
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
||||||
|
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
|
||||||
|
<feMergeNode in="SourceGraphic"></feMergeNode>
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
<rect id="path-2" x="0" y="0" width="48" height="40" rx="4"></rect>
|
||||||
|
<filter x="-4.2%" y="-2.5%" width="108.3%" height="110.0%" filterUnits="objectBoundingBox" id="filter-4">
|
||||||
|
<feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
|
||||||
|
<feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
||||||
|
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g id="setting-copy-2" transform="translate(-1254.000000, -337.000000)">
|
||||||
|
<g id="Group-8" transform="translate(1167.000000, 0.000000)">
|
||||||
|
<g id="Group-5" filter="url(#filter-1)" transform="translate(89.000000, 338.000000)">
|
||||||
|
<mask id="mask-3" fill="white">
|
||||||
|
<use xlink:href="#path-2"></use>
|
||||||
|
</mask>
|
||||||
|
<g id="Rectangle-18">
|
||||||
|
<use fill="black" fill-opacity="1" filter="url(#filter-4)" xlink:href="#path-2"></use>
|
||||||
|
<use fill="#F0F2F5" fill-rule="evenodd" xlink:href="#path-2"></use>
|
||||||
|
</g>
|
||||||
|
<rect id="Rectangle-11" fill="#303648" mask="url(#mask-3)" x="0" y="0" width="48" height="10"></rect>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
67
src/assets/images/loading.svg
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg viewBox="0 0 200 200" version="1.1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<style type="text/css">
|
||||||
|
.left-linear {
|
||||||
|
fill: url(#left-linear);
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-linear {
|
||||||
|
fill: url(#right-linear);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top {
|
||||||
|
fill: #64acff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom {
|
||||||
|
fill: #9dbfe4;
|
||||||
|
}
|
||||||
|
@keyframes load {
|
||||||
|
0% {
|
||||||
|
transform: rotate(-360deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.load {
|
||||||
|
animation: load 1.4s linear infinite;
|
||||||
|
transform-origin: center center;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip {
|
||||||
|
display: block;
|
||||||
|
min-width: 100px;
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #303133;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<circle cx="97" cy="97" r="81" stroke-width="16" stroke="#327fd8" fill="none"></circle>
|
||||||
|
<g class="load">
|
||||||
|
<!--右半圆环-->
|
||||||
|
<linearGradient id="left-linear" gradientUnits="userSpaceOnUse" x1="50" y1="0" x2="100" y2="180">
|
||||||
|
<stop offset="0" style="stop-color: #64acff;" />
|
||||||
|
<stop offset="1" style="stop-color: #9DBFE4;" />
|
||||||
|
</linearGradient>
|
||||||
|
<path class="left-linear" d="M20,100c0-44.1,35.9-80,80-80V0C44.8,0,0,44.8,0,100s44.8,100,100,100v-20C55.9,180,20,144.1,20,100z" />
|
||||||
|
<!--左半圆环-->
|
||||||
|
<circle class="bottom" cx="100" cy="190" r="10" />
|
||||||
|
<linearGradient id="right-linear" gradientUnits="userSpaceOnUse" x1="100" y1="120" x2="100" y2="180">
|
||||||
|
<stop offset="0" style="stop-color: transparent;" />
|
||||||
|
<stop offset="1" style="stop-color: transparent;" />
|
||||||
|
</linearGradient>
|
||||||
|
<path class="right-linear" d="M100,0v20c44.1,0,80,35.9,80,80c0,44.1-35.9,80-80,80v20c55.2,0,100-44.8,100-100S155.2,0,100,0z" />
|
||||||
|
<!--左半圆环-->
|
||||||
|
<circle class="top" cx="100" cy="10" r="10" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.0 KiB |
BIN
src/assets/images/lock-page.jpg
Normal file
After Width: | Height: | Size: 218 KiB |
BIN
src/assets/images/lock-page.png
Normal file
After Width: | Height: | Size: 372 KiB |
BIN
src/assets/images/login/login-bg.png
Normal file
After Width: | Height: | Size: 180 KiB |
BIN
src/assets/images/login/login-in.png
Normal file
After Width: | Height: | Size: 92 KiB |
BIN
src/assets/images/logo.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
src/assets/images/no-data.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
src/assets/images/page_null.png
Normal file
After Width: | Height: | Size: 9.9 KiB |
BIN
src/assets/images/qq.jpeg
Normal file
After Width: | Height: | Size: 110 KiB |
BIN
src/assets/images/sidebar/dark-mini.png
Normal file
After Width: | Height: | Size: 8.4 KiB |
BIN
src/assets/images/sidebar/dark.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
src/assets/images/sidebar/light-mini.png
Normal file
After Width: | Height: | Size: 8.7 KiB |
BIN
src/assets/images/sidebar/light.png
Normal file
After Width: | Height: | Size: 24 KiB |
1
src/assets/svg/preview/p-rotate.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1595306944988" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1820" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><defs><style type="text/css"></style></defs><path d="M1464.3 279.7" p-id="1821" fill="#ffffff"></path><path d="M512 960c-60.5 0-119.1-11.9-174.4-35.2-53.4-22.6-101.3-54.9-142.4-96s-73.4-89-96-142.4C75.9 631.1 64 572.5 64 512s11.9-119.1 35.2-174.4c22.6-53.4 54.9-101.3 96-142.4s89-73.4 142.4-96C392.9 75.9 451.5 64 512 64s119.1 11.9 174.4 35.2c53.4 22.6 101.3 54.9 142.4 96s73.4 89 96 142.4C948.1 392.9 960 451.5 960 512c0 19.1-15.5 34.6-34.6 34.6s-34.6-15.5-34.6-34.6c0-51.2-10-100.8-29.8-147.4-19.1-45.1-46.4-85.6-81.2-120.4C745 209.4 704.5 182 659.4 163c-46.7-19.7-96.3-29.8-147.4-29.8-51.2 0-100.8 10-147.4 29.8-45.1 19.1-85.6 46.4-120.4 81.2S182 319.5 163 364.6c-19.7 46.7-29.8 96.3-29.8 147.4 0 51.2 10 100.8 29.8 147.4 19.1 45.1 46.4 85.6 81.2 120.4C279 814.6 319.5 842 364.6 861c46.7 19.7 96.3 29.8 147.4 29.8 64.6 0 128.4-16.5 184.4-47.8 54.4-30.4 100.9-74.1 134.6-126.6 10.3-16.1 31.7-20.8 47.8-10.4 16.1 10.3 20.8 31.7 10.4 47.8-39.8 62-94.8 113.7-159.1 149.6-66.2 37-141.7 56.6-218.1 56.6z" p-id="1822" fill="#ffffff"></path><path d="M924 552c-19.8 0-36-16.2-36-36V228c0-19.8 16.2-36 36-36s36 16.2 36 36v288c0 19.8-16.2 36-36 36zM275.4 575.5c9.5-2.5 19.1 2.9 22.3 12.2 3.5 10.2 9.9 17.7 19.1 22.6 7.1 3.9 15.1 5.8 24 5.8 16.6 0 30.8-6.9 42.5-20.8 11.7-13.8 20-32.7 24.9-75.1-7.7 12.2-17.3 20.8-28.7 25.8-11.4 5-23.7 7.4-36.8 7.4-26.7 0-47.7-8.3-63.3-24.9-15.5-16.6-23.3-37.9-23.3-64.1 0-25.1 7.7-47.1 23-66.2 15.3-19 37.9-28.6 67.8-28.6 40.3 0 68.1 18.1 83.4 54.4 8.5 19.9 12.7 44.9 12.7 74.9 0 33.8-5.1 63.8-15.3 89.9-16.9 43.5-45.5 65.2-85.8 65.2-27 0-47.6-7.1-61.6-21.2-10-10.1-16.4-22-19.3-35.8-2-9.6 4-19.1 13.5-21.6l0.9 0.1z m103-74.4c9.4-7.5 14.1-20.6 14.1-39.3 0-16.8-4.2-29.3-12.7-37.5S360.6 412 347.5 412c-14 0-25.2 4.7-33.4 14.1-8.2 9.4-12.4 22-12.4 37.7 0 14.9 3.6 26.7 10.9 35.5 7.2 8.8 18.8 13.1 34.6 13.1 11.4 0 21.8-3.8 31.2-11.3zM646.6 414.4c12.4 22.8 18.5 54 18.5 93.7 0 37.6-5.6 68.7-16.8 93.3-16.2 35.3-42.8 52.9-79.6 52.9-33.2 0-57.9-14.4-74.2-43.3-13.5-24.1-20.3-56.4-20.3-97 0-31.4 4.1-58.4 12.2-80.9 15.2-42 42.7-63 82.5-63 35.9 0 61.8 14.8 77.7 44.3z m-40.2 173.3c9.4-13.9 14-39.9 14-78 0-27.4-3.4-50-10.1-67.7-6.8-17.7-19.9-26.6-39.4-26.6-17.9 0-31 8.4-39.3 25.2-8.3 16.8-12.4 41.6-12.4 74.3 0 24.6 2.6 44.4 7.9 59.4 8.1 22.8 22 34.3 41.6 34.3 15.7 0 28.3-7 37.7-20.9zM803.3 387.2c11.2 11.3 16.8 25 16.8 41.2 0 16.7-5.8 30.7-17.5 41.8C791 481.4 777.4 487 762 487c-17.1 0-31.2-5.8-42.1-17.4-10.9-11.6-16.4-25.1-16.4-40.6 0-16.5 5.8-30.4 17.3-41.7 11.5-11.3 25.3-17 41.2-17 16.3 0 30.1 5.7 41.3 16.9zM739.5 451c6.2 6.2 13.7 9.3 22.5 9.3 8.4 0 15.8-3.1 22.1-9.3 6.3-6.2 9.4-13.7 9.4-22.6 0-8.5-3.1-15.9-9.3-22.1-6.2-6.2-13.6-9.3-22.2-9.3s-16.1 3.1-22.4 9.3c-6.3 6.2-9.4 13.7-9.4 22.6-0.1 8.4 3 15.8 9.3 22.1z" p-id="1823" fill="#ffffff"></path></svg>
|
After Width: | Height: | Size: 3.0 KiB |
1
src/assets/svg/preview/resume.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1595307154239" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7317" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><defs><style type="text/css"></style></defs><path d="M316 672h60c4.4 0 8-3.6 8-8V360c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v304c0 4.4 3.6 8 8 8zM512 622c22.1 0 40-17.9 40-39 0-23.1-17.9-41-40-41s-40 17.9-40 41c0 21.1 17.9 39 40 39zM512 482c22.1 0 40-17.9 40-39 0-23.1-17.9-41-40-41s-40 17.9-40 41c0 21.1 17.9 39 40 39z" p-id="7318" fill="#ffffff"></path><path d="M880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32z m-40 728H184V184h656v656z" p-id="7319" fill="#ffffff"></path><path d="M648 672h60c4.4 0 8-3.6 8-8V360c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v304c0 4.4 3.6 8 8 8z" p-id="7320" fill="#ffffff"></path></svg>
|
After Width: | Height: | Size: 995 B |
1
src/assets/svg/preview/scale.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1595307195033" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8116" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><defs><style type="text/css"></style></defs><path d="M887.081 904.791a25.8 25.8 0 0 1-18.376-7.619L705.618 734.075l-4.163 3.369c-58.255 47.18-131.522 73.16-206.32 73.16-181.07 0-328.377-147.308-328.377-328.367 0-181.068 147.308-328.376 328.377-328.376 181.063 0 328.376 147.308 328.376 328.376 0 77.072-27.412 152.07-77.169 211.17l-3.522 4.173 162.719 162.744a25.846 25.846 0 0 1 7.639 18.432 26.081 26.081 0 0 1-26.051 26.045l-0.046-0.01zM495.13 205.957c-152.336 0-276.27 123.935-276.27 276.27 0 152.33 123.934 276.27 276.27 276.27 152.34 0 276.275-123.94 276.275-276.27 0-152.335-123.935-276.27-276.275-276.27z" fill="#ffffff" p-id="8117"></path><path d="M626.545 508.355h-262.83a26.127 26.127 0 0 1 0-52.255h262.83a26.127 26.127 0 0 1 0 52.255z" fill="#ffffff" p-id="8118"></path><path d="M495.13 639.77a26.127 26.127 0 0 1-26.128-26.128v-262.83a26.127 26.127 0 0 1 52.255 0v262.835a26.127 26.127 0 0 1-26.127 26.123z" fill="#ffffff" p-id="8119"></path></svg>
|
After Width: | Height: | Size: 1.3 KiB |
1
src/assets/svg/preview/unrotate.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1595306911635" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1352" width="48" height="48" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css"></style></defs><path d="M924.8 337.6c-22.6-53.4-54.9-101.3-96-142.4s-89-73.4-142.4-96C631.1 75.9 572.5 64 512 64S392.9 75.9 337.6 99.2c-53.4 22.6-101.3 54.9-142.4 96-22.4 22.4-42.2 46.8-59.2 73.1V228c0-19.8-16.2-36-36-36s-36 16.2-36 36v288c0 19.8 16.2 36 36 36s36-16.2 36-36v-50.2c4.2-34.8 13.2-68.7 27-101.2 19.1-45.1 46.4-85.6 81.2-120.4C279 209.4 319.5 182 364.6 163c46.7-19.7 96.3-29.8 147.4-29.8 51.2 0 100.8 10 147.4 29.8 45.1 19.1 85.6 46.4 120.4 81.2C814.6 279 842 319.5 861 364.6c19.7 46.7 29.8 96.3 29.8 147.4 0 51.2-10 100.8-29.8 147.4-19.1 45.1-46.4 85.6-81.2 120.4C745 814.6 704.5 842 659.4 861c-46.7 19.7-96.3 29.8-147.4 29.8-64.6 0-128.4-16.5-184.4-47.8-54.4-30.4-100.9-74.1-134.6-126.6-10.3-16.1-31.7-20.8-47.8-10.4-16.1 10.3-20.8 31.7-10.4 47.8 39.8 62 94.8 113.7 159.1 149.6 66.2 37 141.7 56.6 218.1 56.6 60.5 0 119.1-11.9 174.4-35.2 53.4-22.6 101.3-54.9 142.4-96 41.1-41.1 73.4-89 96-142.4C948.1 631.1 960 572.5 960 512s-11.9-119.1-35.2-174.4z" p-id="1353" fill="#ffffff"></path><path d="M275.4 575.5c9.5-2.5 19.1 2.9 22.3 12.2 3.5 10.2 9.9 17.7 19.1 22.6 7.1 3.9 15.1 5.8 24 5.8 16.6 0 30.8-6.9 42.5-20.8 11.7-13.8 20-32.7 24.9-75.1-7.7 12.2-17.3 20.8-28.7 25.8-11.4 5-23.7 7.4-36.8 7.4-26.7 0-47.7-8.3-63.3-24.9-15.5-16.6-23.3-37.9-23.3-64.1 0-25.1 7.7-47.1 23-66.2 15.3-19 37.9-28.6 67.8-28.6 40.3 0 68.1 18.1 83.4 54.4 8.5 19.9 12.7 44.9 12.7 74.9 0 33.8-5.1 63.8-15.3 89.9-16.9 43.5-45.5 65.2-85.8 65.2-27 0-47.6-7.1-61.6-21.2-10-10.1-16.4-22-19.3-35.8-2-9.6 4-19.1 13.5-21.6l0.9 0.1z m103-74.4c9.4-7.5 14.1-20.6 14.1-39.3 0-16.8-4.2-29.3-12.7-37.5S360.6 412 347.5 412c-14 0-25.2 4.7-33.4 14.1-8.2 9.4-12.4 22-12.4 37.7 0 14.9 3.6 26.7 10.9 35.5 7.2 8.8 18.8 13.1 34.6 13.1 11.4 0 21.8-3.8 31.2-11.3zM646.6 414.4c12.4 22.8 18.5 54 18.5 93.7 0 37.6-5.6 68.7-16.8 93.3-16.2 35.3-42.8 52.9-79.6 52.9-33.2 0-57.9-14.4-74.2-43.3-13.5-24.1-20.3-56.4-20.3-97 0-31.4 4.1-58.4 12.2-80.9 15.2-42 42.7-63 82.5-63 35.9 0 61.8 14.8 77.7 44.3z m-40.2 173.3c9.4-13.9 14-39.9 14-78 0-27.4-3.4-50-10.1-67.7-6.8-17.7-19.9-26.6-39.4-26.6-17.9 0-31 8.4-39.3 25.2-8.3 16.8-12.4 41.6-12.4 74.3 0 24.6 2.6 44.4 7.9 59.4 8.1 22.8 22 34.3 41.6 34.3 15.7 0 28.3-7 37.7-20.9zM803.3 387.2c11.2 11.3 16.8 25 16.8 41.2 0 16.7-5.8 30.7-17.5 41.8C791 481.4 777.4 487 762 487c-17.1 0-31.2-5.8-42.1-17.4-10.9-11.6-16.4-25.1-16.4-40.6 0-16.5 5.8-30.4 17.3-41.7 11.5-11.3 25.3-17 41.2-17 16.3 0 30.1 5.7 41.3 16.9zM739.5 451c6.2 6.2 13.7 9.3 22.5 9.3 8.4 0 15.8-3.1 22.1-9.3 6.3-6.2 9.4-13.7 9.4-22.6 0-8.5-3.1-15.9-9.3-22.1-6.2-6.2-13.6-9.3-22.2-9.3s-16.1 3.1-22.4 9.3c-6.3 6.2-9.4 13.7-9.4 22.6-0.1 8.4 3 15.8 9.3 22.1z" p-id="1354" fill="#ffffff"></path></svg>
|
After Width: | Height: | Size: 2.9 KiB |
1
src/assets/svg/preview/unscale.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1595308005241" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9878" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><defs><style type="text/css"></style></defs><path d="M750.3 198.7C598 46.4 351.1 46.4 198.7 198.7s-152.3 399.2 0 551.5C345.1 896.6 578.8 902.3 732 767.3l172.1 172.1 35.4-35.4-172.1-171.9c135-153.2 129.3-387-17.1-533.4z m39.3 403.8c-17.1 42.1-42.2 80-74.7 112.4-32.5 32.5-70.3 57.6-112.4 74.7-40.7 16.5-83.8 24.9-128 24.9s-87.2-8.4-128-24.9c-42.1-17.1-80-42.2-112.4-74.7s-57.6-70.3-74.7-112.4c-16.5-40.7-24.9-83.8-24.9-128s8.4-87.2 24.9-128c17.1-42.1 42.2-80 74.7-112.4s70.3-57.6 112.4-74.7c40.7-16.5 83.8-24.9 128-24.9s87.2 8.4 128 24.9c42.1 17.1 80 42.2 112.4 74.7 32.5 32.5 57.6 70.3 74.7 112.4 16.5 40.7 24.9 83.8 24.9 128s-8.4 87.3-24.9 128zM671 502H271v-50h400v50z" fill="#ffffff" p-id="9879"></path></svg>
|
After Width: | Height: | Size: 1.0 KiB |
59
src/components/Authority/index.tsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { defineComponent, PropType, computed, unref } from 'vue';
|
||||||
|
|
||||||
|
import { PermissionModeEnum } from '/@/enums/appEnum';
|
||||||
|
import { RoleEnum } from '/@/enums/roleEnum';
|
||||||
|
import { usePermission } from '/@/hooks/web/usePermission';
|
||||||
|
import { appStore } from '/@/store/modules/app';
|
||||||
|
import { getSlot } from '/@/utils/helper/tsxHelper';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'Authority',
|
||||||
|
props: {
|
||||||
|
// 指定角色可见
|
||||||
|
value: {
|
||||||
|
type: [Number, Array, String] as PropType<RoleEnum | RoleEnum[]>,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props, { slots }) {
|
||||||
|
const getModeRef = computed(() => {
|
||||||
|
return appStore.getProjectConfig.permissionMode;
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* 渲染角色按钮
|
||||||
|
*/
|
||||||
|
function renderRoleAuth() {
|
||||||
|
const { value } = props;
|
||||||
|
if (!value) {
|
||||||
|
return getSlot(slots, 'default');
|
||||||
|
}
|
||||||
|
const { hasPermission } = usePermission();
|
||||||
|
return hasPermission(value) ? getSlot(slots, 'default') : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染编码按钮
|
||||||
|
* 这里只判断是否包含,具体实现可以根据项目自行写逻辑
|
||||||
|
*/
|
||||||
|
function renderCodeAuth() {
|
||||||
|
const { value } = props;
|
||||||
|
if (!value) {
|
||||||
|
return getSlot(slots, 'default');
|
||||||
|
}
|
||||||
|
const { hasPermission } = usePermission();
|
||||||
|
return hasPermission(value) ? getSlot(slots, 'default') : null;
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
const mode = unref(getModeRef);
|
||||||
|
// 基于角色渲染
|
||||||
|
if (mode === PermissionModeEnum.ROLE) {
|
||||||
|
return renderRoleAuth();
|
||||||
|
}
|
||||||
|
// 基于后台编码渲染
|
||||||
|
if (mode === PermissionModeEnum.BACK) {
|
||||||
|
return renderCodeAuth();
|
||||||
|
}
|
||||||
|
return getSlot(slots, 'default');
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
4
src/components/Basic/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export { default as BasicArrow } from './src/BasicArrow.vue';
|
||||||
|
export { default as BasicHelp } from './src/BasicHelp';
|
||||||
|
export { default as BasicTitle } from './src/BasicTitle.vue';
|
||||||
|
export { default as BasicEmpty } from './src/BasicEmpty.vue';
|
53
src/components/Basic/src/BasicArrow.vue
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<template>
|
||||||
|
<span :class="getClass">
|
||||||
|
<RightOutlined />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import type { PropType } from 'vue';
|
||||||
|
|
||||||
|
import { defineComponent, computed } from 'vue';
|
||||||
|
|
||||||
|
import { RightOutlined } from '@ant-design/icons-vue';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'BaseArrow',
|
||||||
|
components: { RightOutlined },
|
||||||
|
props: {
|
||||||
|
// Expand contract, expand by default
|
||||||
|
expand: {
|
||||||
|
type: Boolean as PropType<boolean>,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const getClass = computed(() => {
|
||||||
|
const preCls = 'base-arrow';
|
||||||
|
const cls = [preCls];
|
||||||
|
|
||||||
|
props.expand && cls.push(`${preCls}__active`);
|
||||||
|
return cls;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
getClass,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.base-arrow {
|
||||||
|
transform: rotate(-90deg) !important;
|
||||||
|
transition: all 0.3s ease 0.1s;
|
||||||
|
transform-origin: center center;
|
||||||
|
|
||||||
|
&.right {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__active {
|
||||||
|
transform: rotate(90deg) !important;
|
||||||
|
transition: all 0.3s ease 0.1s !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
28
src/components/Basic/src/BasicEmpty.vue
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<template>
|
||||||
|
<Empty :image="image" :description="description" />
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import { Empty } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import emptySrc from '/@/assets/images/page_null.png';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
extends: Empty as any,
|
||||||
|
components: { Empty },
|
||||||
|
props: {
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
default: '暂无内容',
|
||||||
|
},
|
||||||
|
image: {
|
||||||
|
type: String,
|
||||||
|
default: emptySrc,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
18
src/components/Basic/src/BasicHelp.less
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
@import (reference) '../../../design/index.less';
|
||||||
|
|
||||||
|
.base-help {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 14px;
|
||||||
|
color: @text-color-help-dark;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: @primary-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__wrap {
|
||||||
|
p {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
107
src/components/Basic/src/BasicHelp.tsx
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import type { PropType } from 'vue';
|
||||||
|
|
||||||
|
import { Tooltip } from 'ant-design-vue';
|
||||||
|
import { InfoCircleOutlined } from '@ant-design/icons-vue';
|
||||||
|
import { defineComponent, computed, unref } from 'vue';
|
||||||
|
|
||||||
|
import { getPopupContainer } from '/@/utils';
|
||||||
|
|
||||||
|
import { isString, isArray } from '/@/utils/is';
|
||||||
|
import { getSlot } from '/@/utils/helper/tsxHelper';
|
||||||
|
import './BasicHelp.less';
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'BaseHelp',
|
||||||
|
props: {
|
||||||
|
// max-width
|
||||||
|
maxWidth: {
|
||||||
|
type: String as PropType<string>,
|
||||||
|
default: '600px',
|
||||||
|
},
|
||||||
|
// Whether to display the serial number
|
||||||
|
showIndex: {
|
||||||
|
type: Boolean as PropType<boolean>,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
// Text list
|
||||||
|
text: {
|
||||||
|
type: [Array, String] as PropType<string[] | string>,
|
||||||
|
},
|
||||||
|
// color
|
||||||
|
color: {
|
||||||
|
type: String as PropType<string>,
|
||||||
|
default: '#ffffff',
|
||||||
|
},
|
||||||
|
fontSize: {
|
||||||
|
type: String as PropType<string>,
|
||||||
|
default: '14px',
|
||||||
|
},
|
||||||
|
absolute: {
|
||||||
|
type: Boolean as PropType<boolean>,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
// 定位
|
||||||
|
position: {
|
||||||
|
type: [Object] as PropType<any>,
|
||||||
|
default: () => ({
|
||||||
|
position: 'absolute',
|
||||||
|
left: 0,
|
||||||
|
bottom: 0,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props, { slots }) {
|
||||||
|
const getOverlayStyleRef = computed(() => {
|
||||||
|
return {
|
||||||
|
maxWidth: props.maxWidth,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const getWrapStyleRef = computed(() => {
|
||||||
|
return {
|
||||||
|
color: props.color,
|
||||||
|
fontSize: props.fontSize,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const getMainStyleRef = computed(() => {
|
||||||
|
return props.absolute ? props.position : {};
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 渲染内容
|
||||||
|
*/
|
||||||
|
const renderTitle = () => {
|
||||||
|
const list = props.text;
|
||||||
|
if (isString(list)) {
|
||||||
|
return <p>{list}</p>;
|
||||||
|
}
|
||||||
|
if (isArray(list)) {
|
||||||
|
return list.map((item, index) => {
|
||||||
|
return (
|
||||||
|
<p key={item}>
|
||||||
|
{props.showIndex ? `${index + 1}. ` : ''}
|
||||||
|
{item}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
return () => (
|
||||||
|
<Tooltip
|
||||||
|
title={(<div style={unref(getWrapStyleRef)}>{renderTitle()}</div>) as any}
|
||||||
|
placement="right"
|
||||||
|
overlayStyle={unref(getOverlayStyleRef)}
|
||||||
|
autoAdjustOverflow={true}
|
||||||
|
overlayClassName="base-help__wrap"
|
||||||
|
getPopupContainer={() => getPopupContainer()}
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
default: () => (
|
||||||
|
<span class="base-help" style={unref(getMainStyleRef)}>
|
||||||
|
{getSlot(slots) || <InfoCircleOutlined />}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
58
src/components/Basic/src/BasicTitle.vue
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<template>
|
||||||
|
<span class="base-title" :class="{ 'show-span': showSpan && $slots.default }">
|
||||||
|
<slot />
|
||||||
|
<BaseHelp class="base-title__help" v-if="helpMessage" :text="helpMessage" />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import type { PropType } from 'vue';
|
||||||
|
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'BaseTitle',
|
||||||
|
props: {
|
||||||
|
helpMessage: {
|
||||||
|
type: [String, Array] as PropType<string | string[]>,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
showSpan: {
|
||||||
|
type: Boolean as PropType<boolean>,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
@import (reference) '../../../design/index.less';
|
||||||
|
|
||||||
|
.base-title {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
padding-left: 7px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 24px;
|
||||||
|
color: @text-color-base;
|
||||||
|
|
||||||
|
.unselect();
|
||||||
|
|
||||||
|
&.show-span::before {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
left: 0;
|
||||||
|
width: 3px;
|
||||||
|
height: 16px;
|
||||||
|
margin-right: 4px;
|
||||||
|
background: @primary-color;
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
&__help {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
100
src/components/Breadcrumb/Breadcrumb.vue
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="breadcrumbRef" class="breadcrumb">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import type { PropType } from 'vue';
|
||||||
|
import { defineComponent, provide, ref } from 'vue';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'Breadcrumb',
|
||||||
|
props: {
|
||||||
|
separator: {
|
||||||
|
type: String as PropType<string>,
|
||||||
|
default: '/',
|
||||||
|
},
|
||||||
|
separatorClass: {
|
||||||
|
type: String as PropType<string>,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const breadcrumbRef = ref<Nullable<HTMLElement>>(null);
|
||||||
|
|
||||||
|
provide('breadcrumb', props);
|
||||||
|
|
||||||
|
return {
|
||||||
|
breadcrumbRef,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style lang="less">
|
||||||
|
@import (reference) '../../design/index.less';
|
||||||
|
|
||||||
|
.breadcrumb {
|
||||||
|
height: @header-height;
|
||||||
|
padding-right: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: @header-height;
|
||||||
|
// line-height: 1;
|
||||||
|
|
||||||
|
&::after,
|
||||||
|
&::before {
|
||||||
|
display: table;
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__separator {
|
||||||
|
margin: 0 9px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: @breadcrumb-item-normal-color;
|
||||||
|
|
||||||
|
&[class*='icon'] {
|
||||||
|
margin: 0 6px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__inner {
|
||||||
|
color: @breadcrumb-item-normal-color;
|
||||||
|
|
||||||
|
&.is-link,
|
||||||
|
a {
|
||||||
|
font-weight: 700;
|
||||||
|
color: @text-color-base;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover,
|
||||||
|
&.is-link:hover {
|
||||||
|
color: @primary-color;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item:last-child .breadcrumb__inner,
|
||||||
|
&__item:last-child &__inner a,
|
||||||
|
&__item:last-child &__inner a:hover,
|
||||||
|
&__item:last-child &__inner:hover {
|
||||||
|
font-weight: 400;
|
||||||
|
color: @breadcrumb-item-normal-color;
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item:last-child &__separator {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
62
src/components/Breadcrumb/BreadcrumbItem.vue
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<template>
|
||||||
|
<span class="breadcrumb__item">
|
||||||
|
<span ref="linkRef" :class="['breadcrumb__inner', to || isLink ? 'is-link' : '']">
|
||||||
|
<slot />
|
||||||
|
</span>
|
||||||
|
<i v-if="separatorClass" class="breadcrumb__separator" :class="separatorClass"></i>
|
||||||
|
<span v-else class="breadcrumb__separator">{{ separator }}</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, inject, ref, onMounted, unref } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { useEvent } from '/@/hooks/event/useEvent';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'BreadcrumbItem',
|
||||||
|
props: {
|
||||||
|
to: {
|
||||||
|
type: [String, Object],
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
replace: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
isLink: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const linkRef = ref<Nullable<HTMLElement>>(null);
|
||||||
|
const parent = inject('breadcrumb') as {
|
||||||
|
separator: string;
|
||||||
|
separatorClass: string;
|
||||||
|
};
|
||||||
|
const { push, replace } = useRouter();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const link = unref(linkRef);
|
||||||
|
if (!link) return;
|
||||||
|
useEvent({
|
||||||
|
el: link,
|
||||||
|
listener: () => {
|
||||||
|
const { to } = props;
|
||||||
|
if (!props.to) return;
|
||||||
|
props.replace ? replace(to) : push(to);
|
||||||
|
},
|
||||||
|
name: 'click',
|
||||||
|
wait: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
linkRef,
|
||||||
|
separator: parent.separator && parent.separator,
|
||||||
|
separatorClass: parent.separatorClass && parent.separatorClass,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
88
src/components/Button/index.vue
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
<template>
|
||||||
|
<Button v-bind="getBindValue" :class="[getColor, $attrs.class]">
|
||||||
|
<template v-slot:[item] v-for="item in Object.keys($slots)">
|
||||||
|
<slot :name="item" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { PropType } from 'vue';
|
||||||
|
|
||||||
|
import { defineComponent, computed, unref } from 'vue';
|
||||||
|
import { Button } from 'ant-design-vue';
|
||||||
|
// import { extendSlots } from '/@/utils/helper/tsxHelper';
|
||||||
|
import { useThrottle } from '/@/hooks/core/useThrottle';
|
||||||
|
import { isFunction } from '/@/utils/is';
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'AButton',
|
||||||
|
inheritAttrs: false,
|
||||||
|
components: { Button },
|
||||||
|
props: {
|
||||||
|
// 按钮类型
|
||||||
|
type: {
|
||||||
|
type: String as PropType<'primary' | 'default' | 'danger' | 'dashed' | 'link'>,
|
||||||
|
default: 'default',
|
||||||
|
},
|
||||||
|
// 节流防抖类型 throttle debounce
|
||||||
|
throttle: {
|
||||||
|
type: String as PropType<'throttle' | 'debounce'>,
|
||||||
|
default: 'throttle',
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: String as PropType<'error' | 'warning' | 'success'>,
|
||||||
|
},
|
||||||
|
// 防抖节流时间
|
||||||
|
throttleTime: {
|
||||||
|
type: Number as PropType<number>,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean as PropType<boolean>,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean as PropType<boolean>,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props, { attrs }) {
|
||||||
|
const getListeners = computed(() => {
|
||||||
|
const { throttle, throttleTime = 0 } = props;
|
||||||
|
// 是否开启节流防抖
|
||||||
|
const throttleType = throttle!.toLowerCase();
|
||||||
|
const isDebounce = throttleType === 'debounce';
|
||||||
|
const openThrottle = ['throttle', 'debounce'].includes(throttleType) && throttleTime > 0;
|
||||||
|
|
||||||
|
const on: {
|
||||||
|
onClick?: Fn;
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
if (attrs.onClick && isFunction(attrs.onClick) && openThrottle) {
|
||||||
|
const [handler] = useThrottle(attrs.onClick as any, throttleTime!, {
|
||||||
|
debounce: isDebounce,
|
||||||
|
immediate: true,
|
||||||
|
});
|
||||||
|
on.onClick = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...attrs,
|
||||||
|
...on,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const getColor = computed(() => {
|
||||||
|
const res: string[] = [];
|
||||||
|
const { color, disabled } = props;
|
||||||
|
color && res.push(`ant-btn-${color}`);
|
||||||
|
disabled && res.push('is-disabled');
|
||||||
|
return res;
|
||||||
|
});
|
||||||
|
|
||||||
|
const getBindValue = computed((): any => {
|
||||||
|
return { ...unref(getListeners), ...props };
|
||||||
|
});
|
||||||
|
return { getBindValue, getColor };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
66
src/components/Button/types.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { VNodeChild } from 'vue';
|
||||||
|
|
||||||
|
export interface BasicButtonProps {
|
||||||
|
/**
|
||||||
|
* can be set to primary ghost dashed danger(added in 2.7) or omitted (meaning default)
|
||||||
|
* @default 'default'
|
||||||
|
* @type string
|
||||||
|
*/
|
||||||
|
type?: 'primary' | 'danger' | 'dashed' | 'ghost' | 'default';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* set the original html type of button
|
||||||
|
* @default 'button'
|
||||||
|
* @type string
|
||||||
|
*/
|
||||||
|
htmlType?: 'button' | 'submit' | 'reset' | 'menu';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* set the icon of button
|
||||||
|
* @type string
|
||||||
|
*/
|
||||||
|
icon?: VNodeChild | JSX.Element;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* can be set to circle or circle-outline or omitted
|
||||||
|
* @type string
|
||||||
|
*/
|
||||||
|
shape?: 'circle' | 'circle-outline';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* can be set to small large or omitted
|
||||||
|
* @default 'default'
|
||||||
|
* @type string
|
||||||
|
*/
|
||||||
|
size?: 'small' | 'large' | 'default';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* set the loading status of button
|
||||||
|
* @default false
|
||||||
|
* @type boolean | { delay: number }
|
||||||
|
*/
|
||||||
|
loading?: boolean | { delay: number };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* disabled state of button
|
||||||
|
* @default false
|
||||||
|
* @type boolean
|
||||||
|
*/
|
||||||
|
disabled?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* make background transparent and invert text and border colors, added in 2.7
|
||||||
|
* @default false
|
||||||
|
* @type boolean
|
||||||
|
*/
|
||||||
|
ghost?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* option to fit button width to its parent width
|
||||||
|
* @default false
|
||||||
|
* @type boolean
|
||||||
|
*/
|
||||||
|
block?: boolean;
|
||||||
|
|
||||||
|
onClick?: (e?: Event) => void;
|
||||||
|
}
|
21
src/components/ClickOutSide/index.vue
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="wrapRef"><slot /></div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import type { Ref } from 'vue';
|
||||||
|
import { defineComponent, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useClickOutside } from '/@/hooks/web/useClickOutside';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'ClickOutSide',
|
||||||
|
|
||||||
|
setup(_, { emit }) {
|
||||||
|
const wrapRef = ref<Nullable<HTMLDivElement | null>>(null);
|
||||||
|
useClickOutside(wrapRef as Ref<HTMLDivElement>, () => {
|
||||||
|
emit('clickOutside');
|
||||||
|
});
|
||||||
|
return { wrapRef };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
5
src/components/Container/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export { default as ScrollContainer } from './src/ScrollContainer.vue';
|
||||||
|
export { default as CollapseContainer } from './src/collapse/CollapseContainer.vue';
|
||||||
|
export { default as LazyContainer } from './src/LazyContainer';
|
||||||
|
|
||||||
|
export * from './src/types.d';
|
27
src/components/Container/src/LazyContainer.less
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
.lazy-container-enter {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lazy-container-enter-to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lazy-container-enter-from,
|
||||||
|
.lazy-container-enter-active {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
transition: opacity 0.3s 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lazy-container-leave {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lazy-container-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lazy-container-leave-active {
|
||||||
|
transition: opacity 0.5s;
|
||||||
|
}
|
200
src/components/Container/src/LazyContainer.tsx
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
import type { PropType } from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
defineComponent,
|
||||||
|
reactive,
|
||||||
|
onMounted,
|
||||||
|
ref,
|
||||||
|
unref,
|
||||||
|
onUnmounted,
|
||||||
|
TransitionGroup,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
import { Skeleton } from 'ant-design-vue';
|
||||||
|
import { useRaf } from '/@/hooks/event/useRaf';
|
||||||
|
import { useTimeout } from '/@/hooks/core/useTimeout';
|
||||||
|
import { getListeners, getSlot } from '/@/utils/helper/tsxHelper';
|
||||||
|
|
||||||
|
import './LazyContainer.less';
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
isInit: boolean;
|
||||||
|
loading: boolean;
|
||||||
|
intersectionObserverInstance: IntersectionObserver | null;
|
||||||
|
}
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'LazyContainer',
|
||||||
|
emits: ['before-init', 'init'],
|
||||||
|
props: {
|
||||||
|
// 等待时间,如果指定了时间,不论可见与否,在指定时间之后自动加载
|
||||||
|
timeout: {
|
||||||
|
type: Number as PropType<number>,
|
||||||
|
default: 8000,
|
||||||
|
// default: 8000,
|
||||||
|
},
|
||||||
|
// 组件所在的视口,如果组件是在页面容器内滚动,视口就是该容器
|
||||||
|
viewport: {
|
||||||
|
type: (typeof window !== 'undefined' ? window.HTMLElement : Object) as PropType<HTMLElement>,
|
||||||
|
default: () => null,
|
||||||
|
},
|
||||||
|
// 预加载阈值, css单位
|
||||||
|
threshold: {
|
||||||
|
type: String as PropType<string>,
|
||||||
|
default: '0px',
|
||||||
|
},
|
||||||
|
|
||||||
|
// 视口的滚动方向, vertical代表垂直方向,horizontal代表水平方向
|
||||||
|
direction: {
|
||||||
|
type: String as PropType<'vertical' | 'horizontal'>,
|
||||||
|
default: 'vertical',
|
||||||
|
},
|
||||||
|
// 包裹组件的外层容器的标签名
|
||||||
|
tag: {
|
||||||
|
type: String as PropType<string>,
|
||||||
|
default: 'div',
|
||||||
|
},
|
||||||
|
|
||||||
|
maxWaitingTime: {
|
||||||
|
type: Number as PropType<number>,
|
||||||
|
default: 80,
|
||||||
|
},
|
||||||
|
|
||||||
|
// 是否在不可见的时候销毁
|
||||||
|
autoDestory: {
|
||||||
|
type: Boolean as PropType<boolean>,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// transition name
|
||||||
|
transitionName: {
|
||||||
|
type: String as PropType<string>,
|
||||||
|
default: 'lazy-container',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props, { attrs, emit, slots }) {
|
||||||
|
const elRef = ref<any>(null);
|
||||||
|
const state = reactive<State>({
|
||||||
|
isInit: false,
|
||||||
|
loading: false,
|
||||||
|
intersectionObserverInstance: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
// If there is a set delay time, it will be executed immediately
|
||||||
|
function immediateInit() {
|
||||||
|
const { timeout } = props;
|
||||||
|
timeout &&
|
||||||
|
useTimeout(() => {
|
||||||
|
init();
|
||||||
|
}, timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
// At this point, the skeleton component is about to be switched
|
||||||
|
emit('before-init');
|
||||||
|
// At this point you can prepare to load the resources of the lazy-loaded component
|
||||||
|
state.loading = true;
|
||||||
|
|
||||||
|
requestAnimationFrameFn(() => {
|
||||||
|
state.isInit = true;
|
||||||
|
emit('init');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function requestAnimationFrameFn(callback: () => any) {
|
||||||
|
// Prevent waiting too long without executing the callback
|
||||||
|
// Set the maximum waiting time
|
||||||
|
useTimeout(() => {
|
||||||
|
if (state.isInit) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
}, props.maxWaitingTime || 80);
|
||||||
|
|
||||||
|
const { requestAnimationFrame } = useRaf();
|
||||||
|
|
||||||
|
return requestAnimationFrame;
|
||||||
|
}
|
||||||
|
function initIntersectionObserver() {
|
||||||
|
const { timeout, direction, threshold, viewport } = props;
|
||||||
|
if (timeout) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// According to the scrolling direction to construct the viewport margin, used to load in advance
|
||||||
|
let rootMargin;
|
||||||
|
switch (direction) {
|
||||||
|
case 'vertical':
|
||||||
|
rootMargin = `${threshold} 0px`;
|
||||||
|
break;
|
||||||
|
case 'horizontal':
|
||||||
|
rootMargin = `0px ${threshold}`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// Observe the intersection of the viewport and the component container
|
||||||
|
state.intersectionObserverInstance = new window.IntersectionObserver(intersectionHandler, {
|
||||||
|
rootMargin,
|
||||||
|
root: viewport,
|
||||||
|
threshold: [0, Number.MIN_VALUE, 0.01],
|
||||||
|
});
|
||||||
|
|
||||||
|
const el = unref(elRef);
|
||||||
|
|
||||||
|
state.intersectionObserverInstance.observe(el.$el);
|
||||||
|
} catch (e) {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Cross-condition change handling function
|
||||||
|
function intersectionHandler(entries: any[]) {
|
||||||
|
const isIntersecting = entries[0].isIntersecting || entries[0].intersectionRatio;
|
||||||
|
if (isIntersecting) {
|
||||||
|
init();
|
||||||
|
if (state.intersectionObserverInstance) {
|
||||||
|
const el = unref(elRef);
|
||||||
|
state.intersectionObserverInstance.unobserve(el.$el);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// else {
|
||||||
|
// const { autoDestory } = props;
|
||||||
|
// autoDestory && destory();
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
// function destory() {
|
||||||
|
// emit('beforeDestory');
|
||||||
|
// state.loading = false;
|
||||||
|
// nextTick(() => {
|
||||||
|
// emit('destory');
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
immediateInit();
|
||||||
|
onMounted(() => {
|
||||||
|
initIntersectionObserver();
|
||||||
|
});
|
||||||
|
onUnmounted(() => {
|
||||||
|
// Cancel the observation before the component is destroyed
|
||||||
|
if (state.intersectionObserverInstance) {
|
||||||
|
const el = unref(elRef);
|
||||||
|
state.intersectionObserverInstance.unobserve(el.$el);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderContent() {
|
||||||
|
const { isInit, loading } = state;
|
||||||
|
if (isInit) {
|
||||||
|
return <div key="component">{getSlot(slots, 'default', { loading })}</div>;
|
||||||
|
}
|
||||||
|
if (slots.skeleton) {
|
||||||
|
return <div key="skeleton">{getSlot(slots, 'skeleton') || <Skeleton />}</div>;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
const { tag, transitionName } = props;
|
||||||
|
return (
|
||||||
|
<TransitionGroup ref={elRef} name={transitionName} tag={tag} {...getListeners(attrs)}>
|
||||||
|
{() => renderContent()}
|
||||||
|
</TransitionGroup>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
80
src/components/Container/src/ScrollContainer.vue
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
<template>
|
||||||
|
<Scrollbar
|
||||||
|
ref="scrollbarRef"
|
||||||
|
:wrapClass="`scrollbar__wrap`"
|
||||||
|
:viewClass="`scrollbar__view`"
|
||||||
|
class="scroll-container"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</Scrollbar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
// component
|
||||||
|
import { defineComponent, ref, unref, nextTick } from 'vue';
|
||||||
|
import { Scrollbar } from '/@/components/Scrollbar';
|
||||||
|
|
||||||
|
// hook
|
||||||
|
import { useScrollTo } from '/@/hooks/event/useScrollTo';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'ScrollContainer',
|
||||||
|
components: { Scrollbar },
|
||||||
|
setup() {
|
||||||
|
const scrollbarRef = ref<RefInstanceType<any>>(null);
|
||||||
|
|
||||||
|
function scrollTo(to: number, duration = 500) {
|
||||||
|
const scrollbar = unref(scrollbarRef);
|
||||||
|
if (!scrollbar) return;
|
||||||
|
nextTick(() => {
|
||||||
|
const { start } = useScrollTo({
|
||||||
|
el: unref(scrollbar.$.wrap),
|
||||||
|
to,
|
||||||
|
duration,
|
||||||
|
});
|
||||||
|
start();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getScrollWrap() {
|
||||||
|
const scrollbar = unref(scrollbarRef);
|
||||||
|
if (!scrollbar) return null;
|
||||||
|
return scrollbar.$.wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollBottom() {
|
||||||
|
const scrollbar = unref(scrollbarRef);
|
||||||
|
if (!scrollbar) return;
|
||||||
|
nextTick(() => {
|
||||||
|
const scrollHeight = scrollbar.$.wrap.scrollHeight as number;
|
||||||
|
const { start } = useScrollTo({
|
||||||
|
el: unref(scrollbar.$.wrap),
|
||||||
|
to: scrollHeight,
|
||||||
|
});
|
||||||
|
start();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
scrollbarRef,
|
||||||
|
scrollTo,
|
||||||
|
scrollBottom,
|
||||||
|
getScrollWrap,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style lang="less">
|
||||||
|
.scroll-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.scrollbar__wrap {
|
||||||
|
margin-bottom: 18px !important;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar__view {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
117
src/components/Container/src/collapse/CollapseContainer.vue
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
<template>
|
||||||
|
<div class="collapse-container p-2 bg:white rounded-sm">
|
||||||
|
<CollapseHeader v-bind="$props" :show="show" @expand="handleExpand" />
|
||||||
|
<CollapseTransition :enable="canExpan">
|
||||||
|
<Skeleton v-if="loading" />
|
||||||
|
<div class="collapse-container__body" v-else v-show="show">
|
||||||
|
<LazyContainer :timeout="lazyTime" v-if="lazy">
|
||||||
|
<slot />
|
||||||
|
<template v-slot:skeleton>
|
||||||
|
<slot name="lazySkeleton" />
|
||||||
|
</template>
|
||||||
|
</LazyContainer>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</CollapseTransition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import type { PropType } from 'vue';
|
||||||
|
|
||||||
|
import { defineComponent, ref, unref } from 'vue';
|
||||||
|
// component
|
||||||
|
import { CollapseTransition } from '/@/components/Transition/index';
|
||||||
|
import CollapseHeader from './CollapseHeader.vue';
|
||||||
|
import { Skeleton } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import LazyContainer from '../LazyContainer';
|
||||||
|
|
||||||
|
import { triggerWindowResize } from '/@/utils/event/triggerWindowResizeEvent';
|
||||||
|
// hook
|
||||||
|
import { useTimeout } from '/@/hooks/core/useTimeout';
|
||||||
|
export default defineComponent({
|
||||||
|
components: { Skeleton, LazyContainer, CollapseHeader, CollapseTransition },
|
||||||
|
name: 'CollapseContainer',
|
||||||
|
props: {
|
||||||
|
// 标题
|
||||||
|
title: {
|
||||||
|
type: String as PropType<string>,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
// 是否可以展开
|
||||||
|
canExpan: {
|
||||||
|
type: Boolean as PropType<boolean>,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
// 标题右侧温馨提醒
|
||||||
|
helpMessage: {
|
||||||
|
type: [Array, String] as PropType<string[] | string>,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
// 展开收缩的时候是否触发window.resize,
|
||||||
|
// 可以适应表格和表单,当表单收缩起来,表格触发resize 自适应高度
|
||||||
|
triggerWindowResize: {
|
||||||
|
type: Boolean as PropType<boolean>,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean as PropType<boolean>,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
// 延时加载
|
||||||
|
lazy: {
|
||||||
|
type: Boolean as PropType<boolean>,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
// 延时加载时间
|
||||||
|
lazyTime: {
|
||||||
|
type: Number as PropType<number>,
|
||||||
|
default: 3000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const showRef = ref(true);
|
||||||
|
/**
|
||||||
|
* @description: 处理开展事件
|
||||||
|
*/
|
||||||
|
function handleExpand() {
|
||||||
|
const hasShow = !unref(showRef);
|
||||||
|
showRef.value = hasShow;
|
||||||
|
|
||||||
|
if (props.triggerWindowResize) {
|
||||||
|
// 这里200毫秒是因为展开有动画,
|
||||||
|
useTimeout(triggerWindowResize, 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
show: showRef,
|
||||||
|
handleExpand,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style lang="less">
|
||||||
|
.collapse-container {
|
||||||
|
padding: 10px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.3s ease-in-out;
|
||||||
|
|
||||||
|
&.no-shadow {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
display: flex;
|
||||||
|
height: 32px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__action {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
32
src/components/Container/src/collapse/CollapseHeader.vue
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<template>
|
||||||
|
<div class="collapse-container__header">
|
||||||
|
<BasicTitle :helpMessage="$attrs.helpMessage">
|
||||||
|
<template v-if="$attrs.title">
|
||||||
|
{{ $attrs.title }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<slot name="title" />
|
||||||
|
</template>
|
||||||
|
</BasicTitle>
|
||||||
|
|
||||||
|
<div class="collapse-container__action">
|
||||||
|
<slot name="action" />
|
||||||
|
<BasicArrow v-if="$attrs.canExpan" :expand="$attrs.show" @click="handleExpand" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import { BasicArrow } from '/@/components/Basic';
|
||||||
|
import { BasicTitle } from '/@/components/Basic';
|
||||||
|
export default defineComponent({
|
||||||
|
inheritAttrs: false,
|
||||||
|
components: { BasicArrow, BasicTitle },
|
||||||
|
setup(_, { emit }) {
|
||||||
|
function handleExpand() {
|
||||||
|
emit('expand');
|
||||||
|
}
|
||||||
|
return { handleExpand };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
17
src/components/Container/src/types.d.ts
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
export type ScrollType = 'default' | 'main';
|
||||||
|
|
||||||
|
export interface CollapseContainerOptions {
|
||||||
|
canExpand?: boolean;
|
||||||
|
title?: string;
|
||||||
|
helpMessage?: Array<any> | string;
|
||||||
|
}
|
||||||
|
export interface ScrollContainerOptions {
|
||||||
|
enableScroll?: boolean;
|
||||||
|
type?: ScrollType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ScrollActionType = RefType<{
|
||||||
|
scrollBottom: () => void;
|
||||||
|
getScrollWrap: () => Nullable<HTMLElement>;
|
||||||
|
scrollTo: (top: number) => void;
|
||||||
|
}>;
|
65
src/components/ContextMenu/index.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import contextMenuVue from './src/index';
|
||||||
|
import { isClient } from '/@/utils/is';
|
||||||
|
import { Options, Props } from './src/types';
|
||||||
|
import { createApp } from 'vue';
|
||||||
|
const menuManager: {
|
||||||
|
doms: Element[];
|
||||||
|
resolve: Fn;
|
||||||
|
} = {
|
||||||
|
doms: [],
|
||||||
|
resolve: () => {},
|
||||||
|
};
|
||||||
|
export const createContextMenu = function (options: Options) {
|
||||||
|
const { event } = options || {};
|
||||||
|
try {
|
||||||
|
event.preventDefault();
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isClient) return;
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const wrapDom = document.createElement('div');
|
||||||
|
const propsData: Partial<Props> = {};
|
||||||
|
if (options.styles !== undefined) propsData.styles = options.styles;
|
||||||
|
if (options.items !== undefined) propsData.items = options.items;
|
||||||
|
if (options.event !== undefined) {
|
||||||
|
propsData.customEvent = event;
|
||||||
|
propsData.axis = { x: event.clientX, y: event.clientY };
|
||||||
|
}
|
||||||
|
createApp(contextMenuVue, propsData).mount(wrapDom);
|
||||||
|
const bodyClick = function () {
|
||||||
|
menuManager.resolve('');
|
||||||
|
};
|
||||||
|
const contextMenuDom = wrapDom.children[0];
|
||||||
|
menuManager.doms.push(contextMenuDom);
|
||||||
|
const remove = function () {
|
||||||
|
menuManager.doms.forEach((dom: Element) => {
|
||||||
|
try {
|
||||||
|
document.body.removeChild(dom);
|
||||||
|
} catch (error) {}
|
||||||
|
});
|
||||||
|
document.body.removeEventListener('click', bodyClick);
|
||||||
|
document.body.removeEventListener('scroll', bodyClick);
|
||||||
|
try {
|
||||||
|
(wrapDom as any) = null;
|
||||||
|
} catch (error) {}
|
||||||
|
};
|
||||||
|
menuManager.resolve = function (...arg: any) {
|
||||||
|
resolve(arg[0]);
|
||||||
|
remove();
|
||||||
|
};
|
||||||
|
remove();
|
||||||
|
document.body.appendChild(contextMenuDom);
|
||||||
|
document.body.addEventListener('click', bodyClick);
|
||||||
|
document.body.addEventListener('scroll', bodyClick);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
export const unMountedContextMenu = function () {
|
||||||
|
if (menuManager) {
|
||||||
|
menuManager.resolve('');
|
||||||
|
menuManager.doms = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export * from './src/types';
|
49
src/components/ContextMenu/src/index.less
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
@import (reference) '../../../design/index.less';
|
||||||
|
|
||||||
|
.context-menu {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1500;
|
||||||
|
display: block;
|
||||||
|
width: 156px;
|
||||||
|
min-width: 10rem;
|
||||||
|
margin: 0;
|
||||||
|
list-style: none;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.1),
|
||||||
|
0 1px 5px 0 rgba(0, 0, 0, 0.06);
|
||||||
|
background-clip: padding-box;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item {
|
||||||
|
a {
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 14px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: @text-color-base;
|
||||||
|
background: #eee;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
a {
|
||||||
|
color: @disabled-color;
|
||||||
|
cursor: not-allowed;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: @disabled-color;
|
||||||
|
background: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
90
src/components/ContextMenu/src/index.tsx
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import {
|
||||||
|
defineComponent,
|
||||||
|
nextTick,
|
||||||
|
onMounted,
|
||||||
|
reactive,
|
||||||
|
computed,
|
||||||
|
ref,
|
||||||
|
unref,
|
||||||
|
onUnmounted,
|
||||||
|
} from 'vue';
|
||||||
|
import { props } from './props';
|
||||||
|
import Icon from '/@/components/Icon';
|
||||||
|
import type { ContextMenuItem } from './types';
|
||||||
|
import './index.less';
|
||||||
|
const prefixCls = 'context-menu';
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'ContextMenu',
|
||||||
|
props,
|
||||||
|
setup(props) {
|
||||||
|
const wrapRef = ref<Nullable<HTMLDivElement>>(null);
|
||||||
|
const state = reactive({
|
||||||
|
show: false,
|
||||||
|
});
|
||||||
|
onMounted(() => {
|
||||||
|
nextTick(() => {
|
||||||
|
state.show = true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
onUnmounted(() => {
|
||||||
|
const el = unref(wrapRef);
|
||||||
|
el && document.body.removeChild(el);
|
||||||
|
});
|
||||||
|
const getStyle = computed(() => {
|
||||||
|
const { axis, items, styles, width } = props;
|
||||||
|
const { x, y } = axis || { x: 0, y: 0 };
|
||||||
|
const menuHeight = (items || []).length * 40;
|
||||||
|
const menuWidth = width;
|
||||||
|
const body = document.body;
|
||||||
|
return {
|
||||||
|
...(styles as any),
|
||||||
|
width: `${width}px`,
|
||||||
|
left: (body.clientWidth < x + menuWidth ? x - menuWidth : x) + 'px',
|
||||||
|
top: (body.clientHeight < y + menuHeight ? y - menuHeight : y) + 'px',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
function handleAction(item: ContextMenuItem, e: MouseEvent) {
|
||||||
|
const { handler, disabled } = item;
|
||||||
|
if (disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.show = false;
|
||||||
|
if (e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
handler && handler();
|
||||||
|
}
|
||||||
|
function renderContent(item: ContextMenuItem) {
|
||||||
|
const { icon, label } = item;
|
||||||
|
|
||||||
|
const { showIcon } = props;
|
||||||
|
return (
|
||||||
|
<span style="display: inline-block; width: 100%;">
|
||||||
|
{showIcon && icon && <Icon class="mr-2" icon={icon} />}
|
||||||
|
<span>{label}</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function renderMenuItem(items: ContextMenuItem[]) {
|
||||||
|
return items.map((item) => {
|
||||||
|
const { disabled, label } = item;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li class={`${prefixCls}__item ${disabled ? 'disabled' : ''}`} key={label}>
|
||||||
|
<a onClick={handleAction.bind(null, item)}>{renderContent(item)}</a>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
const { items } = props;
|
||||||
|
return (
|
||||||
|
<ul class={[prefixCls, !state.show && 'hidden']} ref={wrapRef} style={unref(getStyle)}>
|
||||||
|
{renderMenuItem(items)}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
40
src/components/ContextMenu/src/props.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import type { PropType } from 'vue';
|
||||||
|
import type { Axis, ContextMenuItem } from './types';
|
||||||
|
export const props = {
|
||||||
|
width: {
|
||||||
|
type: Number as PropType<number>,
|
||||||
|
default: 180,
|
||||||
|
},
|
||||||
|
customEvent: {
|
||||||
|
type: Object as PropType<Event>,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
// 样式
|
||||||
|
styles: {
|
||||||
|
type: Object as PropType<any>,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
showIcon: {
|
||||||
|
// 是否显示icon
|
||||||
|
type: Boolean as PropType<boolean>,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
axis: {
|
||||||
|
// 鼠标右键点击的位置
|
||||||
|
type: Object as PropType<Axis>,
|
||||||
|
default() {
|
||||||
|
return { x: 0, y: 0 };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
items: {
|
||||||
|
// 最重要的列表,没有的话直接不显示
|
||||||
|
type: Array as PropType<ContextMenuItem[]>,
|
||||||
|
default() {
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
type: Function as PropType<any>,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
};
|
30
src/components/ContextMenu/src/types.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
export interface Axis {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContextMenuItem {
|
||||||
|
label: string;
|
||||||
|
icon?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
handler?: Fn;
|
||||||
|
divider?: boolean;
|
||||||
|
children?: ContextMenuItem[];
|
||||||
|
}
|
||||||
|
export interface Options {
|
||||||
|
event: MouseEvent;
|
||||||
|
icon?: string;
|
||||||
|
styles?: any;
|
||||||
|
items?: ContextMenuItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
resolve?: (...arg: any) => void;
|
||||||
|
event?: MouseEvent;
|
||||||
|
styles?: any;
|
||||||
|
items: ContextMenuItem[];
|
||||||
|
customEvent?: MouseEvent;
|
||||||
|
axis?: Axis;
|
||||||
|
width?: number;
|
||||||
|
showIcon?: boolean;
|
||||||
|
};
|