提交 2f6253cf 编写于 作者: 陈文彬

initial commit

上级
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
VITE_USE_MOCK=true
VITE_USE_MOCK=true
*.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
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',
},
};
node_modules
.DS_Store
dist
*.local
.npmrc
.cache
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
/dist/*
.local
.output.js
/node_modules/**
**/*.svg
**/*.sh
# 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
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.
/**
* 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 };
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');
}
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;
}
// 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);
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;
}
// #!/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,
};
import { exec, which } from 'shelljs';
function ignoreCaseGit() {
try {
if (which('git')) {
exec('git config core.ignorecase false ');
}
} catch (error) {}
}
ignoreCaseGit();
// 是否需要更新依赖,防止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) {}
}
})();
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();
})();
{
"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
}
}
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;
}
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',
],
],
},
};
<!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>
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'],
};
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer';
import userMock from './sys/user';
import menuMock from './sys/menu';
export function setupProdMockServer() {
createProdMockServer([...userMock, ...menuMock]);
}
// 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);
}
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[];
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[];
{
"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"
}
}
const path = require('path');
module.exports = {
plugins: [require('tailwindcss'), require('autoprefixer'), require('postcss-import')],
};
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,
},
},
],
};
<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>
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,
});
}
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[];
/**
* @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;
}
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,
});
}
<?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>
<?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>
<?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>
<?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>
<?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>
\ No newline at end of file
<?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>
\ No newline at end of file
<?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>
\ No newline at end of file
<?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>
\ No newline at end of file
<?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>
\ No newline at end of file
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');
};
},
});
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';
<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>
<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>
@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;
}
}
}
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>
);
},
});
<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>
<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>
<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>
<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>
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;
}
<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>
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';
.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;
}
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>
);
};
},
});
<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>
<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>
<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>
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;
}>;
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';
@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;
}
}
}
}
}
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>
);
};
},
});
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,
},
};
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;
};
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册