@ -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 |
|||
@ -0,0 +1 @@ |
|||
VITE_USE_MOCK=true |
|||
@ -0,0 +1 @@ |
|||
VITE_USE_MOCK=true |
|||
@ -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 |
|||
@ -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', |
|||
}, |
|||
}; |
|||
@ -0,0 +1,6 @@ |
|||
node_modules |
|||
.DS_Store |
|||
dist |
|||
*.local |
|||
.npmrc |
|||
.cache |
|||
@ -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 |
|||
@ -0,0 +1,7 @@ |
|||
/dist/* |
|||
.local |
|||
.output.js |
|||
/node_modules/** |
|||
|
|||
**/*.svg |
|||
**/*.sh |
|||
@ -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 |
|||
@ -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. |
|||
@ -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 }; |
|||
@ -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'); |
|||
} |
|||
@ -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; |
|||
} |
|||
@ -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); |
|||
@ -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; |
|||
} |
|||
@ -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, |
|||
}; |
|||
@ -0,0 +1,10 @@ |
|||
import { exec, which } from 'shelljs'; |
|||
|
|||
function ignoreCaseGit() { |
|||
try { |
|||
if (which('git')) { |
|||
exec('git config core.ignorecase false '); |
|||
} |
|||
} catch (error) {} |
|||
} |
|||
ignoreCaseGit(); |
|||
@ -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) {} |
|||
} |
|||
})(); |
|||
@ -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(); |
|||
})(); |
|||
@ -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 |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
@ -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', |
|||
], |
|||
], |
|||
}, |
|||
}; |
|||
@ -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> |
|||
@ -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'], |
|||
}; |
|||
@ -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]); |
|||
} |
|||
@ -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); |
|||
} |
|||
@ -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[]; |
|||
@ -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[]; |
|||
@ -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" |
|||
} |
|||
} |
|||
@ -0,0 +1,4 @@ |
|||
const path = require('path'); |
|||
module.exports = { |
|||
plugins: [require('tailwindcss'), require('autoprefixer'), require('postcss-import')], |
|||
}; |
|||
@ -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, |
|||
}, |
|||
}, |
|||
], |
|||
}; |
|||
|
After Width: | Height: | Size: 894 B |
@ -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> |
|||
@ -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, |
|||
}); |
|||
} |
|||
@ -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[]; |
|||
@ -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; |
|||
} |
|||
@ -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, |
|||
}); |
|||
} |
|||
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 218 KiB |
|
After Width: | Height: | Size: 372 KiB |
|
After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 995 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
@ -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'); |
|||
}; |
|||
}, |
|||
}); |
|||
@ -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'; |
|||
@ -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> |
|||
@ -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> |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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> |
|||
); |
|||
}, |
|||
}); |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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; |
|||
} |
|||
@ -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> |
|||
@ -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'; |
|||
@ -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; |
|||
} |
|||
@ -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> |
|||
); |
|||
}; |
|||
}, |
|||
}); |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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; |
|||
}>; |
|||
@ -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'; |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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> |
|||
); |
|||
}; |
|||
}, |
|||
}); |
|||
@ -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, |
|||
}, |
|||
}; |
|||
@ -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; |
|||
}; |
|||