未验证 提交 de231901 编写于 作者: sanmen359's avatar sanmen359 提交者: GitHub

main代码合并到electron-main (#1834)

* upgrade electron version:11->18.

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

* main->electron-main合并代码

* main->electron-main合并代码

* main->electron-main合并代码
上级 006d9322
// @ts-check
const { defineConfig } = require('eslint-define-config');
module.exports = defineConfig({
module.exports = {
root: true,
env: {
browser: true,
......@@ -20,9 +18,7 @@ module.exports = defineConfig({
extends: [
'plugin:vue/vue3-recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
'plugin:prettier/recommended',
'plugin:jest/recommended',
],
rules: {
'vue/script-setup-uses-vars': 'error',
......@@ -62,6 +58,7 @@ module.exports = defineConfig({
'vue/singleline-html-element-content-newline': 'off',
'vue/attribute-hyphenation': 'off',
'vue/require-default-prop': 'off',
'vue/require-explicit-emits': 'off',
'vue/html-self-closing': [
'error',
{
......@@ -74,5 +71,6 @@ module.exports = defineConfig({
math: 'always',
},
],
'vue/multi-word-component-names': 'off',
},
});
};
......@@ -2,5 +2,5 @@ ports:
- port: 3344
onOpen: open-preview
tasks:
- init: yarn
command: yarn dev
- init: pnpm install
command: pnpm run dev
module.exports = {
'*.{js,jsx,ts,tsx}': ['eslint --fix', 'prettier --write'],
'{!(package)*.json,*.code-snippets,.!(browserslist)*rc}': ['prettier --write--parser json'],
'package.json': ['prettier --write'],
'*.vue': ['eslint --fix', 'prettier --write', 'stylelint --fix'],
'*.{scss,less,styl,html}': ['stylelint --fix', 'prettier --write'],
'*.md': ['prettier --write'],
};
......@@ -6,5 +6,3 @@
# Format and submit code according to lintstagedrc.js configuration
npm run lint:lint-staged
npm run lint:pretty
{
"recommendations": [
"octref.vetur",
"johnsoncodehk.volar",
"dbaeumer.vscode-eslint",
"stylelint.vscode-stylelint",
"esbenp.prettier-vscode",
......
......@@ -5,7 +5,7 @@
"type": "chrome",
"request": "launch",
"name": "Launch Chrome",
"url": "http://localhost:3100",
"url": "https://localhost:3100",
"webRoot": "${workspaceFolder}/src",
"sourceMaps": true
}
......
{
"typescript.tsdk": "./node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"volar.tsPlugin": true,
"volar.tsPluginStatus": false,
//===========================================
//============= Editor ======================
//===========================================
"explorer.openEditors.visible": 0,
"npm.packageManager": "pnpm",
"editor.tabSize": 2,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"diffEditor.ignoreTrimWhitespace": false,
//===========================================
//============= Other =======================
//===========================================
"breadcrumbs.enabled": true,
"open-in-browser.default": "chrome",
//===========================================
//============= files =======================
//===========================================
"files.eol": "\n",
"search.exclude": {
"**/node_modules": true,
......@@ -68,16 +55,10 @@
"**/yarn.lock": true
},
"stylelint.enable": true,
"stylelint.packageManager": "yarn",
"liveServer.settings.donotShowInfoMsg": true,
"telemetry.enableCrashReporter": false,
"workbench.settings.enableNaturalLanguageSearch": false,
"stylelint.validate": ["css", "less", "postcss", "scss", "vue", "sass"],
"path-intellisense.mappings": {
"/@/": "${workspaceRoot}/src"
},
"prettier.requireConfig": true,
"typescript.updateImportsOnFileMove.enabled": "always",
"workbench.sideBar.location": "left",
"[javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
......@@ -107,7 +88,8 @@
},
"[vue]": {
"editor.codeActionsOnSave": {
"source.fixAll.eslint": false
"source.fixAll.eslint": true,
"source.fixAll.stylelint": true
}
},
"i18n-ally.localesPaths": ["src/locales/lang"],
......@@ -150,6 +132,8 @@
"lintstagedrc",
"brotli",
"tailwindcss",
"sider"
"sider",
"pnpm",
"antd"
]
}
# test directories
__tests__
test
tests
powered-test
# asset directories
docs
doc
website
images
assets
# examples
example
examples
# code coverage directories
coverage
.nyc_output
# build scripts
Makefile
Gulpfile.js
Gruntfile.js
# configs
appveyor.yml
circle.yml
codeship-services.yml
codeship-steps.yml
wercker.yml
.tern-project
.gitattributes
.editorconfig
.*ignore
.eslintrc
.jshintrc
.flowconfig
.documentup.json
.yarn-metadata.json
.travis.yml
# misc
*.md
!istanbul-reports/lib/html/assets
!istanbul-api/node_modules/istanbul-reports/lib/html/assets
## 2.8.0(2021-11.03)
### Upgrade Instructions
- Package manager changed from `yarn` to `pnpm`
- Delete `node_modules` and `yarn.lock`, install `pnpm` globally
- Execute `pnpm install`
### ✨ Features
- **Others**
- The `VITE_PROXY` configuration in the `.env` file supports single quotes
- Remove warnings during build
### 🐛 Bug Fixes
- **BasicTable**
- Fix the issue that editable cells cannot be submitted in some cases
- Fix the problem that the `inset` attribute does not work
- Fix the problem that the performance of `useTable` and `reload` method `await` of `BasicTable` instance are inconsistent
- Fix the issue that `clickToRowSelect` would ignore the disabled state of the row selection box
- Fix the problem that the page of `BasicTable` will be reset in some cases
- Modify the `deleteTableDataRecord` method
- **BasicModal**
- Fixed the problem that `Modal` could not be closed even when clicking on the mask and pressing the `Esc` key
- Fixed the issue that clicking the close button and the blank area next to the maximize button would also cause `Modal` to close
- **BasicTree** Fix the problem that the node slot does not work
- **CodeEditor** Fix the problem that may cause `Build` failure
- **BasicForm** Fix the problem that the content width of the custom FormItem component may be out of range
- **ApiTreeSelect** Fix the problem that the change of `params` failed to trigger the re-request of api data
- **Others** -Fixed an issue where multiple tabs would not jump to routing when closing tabs in some cases
- Fix the issue that some components may cause abnormal hot update
- Fix the problem that some sub-components of `antdv` will be reported in the build process when directly `import` part of the `antdv`, such as: TabPane, RadioGroup
## 2.7.2(2021-09-14)
### ✨ Features
......
## [2.7.2](https://github.com/anncwb/vue-vben-admin/compare/v2.7.1...v2.7.2) (2021-09-13)
## [2.8.0](https://github.com/anncwb/vue-vben-admin/compare/v2.7.2...v2.8.0) (2021-11-03)
### Bug Fixes
......
## 2.8.0(2021-11.03)
### 升级说明
- 包管理器由`yarn`改为 `pnpm`
- 删除`node_modules``yarn.lock`,全局安装`pnpm`
- 执行`pnpm install`
### ✨ Features
- **其它**
- `.env`文件中的`VITE_PROXY`配置支持单引号
- 移除 build 过程中的警告
### 🐛 Bug Fixes
- **BasicTable**
- 修复可编辑单元格某些情况下无法提交的问题
- 修复`inset`属性不起作用的问题
- 修复`useTable``BasicTable`实例的`reload`方法`await`表现不一致的问题
- 修复`clickToRowSelect`会无视行选择框 disabled 状态的问题
- 修复`BasicTable`在某些情况下,分页会被重置的问题
- 修改 `deleteTableDataRecord` 方法
- **BasicModal**
- 修复点击遮罩、按下`Esc`键都不能关闭`Modal`的问题
- 修复点击关闭按钮、最大化按钮旁边的空白区域也会导致`Modal`关闭的问题
- **BasicTree** 修复节点插槽不起作用的问题
- **CodeEditor** 修复可能会造成的`Build`失败的问题
- **BasicForm** 修复自定义 FormItem 组件的内容宽度可能超出范围的问题
- **ApiTreeSelect** 修复`params`变化未能触发重新请求 api 数据的问题
- **其它**
- 修复多标签在某些情况下关闭页签不会跳转路由的问题
- 修复部分组件可能会造成热更新异常的问题
- 修复直接`import`部分`antdv`子组件时会在 build 过程中报错的问题,如:TabPane、RadioGroup
## 2.7.2(2021-09-14)
### ✨ Features
......
......@@ -70,20 +70,20 @@ git clone https://github.com/anncwb/vue-vben-admin.git
```bash
cd vue-vben-admin
yarn install
pnpm install
```
- run
```bash
yarn serve
pnpm serve
```
- build
```bash
yarn build
pnpm build
```
## Change Log
......
......@@ -70,20 +70,20 @@ git clone https://github.com/anncwb/vue-vben-admin.git
```bash
cd vue-vben-admin
yarn install
pnpm install
```
- 运行
```bash
yarn serve
pnpm serve
```
- 打包
```bash
yarn build
pnpm build
```
## 更新日志
......
import path from 'path';
import fs from 'fs-extra';
import inquirer from 'inquirer';
import chalk from 'chalk';
import colors from 'picocolors';
import pkg from '../../../package.json';
async function generateIcon() {
......@@ -64,7 +64,7 @@ async function generateIcon() {
}
fs.emptyDir(path.join(process.cwd(), 'node_modules/.vite'));
console.log(
`✨ ${chalk.cyan(`[${pkg.name}]`)}` + ' - Icon generated successfully:' + `[${prefixSet}]`,
`✨ ${colors.cyan(`[${pkg.name}]`)}` + ' - Icon generated successfully:' + `[${prefixSet}]`,
);
});
}
......
......@@ -3,20 +3,21 @@
*/
import { GLOB_CONFIG_FILE_NAME, OUTPUT_DIR } from '../constant';
import fs, { writeFileSync } from 'fs-extra';
import chalk from 'chalk';
import colors from 'picocolors';
import { getRootPath, getEnvConfig } from '../utils';
import { getEnvConfig, getRootPath } from '../utils';
import { getConfigFileName } from '../getConfigFileName';
import pkg from '../../package.json';
function createConfig(
{
configName,
config,
configFileName = GLOB_CONFIG_FILE_NAME,
}: { configName: string; config: any; configFileName?: string } = { configName: '', config: {} },
) {
interface CreateConfigParams {
configName: string;
config: any;
configFileName?: string;
}
function createConfig(params: CreateConfigParams) {
const { configName, config, configFileName } = params;
try {
const windowConf = `window.${configName}`;
// Ensure that the variable will not be modified
......@@ -30,15 +31,15 @@ function createConfig(
fs.mkdirp(getRootPath(OUTPUT_DIR));
writeFileSync(getRootPath(`${OUTPUT_DIR}/${configFileName}`), configStr);
console.log(chalk.cyan(`✨ [${pkg.name}]`) + ` - configuration file is build successfully:`);
console.log(chalk.gray(OUTPUT_DIR + '/' + chalk.green(configFileName)) + '\n');
console.log(colors.cyan(`✨ [${pkg.name}]`) + ` - configuration file is build successfully:`);
console.log(colors.gray(OUTPUT_DIR + '/' + colors.green(configFileName)) + '\n');
} catch (error) {
console.log(chalk.red('configuration file configuration file failed to package:\n' + error));
console.log(colors.red('configuration file configuration file failed to package:\n' + error));
}
}
export function runBuildConfig() {
const config = getEnvConfig();
const configFileName = getConfigFileName(config);
createConfig({ config, configName: configFileName });
createConfig({ config, configName: configFileName, configFileName: GLOB_CONFIG_FILE_NAME });
}
......@@ -14,7 +14,7 @@ const TAG = '[compiler-electron]';
export function startCompilerElectron(port = 80) {
// 因为 vite 不会重定向到 index.html,所以直接写 index.html 路由。
const ELECTRON_URL = `http://localhost:${port}/index.html`;
const ELECTRON_URL = `https://localhost:${port}/index.html`;
const spinner = ora(`${TAG} Electron build...`);
const electron = electronConnect.server.create({ stopOnClose: true });
......
// #!/usr/bin/env node
import { runBuildConfig } from './buildConf';
import chalk from 'chalk';
import colors from 'picocolors';
import pkg from '../../package.json';
......@@ -14,9 +14,9 @@ export const runBuild = async () => {
runBuildConfig();
}
console.log(`✨ ${chalk.cyan(`[${pkg.name}]`)}` + ' - build successfully!');
console.log(`✨ ${colors.cyan(`[${pkg.name}]`)}` + ' - build successfully!');
} catch (error) {
console.log(chalk.red('vite build error:\n' + error));
console.log(colors.red('vite build error:\n' + error));
process.exit(1);
}
};
......
......@@ -28,9 +28,9 @@ export function wrapperEnv(envConf: Recordable): ViteEnv {
if (envName === 'VITE_PORT') {
realName = Number(realName);
}
if (envName === 'VITE_PROXY') {
if (envName === 'VITE_PROXY' && realName) {
try {
realName = JSON.parse(realName);
realName = JSON.parse(realName.replace(/'/g, '"'));
} catch (error) {
realName = '';
}
......
// TODO
import type { GetManualChunk } from 'rollup';
//
const vendorLibs: { match: string[]; output: string }[] = [
// {
// match: ['xlsx'],
// output: 'xlsx',
// },
];
// @ts-ignore
export const configManualChunk: GetManualChunk = (id: string) => {
if (/[\\/]node_modules[\\/]/.test(id)) {
const matchItem = vendorLibs.find((item) => {
const reg = new RegExp(`[\\/]node_modules[\\/]_?(${item.match.join('|')})(.*)`, 'ig');
return reg.test(id);
});
return matchItem ? matchItem.output : null;
}
};
......@@ -2,16 +2,16 @@
* Used to package and output gzip. Note that this does not work properly in Vite, the specific reason is still being investigated
* https://github.com/anncwb/vite-plugin-compression
*/
import type { Plugin } from 'vite';
import type { PluginOption } from 'vite';
import compressPlugin from 'vite-plugin-compression';
export function configCompressPlugin(
compress: 'gzip' | 'brotli' | 'none',
deleteOriginFile = false,
): Plugin | Plugin[] {
): PluginOption | PluginOption[] {
const compressList = compress.split(',');
const plugins: Plugin[] = [];
const plugins: PluginOption[] = [];
if (compressList.includes('gzip')) {
plugins.push(
......
import type { Plugin } from 'vite';
/**
* TODO
* Temporarily solve the Vite circular dependency problem, and wait for a better solution to fix it later. I don't know what problems this writing will bring.
* @returns
*/
export function configHmrPlugin(): Plugin {
return {
name: 'singleHMR',
handleHotUpdate({ modules, file }) {
if (file.match(/xml$/)) return [];
modules.forEach((m) => {
if (!m.url.match(/\.(css|less)/)) {
m.importedModules = new Set();
m.importers = new Set();
}
});
return modules;
},
};
}
......@@ -2,8 +2,8 @@
* Plugin to minimize and use ejs template syntax in index.html.
* https://github.com/anncwb/vite-plugin-html
*/
import type { Plugin } from 'vite';
import html from 'vite-plugin-html';
import type { PluginOption } from 'vite';
import { createHtmlPlugin } from 'vite-plugin-html';
import pkg from '../../../package.json';
import { GLOB_CONFIG_FILE_NAME } from '../../constant';
......@@ -16,7 +16,7 @@ export function configHtmlPlugin(env: ViteEnv, isBuild: boolean) {
return `${path || '/'}${GLOB_CONFIG_FILE_NAME}?v=${pkg.version}-${new Date().getTime()}`;
};
const htmlPlugin: Plugin[] = html({
const htmlPlugin: PluginOption[] = createHtmlPlugin({
minify: isBuild,
inject: {
// Inject data into ejs template
......
import type { Plugin } from 'vite';
import { PluginOption } from 'vite';
import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
import legacy from '@vitejs/plugin-legacy';
import purgeIcons from 'vite-plugin-purge-icons';
import windiCSS from 'vite-plugin-windicss';
import VitePluginCertificate from 'vite-plugin-mkcert';
import vueSetupExtend from 'vite-plugin-vue-setup-extend';
import { configHtmlPlugin } from './html';
import { configPwaConfig } from './pwa';
......@@ -14,7 +15,6 @@ import { configVisualizerConfig } from './visualizer';
import { configThemePlugin } from './theme';
import { configImageminPlugin } from './imagemin';
import { configSvgIconsPlugin } from './svgSprite';
import { configHmrPlugin } from './hmr';
export function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean) {
const {
......@@ -25,21 +25,21 @@ export function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean) {
VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE,
} = viteEnv;
const vitePlugins: (Plugin | Plugin[])[] = [
const vitePlugins: (PluginOption | PluginOption[])[] = [
// have to
vue(),
// have to
vueJsx(),
// support name
vueSetupExtend(),
VitePluginCertificate({
source: 'coding',
}),
];
// vite-plugin-windicss
vitePlugins.push(windiCSS());
// TODO
!isBuild && vitePlugins.push(configHmrPlugin());
// @vitejs/plugin-legacy
VITE_LEGACY && isBuild && vitePlugins.push(legacy());
......@@ -61,12 +61,12 @@ export function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean) {
// rollup-plugin-visualizer
vitePlugins.push(configVisualizerConfig());
//vite-plugin-theme
// vite-plugin-theme
vitePlugins.push(configThemePlugin(isBuild));
// The following plugins only work in the production environment
if (isBuild) {
//vite-plugin-imagemin
// vite-plugin-imagemin
VITE_USE_IMAGEMIN && vitePlugins.push(configImageminPlugin());
// rollup-plugin-gzip
......
......@@ -2,19 +2,75 @@
* Introduces component library styles on demand.
* https://github.com/anncwb/vite-plugin-style-import
*/
import styleImport from 'vite-plugin-style-import';
import { createStyleImportPlugin } from 'vite-plugin-style-import';
export function configStyleImportPlugin(isBuild: boolean) {
if (!isBuild) {
return [];
}
const styleImportPlugin = styleImport({
export function configStyleImportPlugin(_isBuild: boolean) {
// if (!isBuild) {
// return [];
// }
const styleImportPlugin = createStyleImportPlugin({
libs: [
{
libraryName: 'ant-design-vue',
esModule: true,
resolveStyle: (name) => {
return `ant-design-vue/es/${name}/style/index`;
// 这里是无需额外引入样式文件的“子组件”列表
const ignoreList = [
'anchor-link',
'sub-menu',
'menu-item',
'menu-divider',
'menu-item-group',
'breadcrumb-item',
'breadcrumb-separator',
'form-item',
'step',
'select-option',
'select-opt-group',
'card-grid',
'card-meta',
'collapse-panel',
'descriptions-item',
'list-item',
'list-item-meta',
'table-column',
'table-column-group',
'tab-pane',
'tab-content',
'timeline-item',
'tree-node',
'skeleton-input',
'skeleton-avatar',
'skeleton-title',
'skeleton-paragraph',
'skeleton-image',
'skeleton-button',
];
// 这里是需要额外引入样式的子组件列表
// 单独引入子组件时需引入组件样式,否则会在打包后导致子组件样式丢失
const replaceList = {
'typography-text': 'typography',
'typography-title': 'typography',
'typography-paragraph': 'typography',
'typography-link': 'typography',
'dropdown-button': 'dropdown',
'input-password': 'input',
'input-search': 'input',
'input-group': 'input',
'radio-group': 'radio',
'checkbox-group': 'checkbox',
'layout-sider': 'layout',
'layout-content': 'layout',
'layout-footer': 'layout',
'layout-header': 'layout',
'month-picker': 'date-picker',
};
return ignoreList.includes(name)
? ''
: replaceList.hasOwnProperty(name)
? `ant-design-vue/es/${replaceList[name]}/style/index`
: `ant-design-vue/es/${name}/style/index`;
},
},
],
......
......@@ -3,11 +3,11 @@
* https://github.com/anncwb/vite-plugin-svg-icons
*/
import SvgIconsPlugin from 'vite-plugin-svg-icons';
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
import path from 'path';
export function configSvgIconsPlugin(isBuild: boolean) {
const svgIconsPlugin = SvgIconsPlugin({
const svgIconsPlugin = createSvgIconsPlugin({
iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
svgoOptions: isBuild,
// default
......
......@@ -2,7 +2,7 @@
* Vite plugin for website theme color switching
* https://github.com/anncwb/vite-plugin-theme
*/
import type { Plugin } from 'vite';
import type { PluginOption } from 'vite';
import path from 'path';
import {
viteThemePlugin,
......@@ -14,7 +14,7 @@ import {
import { getThemeColors, generateColors } from '../../config/themeConfig';
import { generateModifyVars } from '../../generate/generateModifyVars';
export function configThemePlugin(isBuild: boolean): Plugin[] {
export function configThemePlugin(isBuild: boolean): PluginOption[] {
const colors = generateColors({
mixDarken,
mixLighten,
......@@ -85,5 +85,5 @@ export function configThemePlugin(isBuild: boolean): Plugin[] {
}),
];
return plugin as unknown as Plugin[];
return plugin as unknown as PluginOption[];
}
......@@ -7,7 +7,7 @@ type ProxyItem = [string, string];
type ProxyList = ProxyItem[];
type ProxyTargetList = Record<string, ProxyOptions & { rewrite: (path: string) => string }>;
type ProxyTargetList = Record<string, ProxyOptions>;
const httpsRE = /^https:\/\//;
......
......@@ -7,6 +7,7 @@ module.exports = {
'header-max-length': [2, 'always', 108],
'subject-empty': [2, 'never'],
'type-empty': [2, 'never'],
'subject-case': [0],
'type-enum': [
2,
'always',
......
......@@ -26,7 +26,7 @@ class createWin {
backgroundColor: '#fff',
});
const URL = is_dev
? `http://localhost:${process.env.PORT}` // vite 启动的服务器地址
? `https://localhost:${process.env.PORT}` // vite 启动的服务器地址
: `file://${join(__dirname, '../index.html')}`; // vite 构建后的静态文件地址
mainWindow.loadURL(URL);
......
......@@ -8,7 +8,6 @@
name="viewport"
content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0"
/>
<title><%= title %></title>
<link rel="icon" href="/favicon.ico" />
</head>
......@@ -30,7 +29,7 @@
}
html[data-theme='dark'] .app-loading .app-loading-title {
color: rgba(255, 255, 255, 0.85);
color: rgb(255 255 255 / 85%);
}
.app-loading {
......@@ -48,7 +47,6 @@
top: 50%;
left: 50%;
display: flex;
-webkit-transform: translate3d(-50%, -50%, 0);
transform: translate3d(-50%, -50%, 0);
justify-content: center;
align-items: center;
......@@ -66,7 +64,7 @@
display: flex;
margin-top: 30px;
font-size: 30px;
color: rgba(0, 0, 0, 0.85);
color: rgb(0 0 0 / 85%);
justify-content: center;
align-items: center;
}
......@@ -97,7 +95,7 @@
height: 20px;
background-color: #0065cc;
border-radius: 100%;
opacity: 0.3;
opacity: 30%;
transform: scale(0.75);
animation: antSpinMove 1s infinite linear alternate;
transform-origin: 50% 50%;
......@@ -111,43 +109,38 @@
.dot i:nth-child(2) {
top: 0;
right: 0;
-webkit-animation-delay: 0.4s;
animation-delay: 0.4s;
}
.dot i:nth-child(3) {
right: 0;
bottom: 0;
-webkit-animation-delay: 0.8s;
animation-delay: 0.8s;
}
.dot i:nth-child(4) {
bottom: 0;
left: 0;
-webkit-animation-delay: 1.2s;
animation-delay: 1.2s;
}
@keyframes antRotate {
to {
-webkit-transform: rotate(405deg);
transform: rotate(405deg);
}
}
@-webkit-keyframes antRotate {
@keyframes antRotate {
to {
-webkit-transform: rotate(405deg);
transform: rotate(405deg);
}
}
@keyframes antSpinMove {
to {
opacity: 1;
opacity: 100%;
}
}
@-webkit-keyframes antSpinMove {
@keyframes antSpinMove {
to {
opacity: 1;
opacity: 100%;
}
}
</style>
......
export default {
preset: 'ts-jest',
roots: ['<rootDir>/tests/'],
clearMocks: true,
moduleDirectories: ['node_modules', 'src'],
moduleFileExtensions: ['js', 'ts', 'vue', 'tsx', 'jsx', 'json', 'node'],
modulePaths: ['<rootDir>/src', '<rootDir>/node_modules'],
testMatch: [
'**/tests/**/*.[jt]s?(x)',
'**/?(*.)+(spec|test).[tj]s?(x)',
'(/__tests__/.*|(\\.|/)(test|spec))\\.(js|ts)$',
],
testPathIgnorePatterns: [
'<rootDir>/tests/server/',
'<rootDir>/tests/__mocks__/',
'/node_modules/',
],
transform: {
'^.+\\.tsx?$': 'ts-jest',
},
transformIgnorePatterns: ['<rootDir>/tests/__mocks__/', '/node_modules/'],
// A map from regular expressions to module names that allow to stub out resources with a single module
moduleNameMapper: {
'\\.(vs|fs|vert|frag|glsl|jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
'<rootDir>/tests/__mocks__/fileMock.ts',
'\\.(sass|s?css|less)$': '<rootDir>/tests/__mocks__/styleMock.ts',
'\\?worker$': '<rootDir>/tests/__mocks__/workerMock.ts',
'^/@/(.*)$': '<rootDir>/src/$1',
},
testEnvironment: 'jsdom',
verbose: true,
collectCoverage: false,
coverageDirectory: 'coverage',
collectCoverageFrom: ['src/**/*.{js,ts,vue}'],
coveragePathIgnorePatterns: ['^.+\\.d\\.ts$'],
};
import { MockMethod } from 'vite-plugin-mock';
import { resultSuccess, resultError } from '../_util';
import { ResultEnum } from '../../src/enums/httpEnum';
const userInfo = {
name: 'Vben',
......@@ -59,4 +60,12 @@ export default [
return resultError();
},
},
{
url: '/basic-api/user/tokenExpired',
method: 'post',
statusCode: 200,
response: () => {
return resultError('Token Expired!', { code: ResultEnum.TIMEOUT as number });
},
},
] as MockMethod[];
import { MockMethod } from 'vite-plugin-mock';
import { resultSuccess } from '../_util';
const areaList: any[] = [
{
id: '530825900854620160',
code: '430000',
parentCode: '100000',
levelType: 1,
name: '湖南省',
province: '湖南省',
city: null,
district: null,
town: null,
village: null,
parentPath: '430000',
createTime: '2020-11-30 15:47:31',
updateTime: '2020-11-30 16:33:42',
customized: false,
usable: true,
},
{
id: '530825900883980288',
code: '430100',
parentCode: '430000',
levelType: 2,
name: '长沙市',
province: '湖南省',
city: '长沙市',
district: null,
town: null,
village: null,
parentPath: '430000,430100',
createTime: '2020-11-30 15:47:31',
updateTime: '2020-11-30 16:33:42',
customized: false,
usable: true,
},
{
id: '530825900951089152',
code: '430102',
parentCode: '430100',
levelType: 3,
name: '芙蓉区',
province: '湖南省',
city: '长沙市',
district: '芙蓉区',
town: null,
village: null,
parentPath: '430000,430100,430102',
createTime: '2020-11-30 15:47:31',
updateTime: '2020-11-30 16:33:42',
customized: false,
usable: true,
},
{
id: '530825901014003712',
code: '430104',
parentCode: '430100',
levelType: 3,
name: '岳麓区',
province: '湖南省',
city: '长沙市',
district: '岳麓区',
town: null,
village: null,
parentPath: '430000,430100,430104',
createTime: '2020-11-30 15:47:31',
updateTime: '2020-11-30 16:33:42',
customized: false,
usable: true,
},
{
id: '530825900988837888',
code: '430103',
parentCode: '430100',
levelType: 3,
name: '天心区',
province: '湖南省',
city: '长沙市',
district: '天心区',
town: null,
village: null,
parentPath: '430000,430100,430103',
createTime: '2020-11-30 15:47:31',
updateTime: '2020-11-30 16:33:42',
customized: false,
usable: true,
},
{
id: '530826672489115648',
code: '430103002',
parentCode: '430103',
levelType: 4,
name: '坡子街街道',
province: '湖南省',
city: '长沙市',
district: '天心区',
town: '坡子街街道',
village: null,
parentPath: '430000,430100,430103,430103002',
createTime: '2020-11-30 15:47:31',
updateTime: '2020-12-14 15:26:43',
customized: false,
usable: true,
},
{
id: '530840241171607552',
code: '430103002001',
parentCode: '430103002',
levelType: 5,
name: '八角亭社区',
province: '湖南省',
city: '长沙市',
district: '天心区',
town: '坡子街街道',
village: '八角亭社区',
parentPath: '430000,430100,430103,430103002,430103002001',
createTime: '2020-11-30 15:47:31',
updateTime: '2021-01-20 14:07:23',
customized: false,
usable: true,
},
{
id: '530840241200967680',
code: '430103002002',
parentCode: '430103002',
levelType: 5,
name: '西牌楼社区',
province: '湖南省',
city: '长沙市',
district: '天心区',
town: '坡子街街道',
village: '西牌楼社区',
parentPath: '430000,430100,430103,430103002,430103002002',
createTime: '2020-11-30 15:47:31',
updateTime: '2020-11-30 17:30:41',
customized: false,
usable: true,
},
{
id: '530840241230327808',
code: '430103002003',
parentCode: '430103002',
levelType: 5,
name: '太平街社区',
province: '湖南省',
city: '长沙市',
district: '天心区',
town: '坡子街街道',
village: '太平街社区',
parentPath: '430000,430100,430103,430103002,430103002003',
createTime: '2020-11-30 15:47:31',
updateTime: '2020-11-30 17:30:41',
customized: false,
usable: true,
},
{
id: '530840241259687936',
code: '430103002005',
parentCode: '430103002',
levelType: 5,
name: '坡子街社区',
province: '湖南省',
city: '长沙市',
district: '天心区',
town: '坡子街街道',
village: '坡子街社区',
parentPath: '430000,430100,430103,430103002,430103002005',
createTime: '2020-11-30 15:47:31',
updateTime: '2020-11-30 17:30:41',
customized: false,
usable: true,
},
{
id: '530840241284853760',
code: '430103002006',
parentCode: '430103002',
levelType: 5,
name: '青山祠社区',
province: '湖南省',
city: '长沙市',
district: '天心区',
town: '坡子街街道',
village: '青山祠社区',
parentPath: '430000,430100,430103,430103002,430103002006',
createTime: '2020-11-30 15:47:31',
updateTime: '2020-11-30 17:30:41',
customized: false,
usable: true,
},
{
id: '530840241310019584',
code: '430103002007',
parentCode: '430103002',
levelType: 5,
name: '沙河社区',
province: '湖南省',
city: '长沙市',
district: '天心区',
town: '坡子街街道',
village: '沙河社区',
parentPath: '430000,430100,430103,430103002,430103002007',
createTime: '2020-11-30 15:47:31',
updateTime: '2020-11-30 17:30:41',
customized: false,
usable: true,
},
{
id: '530840241381322752',
code: '430103002008',
parentCode: '430103002',
levelType: 5,
name: '碧湘社区',
province: '湖南省',
city: '长沙市',
district: '天心区',
town: '坡子街街道',
village: '碧湘社区',
parentPath: '430000,430100,430103,430103002,430103002008',
createTime: '2020-11-30 15:47:31',
updateTime: '2020-11-30 17:30:41',
customized: false,
usable: true,
},
{
id: '530840241410682880',
code: '430103002009',
parentCode: '430103002',
levelType: 5,
name: '创远社区',
province: '湖南省',
city: '长沙市',
district: '天心区',
town: '坡子街街道',
village: '创远社区',
parentPath: '430000,430100,430103,430103002,430103002009',
createTime: '2020-11-30 15:47:31',
updateTime: '2020-11-30 17:30:41',
customized: false,
usable: true,
},
{
id: '530840241431654400',
code: '430103002010',
parentCode: '430103002',
levelType: 5,
name: '楚湘社区',
province: '湖南省',
city: '长沙市',
district: '天心区',
town: '坡子街街道',
village: '楚湘社区',
parentPath: '430000,430100,430103,430103002,430103002010',
createTime: '2020-11-30 15:47:31',
updateTime: '2020-11-30 17:30:41',
customized: false,
usable: true,
},
{
id: '530840241465208832',
code: '430103002011',
parentCode: '430103002',
levelType: 5,
name: '西湖社区',
province: '湖南省',
city: '长沙市',
district: '天心区',
town: '坡子街街道',
village: '西湖社区',
parentPath: '430000,430100,430103,430103002,430103002011',
createTime: '2020-11-30 15:47:31',
updateTime: '2020-11-30 17:30:41',
customized: false,
usable: true,
},
{
id: '530840241502957568',
code: '430103002012',
parentCode: '430103002',
levelType: 5,
name: '登仁桥社区',
province: '湖南省',
city: '长沙市',
district: '天心区',
town: '坡子街街道',
village: '登仁桥社区',
parentPath: '430000,430100,430103,430103002,430103002012',
createTime: '2020-11-30 15:47:31',
updateTime: '2020-11-30 17:30:41',
customized: false,
usable: true,
},
{
id: '530840241553289216',
code: '430103002013',
parentCode: '430103002',
levelType: 5,
name: '文庙坪社区',
province: '湖南省',
city: '长沙市',
district: '天心区',
town: '坡子街街道',
village: '文庙坪社区',
parentPath: '430000,430100,430103,430103002,430103002013',
createTime: '2020-11-30 15:47:31',
updateTime: '2020-11-30 17:30:41',
customized: false,
usable: true,
},
];
export default [
{
url: '/basic-api/cascader/getAreaRecord',
timeout: 1000,
method: 'post',
response: ({ body }) => {
const { parentCode } = body || {};
if (!parentCode) {
return resultSuccess(areaList.filter((it) => it.code === '430000'));
}
return resultSuccess(areaList.filter((it) => it.parentCode === parentCode));
},
},
] as MockMethod[];
import { MockMethod } from 'vite-plugin-mock';
import { resultSuccess } from '../_util';
const demoList = (keyword) => {
const demoList = (keyword, count = 20) => {
const result = {
list: [] as any[],
};
for (let index = 0; index < 20; index++) {
for (let index = 0; index < count; index++) {
result.list.push({
name: `${keyword ?? ''}选项${index}`,
id: `${index}`,
......@@ -20,9 +20,9 @@ export default [
timeout: 1000,
method: 'get',
response: ({ query }) => {
const { keyword } = query;
const { keyword, count } = query;
console.log(keyword);
return resultSuccess(demoList(keyword));
return resultSuccess(demoList(keyword, count));
},
},
] as MockMethod[];
......@@ -12,7 +12,7 @@ function getRandomPics(count = 10): string[] {
const demoList = (() => {
const result: any[] = [];
for (let index = 0; index < 60; index++) {
for (let index = 0; index < 200; index++) {
result.push({
id: `${index}`,
beginTime: '@datetime',
......
......@@ -111,4 +111,12 @@ export default [
return resultSuccess(undefined, { message: 'Token has been destroyed' });
},
},
{
url: '/basic-api/testRetry',
statusCode: 405,
method: 'get',
response: () => {
return resultError('Error!');
},
},
] as MockMethod[];
{
"name": "vben-admin",
"version": "2.7.2",
"version": "2.8.0",
"author": {
"name": "vben",
"email": "anncwb@126.com",
......@@ -48,12 +48,12 @@
}
},
"scripts": {
"bootstrap": "yarn install",
"dev": "vite",
"bootstrap": "pnpm install",
"serve": "npm run dev",
"dev": "vite",
"build": "cross-env NODE_ENV=production vite build && esno ./build/script/postBuild.ts",
"build:test": "cross-env vite build --mode test && esno ./build/script/postBuild.ts",
"build:no-cache": "yarn clean:cache && npm run build",
"build:no-cache": "pnpm clean:cache && npm run build",
"dev:app": "esno ./build/script/startElectron.ts --env=development --watch",
"build:app": "npm run build && esno ./build/script/startElectron.ts --env=production && electron-builder ",
"report": "cross-env REPORT=true npm run build",
......@@ -66,48 +66,53 @@
"lint:eslint": "eslint --cache --max-warnings 0 \"{src,mock}/**/*.{vue,ts,tsx}\" --fix",
"lint:prettier": "prettier --write \"src/**/*.{js,json,tsx,css,less,scss,vue,html,md}\"",
"lint:stylelint": "stylelint --cache --fix \"**/*.{vue,less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/",
"lint:lint-staged": "lint-staged -c ./.husky/lintstagedrc.js",
"lint:pretty": "pretty-quick --staged",
"lint:lint-staged": "lint-staged",
"test:unit": "jest",
"test:unit-coverage": "jest --coverage",
"test:gzip": "http-server dist --cors --gzip -c-1",
"test:br": "http-server dist --cors --brotli -c-1",
"reinstall": "rimraf yarn.lock && rimraf package.lock.json && rimraf node_modules && npm run bootstrap",
"test:gzip": "npx http-server dist --cors --gzip -c-1",
"test:br": "npx http-server dist --cors --brotli -c-1",
"reinstall": "rimraf pnpm-lock.yaml && rimraf package.lock.json && rimraf node_modules && npm run bootstrap",
"prepare": "husky install",
"gen:icon": "esno ./build/generate/icon/index.ts"
},
"dependencies": {
"@iconify/iconify": "^2.0.4",
"@ant-design/colors": "^6.0.0",
"@ant-design/icons-vue": "^6.1.0",
"@logicflow/core": "^0.6.16",
"@logicflow/extension": "^0.6.16",
"@iconify/iconify": "^2.2.1",
"@vue/runtime-core": "^3.2.31",
"@vue/shared": "^3.2.31",
"@vueuse/core": "^6.3.3",
"@zxcvbn-ts/core": "^1.0.0-beta.0",
"ant-design-vue": "2.2.7",
"@vueuse/shared": "^6.3.3",
"@zxcvbn-ts/core": "^2.0.1",
"ant-design-vue": "3.1.1",
"axios": "^0.21.4",
"codemirror": "^5.62.3",
"cropperjs": "^1.5.12",
"electron-is-dev": "^1.2.0",
"crypto-js": "^4.1.1",
"dayjs": "^1.11.0",
"echarts": "^5.2.0",
"intro.js": "^4.2.2",
"lodash-es": "^4.17.21",
"mockjs": "^1.1.0",
"nprogress": "^0.2.0",
"path-to-regexp": "^6.2.0",
"pinia": "2.0.0-rc.9",
"pinia": "2.0.12",
"print-js": "^1.6.0",
"qrcode": "^1.4.4",
"qs": "^6.10.3",
"resize-observer-polyfill": "^1.5.1",
"showdown": "^1.9.1",
"sortablejs": "^1.14.0",
"tinymce": "^5.9.2",
"vditor": "^3.8.6",
"vue": "3.2.11",
"vue-i18n": "9.1.7",
"vue-json-pretty": "^2.0.4",
"vue-router": "^4.0.11",
"vue-types": "^4.1.0",
"xlsx": "^0.17.1"
"vditor": "^3.8.13",
"vue": "^3.2.31",
"vue-i18n": "^9.1.9",
"vue-json-pretty": "^2.0.6",
"vue-router": "^4.0.14",
"vue-types": "^4.1.1",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@commitlint/cli": "^13.1.0",
......@@ -144,7 +149,7 @@
"conventional-changelog-cli": "^2.1.1",
"cross-env": "^7.0.3",
"dotenv": "^10.0.0",
"electron": "^11.0.0",
"electron": "^18.0.0",
"electron-builder": "^22.8.0",
"electron-connect": "^0.6.3",
"electron-contextmenu-middleware": "^1.0.3",
......@@ -163,9 +168,12 @@
"is-ci": "^3.0.0",
"jest": "^27.2.0",
"less": "^4.1.1",
"lint-staged": "^11.1.2",
"lint-staged": "12.3.7",
"npm-run-all": "^4.1.5",
"picocolors": "^1.0.0",
"postcss": "^8.3.6",
"postcss-html": "^1.3.0",
"postcss-less": "^6.0.0",
"prettier": "^2.4.0",
"pretty-quick": "^3.1.1",
"rimraf": "^3.0.2",
......@@ -178,15 +186,16 @@
"ts-jest": "^27.0.5",
"ts-node": "^10.2.1",
"typescript": "4.4.3",
"vite": "2.5.7",
"vite": "^2.9.1",
"vite-plugin-compression": "^0.3.5",
"vite-plugin-html": "^2.1.0",
"vite-plugin-html": "^3.2.0",
"vite-plugin-imagemin": "^0.4.5",
"vite-plugin-mkcert": "^1.6.0",
"vite-plugin-mock": "^2.9.6",
"vite-plugin-purge-icons": "^0.7.0",
"vite-plugin-pwa": "^0.11.2",
"vite-plugin-style-import": "^1.2.1",
"vite-plugin-svg-icons": "^1.0.4",
"vite-plugin-style-import": "^2.0.0",
"vite-plugin-svg-icons": "^2.0.1",
"vite-plugin-theme": "^0.8.1",
"wait-on": "^5.2.1",
"vite-plugin-vue-setup-extend": "^0.1.0",
......@@ -195,9 +204,9 @@
"vue-tsc": "^0.3.0"
},
"resolutions": {
"//": "Used to install imagemin dependencies, because imagemin may not be installed in China. If it is abroad, you can delete it",
"bin-wrapper": "npm:bin-wrapper-china",
"rollup": "^2.56.3"
"rollup": "^2.56.3",
"gifsicle": "5.2.0"
},
"repository": {
"type": "git",
......
module.exports = {
printWidth: 100,
tabWidth: 2,
useTabs: false,
semi: true,
vueIndentScriptAndStyle: true,
singleQuote: true,
quoteProps: 'as-needed',
bracketSpacing: true,
trailingComma: 'all',
jsxSingleQuote: false,
arrowParens: 'always',
insertPragma: false,
requirePragma: false,
proseWrap: 'never',
htmlWhitespaceSensitivity: 'strict',
endOfLine: 'auto',
......
......@@ -12,6 +12,7 @@
import { useTitle } from '/@/hooks/web/useTitle';
import { useLocale } from '/@/locales/useLocale';
import 'dayjs/locale/zh-cn';
// support Multi-language
const { getAntdLocale } = useLocale();
......
......@@ -4,6 +4,7 @@ import { GetAccountInfoModel } from './model/accountModel';
enum Api {
ACCOUNT_INFO = '/account/getAccountInfo',
SESSION_TIMEOUT = '/user/sessionTimeout',
TOKEN_EXPIRED = '/user/tokenExpired',
}
// Get personal center-basic settings
......@@ -11,3 +12,5 @@ enum Api {
export const accountInfoApi = () => defHttp.get<GetAccountInfoModel>({ url: Api.ACCOUNT_INFO });
export const sessionTimeoutApi = () => defHttp.post<void>({ url: Api.SESSION_TIMEOUT });
export const tokenExpiredApi = () => defHttp.post<void>({ url: Api.TOKEN_EXPIRED });
import { defHttp } from '/@/utils/http/axios';
import { AreaModel, AreaParams } from '/@/api/demo/model/areaModel';
enum Api {
AREA_RECORD = '/cascader/getAreaRecord',
}
export const areaRecord = (data: AreaParams) =>
defHttp.post<AreaModel>({ url: Api.AREA_RECORD, data });
export interface AreaModel {
id: string;
code: string;
parentCode: string;
name: string;
levelType: number;
[key: string]: string | number;
}
export interface AreaParams {
parentCode: string;
}
......@@ -14,6 +14,7 @@ export const demoListApi = (params: DemoParams) =>
url: Api.DEMO_LIST,
params,
headers: {
// @ts-ignore
ignoreCancelToken: true,
},
});
......@@ -3,7 +3,7 @@ export interface BasicPageParams {
pageSize: number;
}
export interface BasicFetchResult<T extends any> {
export interface BasicFetchResult<T> {
items: T[];
total: number;
}
......@@ -8,6 +8,7 @@ enum Api {
Logout = '/logout',
GetUserInfo = '/getUserInfo',
GetPermCode = '/getPermCode',
TestRetry = '/testRetry',
}
/**
......@@ -39,3 +40,16 @@ export function getPermCode() {
export function doLogout() {
return defHttp.get({ url: Api.Logout });
}
export function testRetry() {
return defHttp.get(
{ url: Api.TestRetry },
{
retryRequest: {
isOpenRetry: true,
count: 5,
waitTime: 1000,
},
},
);
}
......@@ -39,7 +39,7 @@
html[data-theme='dark'] {
.@{prefix-cls} {
border: 1px solid rgb(196, 188, 188);
border: 1px solid rgb(196 188 188);
}
}
......
......@@ -4,11 +4,11 @@
-->
<template>
<Dropdown
placement="bottomCenter"
placement="bottom"
:trigger="['click']"
:dropMenuList="localeList"
:selectedKeys="selectedKeys"
@menuEvent="handleMenuEvent"
@menu-event="handleMenuEvent"
overlayClassName="app-locale-picker-overlay"
>
<span class="cursor-pointer flex items-center">
......
......@@ -87,6 +87,7 @@
font-size: 16px;
font-weight: 700;
transition: all 0.5s;
line-height: normal;
}
}
</style>
......@@ -42,7 +42,7 @@
background-color: linear-gradient(-225deg, #d5dbe4, #f8f8f8);
border-radius: 2px;
box-shadow: inset 0 -2px 0 0 #cdcde6, inset 0 0 1px 1px #fff,
0 1px 2px 1px rgba(30, 35, 90, 0.4);
0 1px 2px 1px rgb(30 35 90 / 40%);
align-items: center;
justify-content: center;
......
......@@ -125,7 +125,7 @@
width: 100%;
height: 100%;
padding-top: 50px;
background-color: rgba(0, 0, 0, 0.25);
background-color: rgb(0 0 0 / 25%);
justify-content: center;
&--mobile {
......@@ -159,7 +159,7 @@
&__item {
&-enter {
opacity: 0 !important;
opacity: 0% !important;
}
}
}
......@@ -168,16 +168,16 @@
&-content {
position: relative;
width: 632px;
margin: 0 auto auto auto;
margin: 0 auto auto;
background-color: @component-background;
border-radius: 16px;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
box-shadow: 0 25px 50px -12px rgb(0 0 0 / 25%);
flex-direction: column;
}
&-input__wrapper {
display: flex;
padding: 14px 14px 0 14px;
padding: 14px 14px 0;
justify-content: space-between;
align-items: center;
}
......@@ -245,7 +245,7 @@
background-color: @primary-color;
.@{prefix-cls}-list__item-enter {
opacity: 1;
opacity: 100%;
}
}
......@@ -259,7 +259,7 @@
&-enter {
width: 30px;
opacity: 0;
opacity: 0%;
}
}
}
......
<template>
<div class="p-2">
<div class="bg-white mb-2 p-4">
<div class="p-4 mb-2 bg-white">
<BasicForm @register="registerForm" />
</div>
{{ sliderProp.width }}
<div class="bg-white p-2">
<div class="p-2 bg-white">
<List
:grid="{ gutter: 5, xs: 1, sm: 2, md: 4, lg: 4, xl: 6, xxl: grid }"
:data-source="data"
......@@ -39,7 +38,7 @@
<Image :src="item.imgs[0]" />
</div>
</template>
<template class="ant-card-actions" #actions>
<template #actions>
<!-- <SettingOutlined key="setting" />-->
<EditOutlined key="edit" />
<Dropdown
......@@ -167,7 +166,7 @@
pageSize.value = pz;
fetch();
}
function pageSizeChange(current, size) {
function pageSizeChange(_current, size) {
pageSize.value = size;
fetch();
}
......
import { ref } from 'vue';
//每行个数
// 每行个数
export const grid = ref(12);
// slider属性
export const useSlider = (min = 6, max = 12) => {
......
......@@ -4,3 +4,5 @@ import jsonPreview from './src/json-preview/JsonPreview.vue';
export const CodeEditor = withInstall(codeEditor);
export const JsonPreview = withInstall(jsonPreview);
export * from './src/typing';
......@@ -8,22 +8,22 @@
/>
</div>
</template>
<script lang="ts">
export const MODE = {
JSON: 'application/json',
html: 'htmlmixed',
js: 'javascript',
};
</script>
<script lang="ts" setup>
import { computed } from 'vue';
import CodeMirrorEditor from './codemirror/CodeMirror.vue';
import { isString } from '/@/utils/is';
import { MODE } from './typing';
const props = defineProps({
value: { type: [Object, String] as PropType<Record<string, any> | string> },
mode: { type: String, default: MODE.JSON },
mode: {
type: String as PropType<MODE>,
default: MODE.JSON,
validator(value: any) {
// 这个值必须匹配下列字符串中的一个
return Object.values(MODE).includes(value);
},
},
readonly: { type: Boolean },
autoFormat: { type: Boolean, default: true },
});
......
......@@ -8,6 +8,7 @@
import { useAppStore } from '/@/store/modules/app';
import { useWindowSizeFn } from '/@/hooks/event/useWindowSizeFn';
import CodeMirror from 'codemirror';
import { MODE } from './../typing';
// css
import './codemirror.css';
import 'codemirror/theme/idea.css';
......@@ -18,7 +19,14 @@
import 'codemirror/mode/htmlmixed/htmlmixed';
const props = defineProps({
mode: { type: String, default: 'application/json' },
mode: {
type: String as PropType<MODE>,
default: MODE.JSON,
validator(value: any) {
// 这个值必须匹配下列字符串中的一个
return Object.values(MODE).includes(value);
},
},
value: { type: String, default: '' },
readonly: { type: Boolean, default: false },
});
......
......@@ -2,7 +2,7 @@
.CodeMirror {
--base: #545281;
--comment: hsl(210, 25%, 60%);
--comment: hsl(210deg 25% 60%);
--keyword: #af4ab1;
--variable: #0055d1;
--function: #c25205;
......@@ -125,9 +125,7 @@
}
.cm-fat-cursor-mark {
background-color: rgba(20, 255, 20, 0.5);
-webkit-animation: blink 1.06s steps(1) infinite;
-moz-animation: blink 1.06s steps(1) infinite;
background-color: rgb(20 255 20 / 50%);
animation: blink 1.06s steps(1) infinite;
}
......@@ -135,16 +133,14 @@
width: auto;
background-color: #7e7;
border: 0;
-webkit-animation: blink 1.06s steps(1) infinite;
-moz-animation: blink 1.06s steps(1) infinite;
animation: blink 1.06s steps(1) infinite;
}
@-moz-keyframes blink {
@keyframes blink {
50% {
background-color: transparent;
}
}
@-webkit-keyframes blink {
@keyframes blink {
50% {
background-color: transparent;
}
......@@ -294,7 +290,7 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {
}
.CodeMirror-matchingtag {
background: rgba(255, 150, 0, 0.3);
background: rgb(255 150 0 / 30%);
}
.CodeMirror-activeline-background {
......@@ -394,7 +390,7 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {
background-color: transparent;
}
.CodeMirror-gutter-wrapper ::-moz-selection {
.CodeMirrorwrapper ::selection {
background-color: transparent;
}
......@@ -414,11 +410,8 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {
border-width: 0;
/* Reset some styles that the rest of the page might have set */
-moz-border-radius: 0;
-webkit-border-radius: 0;
border-radius: 0;
-webkit-tap-highlight-color: transparent;
-webkit-font-variant-ligatures: contextual;
font-variant-ligatures: contextual;
}
......@@ -457,7 +450,6 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {
.CodeMirror-gutter,
.CodeMirror-gutters,
.CodeMirror-linenumber {
-moz-box-sizing: content-box;
box-sizing: content-box;
}
......@@ -505,15 +497,9 @@ div.CodeMirror-dragcursors {
background: #d7d4f0;
}
.CodeMirror-line::-moz-selection,
.CodeMirror-line > span::-moz-selection,
.CodeMirror-line > span > span::-moz-selection {
background: #d7d4f0;
}
.cm-searching {
background-color: #ffa;
background-color: rgba(255, 255, 0, 0.4);
background-color: rgb(255 255 0 / 40%);
}
/* Used to force a border model for a node */
......
export enum MODE {
JSON = 'application/json',
HTML = 'htmlmixed',
JS = 'javascript',
}
<template>
<div :class="prefixCls">
<CollapseHeader v-bind="$props" :prefixCls="prefixCls" :show="show" @expand="handleExpand">
<CollapseHeader v-bind="props" :prefixCls="prefixCls" :show="show" @expand="handleExpand">
<template #title>
<slot name="title"></slot>
</template>
......@@ -25,6 +25,7 @@
<script lang="ts" setup>
import type { PropType } from 'vue';
import { ref } from 'vue';
import { isNil } from 'lodash-es';
// component
import { Skeleton } from 'ant-design-vue';
import { CollapseTransition } from '/@/components/Transition';
......@@ -66,13 +67,17 @@
/**
* @description: Handling development events
*/
function handleExpand() {
show.value = !show.value;
function handleExpand(val: boolean) {
show.value = isNil(val) ? !show.value : val;
if (props.triggerWindowResize) {
// 200 milliseconds here is because the expansion has animation,
useTimeoutFn(triggerWindowResize, 200);
}
}
defineExpose({
handleExpand,
});
</script>
<style lang="less">
@prefix-cls: ~'@{namespace}-collapse-container';
......
......@@ -60,9 +60,11 @@
const top = body.clientHeight < y + menuHeight ? y - menuHeight : y;
return {
...styles,
position: 'absolute',
width: `${width}px`,
left: `${left + 1}px`,
top: `${top + 1}px`,
zIndex: 9999,
};
});
......@@ -124,15 +126,11 @@
}
const { items } = props;
return (
<Menu
inlineIndent={12}
mode="vertical"
class={prefixCls}
ref={wrapRef}
style={unref(getStyle)}
>
{renderMenuItem(items)}
</Menu>
<div class={prefixCls}>
<Menu inlineIndent={12} mode="vertical" ref={wrapRef} style={unref(getStyle)}>
{renderMenuItem(items)}
</Menu>
</div>
);
};
},
......@@ -178,22 +176,25 @@
margin: 0;
list-style: none;
background-color: @component-background;
border: 1px solid rgba(0, 0, 0, 0.08);
border: 1px solid rgb(0 0 0 / 8%);
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);
box-shadow: 0 2px 2px 0 rgb(0 0 0 / 14%), 0 3px 1px -2px rgb(0 0 0 / 10%),
0 1px 5px 0 rgb(0 0 0 / 6%);
background-clip: padding-box;
user-select: none;
&__item {
margin: 0 !important;
}
.item-style();
.ant-divider {
margin: 0 0;
margin: 0;
}
&__popup {
.ant-divider {
margin: 0 0;
margin: 0;
}
.item-style();
......
......@@ -234,17 +234,17 @@
background: #eee;
background-image: linear-gradient(
45deg,
rgba(0, 0, 0, 0.25) 25%,
rgb(0 0 0 / 25%) 25%,
transparent 0,
transparent 75%,
rgba(0, 0, 0, 0.25) 0
rgb(0 0 0 / 25%) 0
),
linear-gradient(
45deg,
rgba(0, 0, 0, 0.25) 25%,
rgb(0 0 0 / 25%) 25%,
transparent 0,
transparent 75%,
rgba(0, 0, 0, 0.25) 0
rgb(0 0 0 / 25%) 0
);
background-position: 0 0, 12px 12px;
background-size: 24px 24px;
......
......@@ -22,7 +22,7 @@
<CopperModal
@register="register"
@uploadSuccess="handleUploadSuccess"
@upload-success="handleUploadSuccess"
:uploadApi="uploadApi"
:src="sourceValue"
/>
......@@ -135,15 +135,14 @@
}
&-image-mask {
opacity: 0;
opacity: 0%;
position: absolute;
width: inherit;
height: inherit;
border-radius: inherit;
border: inherit;
background: rgba(0, 0, 0, 0.4);
background: rgb(0 0 0 / 40%);
cursor: pointer;
-webkit-transition: opacity 0.4s;
transition: opacity 0.4s;
::v-deep(svg) {
......@@ -152,7 +151,7 @@
}
&-image-mask:hover {
opacity: 40;
opacity: 4000%;
}
&-upload-btn {
......
......@@ -3,7 +3,7 @@
import type { DescriptionsProps } from 'ant-design-vue/es/descriptions/index';
import type { CSSProperties } from 'vue';
import type { CollapseContainerOptions } from '/@/components/Container/index';
import { defineComponent, computed, ref, unref } from 'vue';
import { defineComponent, computed, ref, unref, toRefs } from 'vue';
import { get } from 'lodash-es';
import { Descriptions } from 'ant-design-vue';
import { CollapseContainer } from '/@/components/Container/index';
......@@ -121,6 +121,9 @@
return null;
}
const getField = get(_data, field);
if (getField && !toRefs(_data).hasOwnProperty(field)) {
return isFunction(render) ? render('', _data) : '';
}
return isFunction(render) ? render(getField, _data) : getField ?? '';
};
......
......@@ -94,7 +94,7 @@
opt.width = '100%';
}
const detailCls = `${prefixCls}__detail`;
opt.wrapClassName = wrapClassName ? `${wrapClassName} ${detailCls}` : detailCls;
opt.class = wrapClassName ? `${wrapClassName} ${detailCls}` : detailCls;
if (!getContainer) {
// TODO type error?
......
......@@ -128,13 +128,12 @@ export interface DrawerProps extends DrawerFooterProps {
* @type any (string | slot)
*/
title?: VNodeChild | JSX.Element;
/**
* The class name of the container of the Drawer dialog.
* @type string
*/
wrapClassName?: string;
class?: string;
/**
* Style of wrapper element which **contains mask** compare to `drawerStyle`
* @type object
......
<template>
<Dropdown :trigger="trigger" v-bind="$attrs">
<a-dropdown :trigger="trigger" v-bind="$attrs">
<span>
<slot></slot>
</span>
<template #overlay>
<Menu :selectedKeys="selectedKeys">
<a-menu :selectedKeys="selectedKeys">
<template v-for="item in dropMenuList" :key="`${item.event}`">
<MenuItem
<a-menu-item
v-bind="getAttr(item.event)"
@click="handleClickMenu(item)"
:disabled="item.disabled"
>
<Popconfirm
<a-popconfirm
v-if="popconfirm && item.popConfirm"
v-bind="getPopConfirmAttrs(item.popConfirm)"
>
......@@ -22,86 +22,75 @@
<Icon :icon="item.icon" v-if="item.icon" />
<span class="ml-1">{{ item.text }}</span>
</div>
</Popconfirm>
</a-popconfirm>
<template v-else>
<Icon :icon="item.icon" v-if="item.icon" />
<span class="ml-1">{{ item.text }}</span>
</template>
</MenuItem>
<MenuDivider v-if="item.divider" :key="`d-${item.event}`" />
</a-menu-item>
<a-menu-divider v-if="item.divider" :key="`d-${item.event}`" />
</template>
</Menu>
</a-menu>
</template>
</Dropdown>
</a-dropdown>
</template>
<script lang="ts">
<script lang="ts" setup>
import { computed, PropType } from 'vue';
import type { DropMenu } from './typing';
import { defineComponent } from 'vue';
import { Dropdown, Menu, Popconfirm } from 'ant-design-vue';
import { Icon } from '/@/components/Icon';
import { omit } from 'lodash-es';
import { isFunction } from '/@/utils/is';
export default defineComponent({
name: 'BasicDropdown',
components: {
Dropdown,
Menu,
MenuItem: Menu.Item,
MenuDivider: Menu.Divider,
Icon,
Popconfirm,
},
props: {
popconfirm: Boolean,
/**
* the trigger mode which executes the drop-down action
* @default ['hover']
* @type string[]
*/
trigger: {
type: [Array] as PropType<('contextmenu' | 'click' | 'hover')[]>,
default: () => {
return ['contextmenu'];
},
},
dropMenuList: {
type: Array as PropType<(DropMenu & Recordable)[]>,
default: () => [],
},
selectedKeys: {
type: Array as PropType<string[]>,
default: () => [],
const ADropdown = Dropdown;
const AMenu = Menu;
const AMenuItem = Menu.Item;
const AMenuDivider = Menu.Divider;
const APopconfirm = Popconfirm;
const props = defineProps({
popconfirm: Boolean,
/**
* the trigger mode which executes the drop-down action
* @default ['hover']
* @type string[]
*/
trigger: {
type: [Array] as PropType<('contextmenu' | 'click' | 'hover')[]>,
default: () => {
return ['contextmenu'];
},
},
emits: ['menuEvent'],
setup(props, { emit }) {
function handleClickMenu(item: DropMenu) {
const { event } = item;
const menu = props.dropMenuList.find((item) => `${item.event}` === `${event}`);
emit('menuEvent', menu);
item.onClick?.();
}
dropMenuList: {
type: Array as PropType<(DropMenu & Recordable)[]>,
default: () => [],
},
selectedKeys: {
type: Array as PropType<string[]>,
default: () => [],
},
});
const getPopConfirmAttrs = computed(() => {
return (attrs) => {
const originAttrs = omit(attrs, ['confirm', 'cancel', 'icon']);
if (!attrs.onConfirm && attrs.confirm && isFunction(attrs.confirm))
originAttrs['onConfirm'] = attrs.confirm;
if (!attrs.onCancel && attrs.cancel && isFunction(attrs.cancel))
originAttrs['onCancel'] = attrs.cancel;
return originAttrs;
};
});
const emit = defineEmits(['menuEvent']);
return {
handleClickMenu,
getPopConfirmAttrs,
getAttr: (key: string | number) => ({ key }),
};
},
function handleClickMenu(item: DropMenu) {
const { event } = item;
const menu = props.dropMenuList.find((item) => `${item.event}` === `${event}`);
emit('menuEvent', menu);
item.onClick?.();
}
const getPopConfirmAttrs = computed(() => {
return (attrs) => {
const originAttrs = omit(attrs, ['confirm', 'cancel', 'icon']);
if (!attrs.onConfirm && attrs.confirm && isFunction(attrs.confirm))
originAttrs['onConfirm'] = attrs.confirm;
if (!attrs.onCancel && attrs.cancel && isFunction(attrs.cancel))
originAttrs['onCancel'] = attrs.cancel;
return originAttrs;
};
});
const getAttr = (key: string | number) => ({ key });
</script>
import xlsx from 'xlsx';
import * as xlsx from 'xlsx';
import type { WorkBook } from 'xlsx';
import type { JsonToSheet, AoAToSheet } from './typing';
......
......@@ -14,13 +14,26 @@
</template>
<script lang="ts">
import { defineComponent, ref, unref } from 'vue';
import XLSX from 'xlsx';
import * as XLSX from 'xlsx';
import { dateUtil } from '/@/utils/dateUtil';
import type { ExcelData } from './typing';
export default defineComponent({
name: 'ImportExcel',
props: {
// 日期时间格式。如果不提供或者提供空值,将返回原始Date对象
dateFormat: {
type: String,
},
// 时区调整。实验性功能,仅为了解决读取日期时间值有偏差的问题。目前仅提供了+08:00时区的偏差修正值
// https://github.com/SheetJS/sheetjs/issues/1470#issuecomment-501108554
timeZone: {
type: Number,
default: 8,
},
},
emits: ['success', 'error'],
setup(_, { emit }) {
setup(props, { emit }) {
const inputRef = ref<HTMLInputElement | null>(null);
const loadingRef = ref<Boolean>(false);
......@@ -51,10 +64,28 @@
*/
function getExcelData(workbook: XLSX.WorkBook) {
const excelData: ExcelData[] = [];
const { dateFormat, timeZone } = props;
for (const sheetName of workbook.SheetNames) {
const worksheet = workbook.Sheets[sheetName];
const header: string[] = getHeaderRow(worksheet);
const results = XLSX.utils.sheet_to_json(worksheet);
let results = XLSX.utils.sheet_to_json(worksheet, {
raw: true,
dateNF: dateFormat, //Not worked
}) as object[];
results = results.map((row: object) => {
for (let field in row) {
if (row[field] instanceof Date) {
if (timeZone === 8) {
row[field].setSeconds(row[field].getSeconds() + 43);
}
if (dateFormat) {
row[field] = dateUtil(row[field]).format(dateFormat);
}
}
}
return row;
});
excelData.push({
header,
results,
......@@ -76,7 +107,7 @@
reader.onload = async (e) => {
try {
const data = e.target && e.target.result;
const workbook = XLSX.read(data, { type: 'array' });
const workbook = XLSX.read(data, { type: 'array', cellDates: true });
// console.log(workbook);
/* DO SOMETHING WITH workbook HERE */
const excelData = getExcelData(workbook);
......
......@@ -9,5 +9,8 @@ export { useForm } from './src/hooks/useForm';
export { default as ApiSelect } from './src/components/ApiSelect.vue';
export { default as RadioButtonGroup } from './src/components/RadioButtonGroup.vue';
export { default as ApiTreeSelect } from './src/components/ApiTreeSelect.vue';
export { default as ApiTree } from './src/components/ApiTree.vue';
export { default as ApiRadioGroup } from './src/components/ApiRadioGroup.vue';
export { default as ApiCascader } from './src/components/ApiCascader.vue';
export { BasicForm };
......@@ -58,6 +58,7 @@
import { createFormContext } from './hooks/useFormContext';
import { useAutoFocus } from './hooks/useAutoFocus';
import { useModalContext } from '/@/components/Modal';
import { useDebounceFn } from '@vueuse/core';
import { basicProps } from './props';
import { useDesign } from '/@/hooks/web/useDesign';
......@@ -66,7 +67,7 @@
name: 'BasicForm',
components: { FormItem, Form, Row, FormAction },
props: basicProps,
emits: ['advanced-change', 'reset', 'submit', 'register'],
emits: ['advanced-change', 'reset', 'submit', 'register', 'field-value-change'],
setup(props, { emit, attrs }) {
const formModel = reactive<Recordable>({});
const modalFn = useModalContext();
......@@ -122,7 +123,7 @@
if (!Array.isArray(defaultValue)) {
schema.defaultValue = dateUtil(defaultValue);
} else {
const def: moment.Moment[] = [];
const def: any[] = [];
defaultValue.forEach((item) => {
def.push(dateUtil(item));
});
......@@ -225,6 +226,14 @@
},
);
watch(
() => formModel,
useDebounceFn(() => {
unref(getProps).submitOnChange && handleSubmit();
}, 300),
{ deep: true },
);
async function setProps(formProps: Partial<FormProps>): Promise<void> {
propsRef.value = deepMerge(unref(propsRef) || {}, formProps);
}
......@@ -235,6 +244,7 @@
if (!validateTrigger || validateTrigger === 'change') {
validateFields([key]).catch((_) => {});
}
emit('field-value-change', key, value);
}
function handleEnterPress(e: KeyboardEvent) {
......
......@@ -21,9 +21,12 @@ import {
Divider,
} from 'ant-design-vue';
import ApiRadioGroup from './components/ApiRadioGroup.vue';
import RadioButtonGroup from './components/RadioButtonGroup.vue';
import ApiSelect from './components/ApiSelect.vue';
import ApiTree from './components/ApiTree.vue';
import ApiTreeSelect from './components/ApiTreeSelect.vue';
import ApiCascader from './components/ApiCascader.vue';
import { BasicUpload } from '/@/components/Upload';
import { StrengthMeter } from '/@/components/StrengthMeter';
import { IconPicker } from '/@/components/Icon';
......@@ -41,13 +44,16 @@ componentMap.set('AutoComplete', AutoComplete);
componentMap.set('Select', Select);
componentMap.set('ApiSelect', ApiSelect);
componentMap.set('ApiTree', ApiTree);
componentMap.set('TreeSelect', TreeSelect);
componentMap.set('ApiTreeSelect', ApiTreeSelect);
componentMap.set('ApiRadioGroup', ApiRadioGroup);
componentMap.set('Switch', Switch);
componentMap.set('RadioButtonGroup', RadioButtonGroup);
componentMap.set('RadioGroup', Radio.Group);
componentMap.set('Checkbox', Checkbox);
componentMap.set('CheckboxGroup', Checkbox.Group);
componentMap.set('ApiCascader', ApiCascader);
componentMap.set('Cascader', Cascader);
componentMap.set('Slider', Slider);
componentMap.set('Rate', Rate);
......
<template>
<a-cascader
v-model:value="state"
:options="options"
:load-data="loadData"
change-on-select
@change="handleChange"
:displayRender="handleRenderDisplay"
>
<template #suffixIcon v-if="loading">
<LoadingOutlined spin />
</template>
<template #notFoundContent v-if="loading">
<span>
<LoadingOutlined spin class="mr-1" />
{{ t('component.form.apiSelectNotFound') }}
</span>
</template>
</a-cascader>
</template>
<script lang="ts">
import { defineComponent, PropType, ref, unref, watch, watchEffect } from 'vue';
import { Cascader } from 'ant-design-vue';
import { propTypes } from '/@/utils/propTypes';
import { isFunction } from '/@/utils/is';
import { get, omit } from 'lodash-es';
import { useRuleFormItem } from '/@/hooks/component/useFormItem';
import { LoadingOutlined } from '@ant-design/icons-vue';
import { useI18n } from '/@/hooks/web/useI18n';
interface Option {
value: string;
label: string;
loading?: boolean;
isLeaf?: boolean;
children?: Option[];
}
export default defineComponent({
name: 'ApiCascader',
components: {
LoadingOutlined,
[Cascader.name]: Cascader,
},
props: {
value: {
type: Array,
},
api: {
type: Function as PropType<(arg?: Recordable) => Promise<Option[]>>,
default: null,
},
numberToString: propTypes.bool,
resultField: propTypes.string.def(''),
labelField: propTypes.string.def('label'),
valueField: propTypes.string.def('value'),
childrenField: propTypes.string.def('children'),
asyncFetchParamKey: propTypes.string.def('parentCode'),
immediate: propTypes.bool.def(true),
// init fetch params
initFetchParams: {
type: Object as PropType<Recordable>,
default: () => ({}),
},
// 是否有下级,默认是
isLeaf: {
type: Function as PropType<(arg: Recordable) => boolean>,
default: null,
},
displayRenderArray: {
type: Array,
},
},
emits: ['change', 'defaultChange'],
setup(props, { emit }) {
const apiData = ref<any[]>([]);
const options = ref<Option[]>([]);
const loading = ref<boolean>(false);
const emitData = ref<any[]>([]);
const isFirstLoad = ref(true);
const { t } = useI18n();
// Embedded in the form, just use the hook binding to perform form verification
const [state] = useRuleFormItem(props, 'value', 'change', emitData);
watch(
apiData,
(data) => {
const opts = generatorOptions(data);
options.value = opts;
},
{ deep: true },
);
function generatorOptions(options: any[]): Option[] {
const { labelField, valueField, numberToString, childrenField, isLeaf } = props;
return options.reduce((prev, next: Recordable) => {
if (next) {
const value = next[valueField];
const item = {
...omit(next, [labelField, valueField]),
label: next[labelField],
value: numberToString ? `${value}` : value,
isLeaf: isLeaf && typeof isLeaf === 'function' ? isLeaf(next) : false,
};
const children = Reflect.get(next, childrenField);
if (children) {
Reflect.set(item, childrenField, generatorOptions(children));
}
prev.push(item);
}
return prev;
}, [] as Option[]);
}
async function initialFetch() {
const api = props.api;
if (!api || !isFunction(api)) return;
apiData.value = [];
loading.value = true;
try {
const res = await api(props.initFetchParams);
if (Array.isArray(res)) {
apiData.value = res;
return;
}
if (props.resultField) {
apiData.value = get(res, props.resultField) || [];
}
} catch (error) {
console.warn(error);
} finally {
loading.value = false;
}
}
async function loadData(selectedOptions: Option[]) {
const targetOption = selectedOptions[selectedOptions.length - 1];
targetOption.loading = true;
const api = props.api;
if (!api || !isFunction(api)) return;
try {
const res = await api({
[props.asyncFetchParamKey]: Reflect.get(targetOption, 'value'),
});
if (Array.isArray(res)) {
const children = generatorOptions(res);
targetOption.children = children;
return;
}
if (props.resultField) {
const children = generatorOptions(get(res, props.resultField) || []);
targetOption.children = children;
}
} catch (e) {
console.error(e);
} finally {
targetOption.loading = false;
}
}
watchEffect(() => {
props.immediate && initialFetch();
});
watch(
() => props.initFetchParams,
() => {
!unref(isFirstLoad) && initialFetch();
},
{ deep: true },
);
function handleChange(keys, args) {
emitData.value = keys;
emit('defaultChange', keys, args);
}
function handleRenderDisplay({ labels, selectedOptions }) {
if (unref(emitData).length === selectedOptions.length) {
return labels.join(' / ');
}
if (props.displayRenderArray) {
return props.displayRenderArray.join(' / ');
}
return '';
}
return {
state,
options,
loading,
t,
handleChange,
loadData,
handleRenderDisplay,
};
},
});
</script>
<!--
* @Description:It is troublesome to implement radio button group in the form. So it is extracted independently as a separate component
-->
<template>
<RadioGroup v-bind="attrs" v-model:value="state" button-style="solid" @change="handleChange">
<template v-for="item in getOptions" :key="`${item.value}`">
<RadioButton v-if="props.isBtn" :value="item.value" :disabled="item.disabled">
{{ item.label }}
</RadioButton>
<Radio v-else :value="item.value" :disabled="item.disabled">
{{ item.label }}
</Radio>
</template>
</RadioGroup>
</template>
<script lang="ts">
import { defineComponent, PropType, ref, watchEffect, computed, unref, watch } from 'vue';
import { Radio } from 'ant-design-vue';
import { isFunction } from '/@/utils/is';
import { useRuleFormItem } from '/@/hooks/component/useFormItem';
import { useAttrs } from '/@/hooks/core/useAttrs';
import { propTypes } from '/@/utils/propTypes';
import { get, omit } from 'lodash-es';
import { useI18n } from '/@/hooks/web/useI18n';
type OptionsItem = { label: string; value: string | number | boolean; disabled?: boolean };
export default defineComponent({
name: 'ApiRadioGroup',
components: {
RadioGroup: Radio.Group,
RadioButton: Radio.Button,
Radio,
},
props: {
api: {
type: Function as PropType<(arg?: Recordable | string) => Promise<OptionsItem[]>>,
default: null,
},
params: {
type: [Object, String] as PropType<Recordable | string>,
default: () => ({}),
},
value: {
type: [String, Number, Boolean] as PropType<string | number | boolean>,
},
isBtn: {
type: [Boolean] as PropType<boolean>,
default: false,
},
numberToString: propTypes.bool,
resultField: propTypes.string.def(''),
labelField: propTypes.string.def('label'),
valueField: propTypes.string.def('value'),
immediate: propTypes.bool.def(true),
},
emits: ['options-change', 'change'],
setup(props, { emit }) {
const options = ref<OptionsItem[]>([]);
const loading = ref(false);
const isFirstLoad = ref(true);
const emitData = ref<any[]>([]);
const attrs = useAttrs();
const { t } = useI18n();
// Embedded in the form, just use the hook binding to perform form verification
const [state] = useRuleFormItem(props);
// Processing options value
const getOptions = computed(() => {
const { labelField, valueField, numberToString } = props;
return unref(options).reduce((prev, next: Recordable) => {
if (next) {
const value = next[valueField];
prev.push({
label: next[labelField],
value: numberToString ? `${value}` : value,
...omit(next, [labelField, valueField]),
});
}
return prev;
}, [] as OptionsItem[]);
});
watchEffect(() => {
props.immediate && fetch();
});
watch(
() => props.params,
() => {
!unref(isFirstLoad) && fetch();
},
{ deep: true },
);
async function fetch() {
const api = props.api;
if (!api || !isFunction(api)) return;
options.value = [];
try {
loading.value = true;
const res = await api(props.params);
if (Array.isArray(res)) {
options.value = res;
emitChange();
return;
}
if (props.resultField) {
options.value = get(res, props.resultField) || [];
}
emitChange();
} catch (error) {
console.warn(error);
} finally {
loading.value = false;
}
}
function emitChange() {
emit('options-change', unref(getOptions));
}
function handleChange(_, ...args) {
emitData.value = args;
}
return { state, getOptions, attrs, loading, t, handleChange, props };
},
});
</script>
<template>
<Select
@dropdownVisibleChange="handleFetch"
@dropdown-visible-change="handleFetch"
v-bind="$attrs"
@change="handleChange"
:options="getOptions"
......@@ -57,6 +57,7 @@
labelField: propTypes.string.def('label'),
valueField: propTypes.string.def('value'),
immediate: propTypes.bool.def(true),
alwaysLoad: propTypes.bool.def(false),
},
emits: ['options-change', 'change'],
setup(props, { emit }) {
......@@ -77,9 +78,9 @@
if (next) {
const value = next[valueField];
prev.push({
...omit(next, [labelField, valueField]),
label: next[labelField],
value: numberToString ? `${value}` : value,
...omit(next, [labelField, valueField]),
});
}
return prev;
......@@ -87,7 +88,7 @@
});
watchEffect(() => {
props.immediate && fetch();
props.immediate && !props.alwaysLoad && fetch();
});
watch(
......@@ -121,10 +122,14 @@
}
}
async function handleFetch() {
if (!props.immediate && unref(isFirstLoad)) {
await fetch();
isFirstLoad.value = false;
async function handleFetch(visible) {
if (visible) {
if (props.alwaysLoad) {
await fetch();
} else if (!props.immediate && unref(isFirstLoad)) {
await fetch();
isFirstLoad.value = false;
}
}
}
......
<template>
<a-tree v-bind="getAttrs" @change="handleChange">
<template #[item]="data" v-for="item in Object.keys($slots)">
<slot :name="item" v-bind="data || {}"></slot>
</template>
<template #suffixIcon v-if="loading">
<LoadingOutlined spin />
</template>
</a-tree>
</template>
<script lang="ts">
import { computed, defineComponent, watch, ref, onMounted, unref } from 'vue';
import { Tree } from 'ant-design-vue';
import { isArray, isFunction } from '/@/utils/is';
import { get } from 'lodash-es';
import { propTypes } from '/@/utils/propTypes';
import { LoadingOutlined } from '@ant-design/icons-vue';
export default defineComponent({
name: 'ApiTree',
components: { ATree: Tree, LoadingOutlined },
props: {
api: { type: Function as PropType<(arg?: Recordable) => Promise<Recordable>> },
params: { type: Object },
immediate: { type: Boolean, default: true },
resultField: propTypes.string.def(''),
afterFetch: { type: Function as PropType<Fn> },
},
emits: ['options-change', 'change'],
setup(props, { attrs, emit }) {
const treeData = ref<Recordable[]>([]);
const isFirstLoaded = ref<Boolean>(false);
const loading = ref(false);
const getAttrs = computed(() => {
return {
...(props.api ? { treeData: unref(treeData) } : {}),
...attrs,
};
});
function handleChange(...args) {
emit('change', ...args);
}
watch(
() => props.params,
() => {
!unref(isFirstLoaded) && fetch();
},
{ deep: true },
);
watch(
() => props.immediate,
(v) => {
v && !isFirstLoaded.value && fetch();
},
);
onMounted(() => {
props.immediate && fetch();
});
async function fetch() {
const { api, afterFetch } = props;
if (!api || !isFunction(api)) return;
loading.value = true;
treeData.value = [];
let result;
try {
result = await api(props.params);
} catch (e) {
console.error(e);
}
if (afterFetch && isFunction(afterFetch)) {
result = afterFetch(result);
}
loading.value = false;
if (!result) return;
if (!isArray(result)) {
result = get(result, props.resultField);
}
treeData.value = (result as Recordable[]) || [];
isFirstLoaded.value = true;
emit('options-change', treeData.value);
}
return { getAttrs, loading, handleChange };
},
});
</script>
......@@ -44,7 +44,7 @@
watch(
() => props.params,
() => {
isFirstLoaded.value && fetch();
!unref(isFirstLoaded) && fetch();
},
{ deep: true },
);
......
<script lang="tsx">
import type { PropType, Ref } from 'vue';
import type { FormActionType, FormProps } from '../types/form';
import type { FormSchema } from '../types/form';
import { computed, defineComponent, toRefs, unref } from 'vue';
import type { FormActionType, FormProps, FormSchema } from '../types/form';
import type { ValidationRule } from 'ant-design-vue/lib/form/Form';
import type { TableActionType } from '/@/components/Table';
import { defineComponent, computed, unref, toRefs } from 'vue';
import { Form, Col, Divider } from 'ant-design-vue';
import { Col, Divider, Form } from 'ant-design-vue';
import { componentMap } from '../componentMap';
import { BasicHelp } from '/@/components/Basic';
import { isBoolean, isFunction, isNull } from '/@/utils/is';
import { getSlot } from '/@/utils/helper/tsxHelper';
import { createPlaceholderMessage, setComponentRuleType } from '../helper';
import { upperFirst, cloneDeep } from 'lodash-es';
import { cloneDeep, upperFirst } from 'lodash-es';
import { useItemLabelWidth } from '../hooks/useLabelWidth';
import { useI18n } from '/@/hooks/web/useI18n';
......@@ -178,8 +177,21 @@
const getRequired = isFunction(required) ? required(unref(getValues)) : required;
if ((!rules || rules.length === 0) && getRequired) {
rules = [{ required: getRequired, validator }];
/*
* 1、若设置了required属性,又没有其他的rules,就创建一个验证规则;
* 2、若设置了required属性,又存在其他的rules,则只rules中不存在required属性时,才添加验证required的规则
* 也就是说rules中的required,优先级大于required
*/
if (getRequired) {
if (!rules || rules.length === 0) {
rules = [{ required: getRequired, validator }];
} else {
const requiredIndex: number = rules.findIndex((rule) => Reflect.has(rule, 'required'));
if (requiredIndex === -1) {
rules.push({ required: getRequired, validator });
}
}
}
const requiredRuleIndex: number = rules.findIndex(
......@@ -340,7 +352,7 @@
wrapperCol={wrapperCol}
>
<div style="display:flex">
<div style="flex:1">{getContent()}</div>
<div style="flex:1;">{getContent()}</div>
{showSuffix && <span class="suffix">{getSuffix}</span>}
</div>
</Form.Item>
......
......@@ -70,3 +70,5 @@ export function handleInputNumberValue(component?: ComponentType, val?: any) {
* 时间字段
*/
export const dateItemType = genType();
export const defaultValueComponents = ['Input', 'InputPassword', 'InputSearch', 'InputTextArea'];
import type { ComputedRef, Ref } from 'vue';
import type { FormProps, FormSchema, FormActionType } from '../types/form';
import type { NamePath } from 'ant-design-vue/lib/form/interface';
import { unref, toRaw } from 'vue';
import { isArray, isFunction, isObject, isString } from '/@/utils/is';
import { unref, toRaw, nextTick } from 'vue';
import { isArray, isFunction, isNullOrUnDef, isObject, isString } from '/@/utils/is';
import { deepMerge } from '/@/utils';
import { dateItemType, handleInputNumberValue } from '../helper';
import { dateItemType, handleInputNumberValue, defaultValueComponents } from '../helper';
import { dateUtil } from '/@/utils/dateUtil';
import { cloneDeep, uniqBy } from 'lodash-es';
import { error } from '/@/utils/log';
......@@ -37,9 +37,12 @@ export function useFormEvents({
if (!formEl) return;
Object.keys(formModel).forEach((key) => {
formModel[key] = defaultValueRef.value[key];
const schema = unref(getSchema).find((item) => item.field === key);
const isInput = schema?.component && defaultValueComponents.includes(schema.component);
formModel[key] = isInput ? defaultValueRef.value[key] || '' : defaultValueRef.value[key];
});
clearValidate();
nextTick(() => clearValidate());
emit('reset', toRaw(formModel));
submitOnReset && handleSubmit();
}
......@@ -125,18 +128,18 @@ export function useFormEvents({
const schemaList: FormSchema[] = cloneDeep(unref(getSchema));
const index = schemaList.findIndex((schema) => schema.field === prefixField);
const hasInList = schemaList.some((item) => item.field === prefixField || schema.field);
if (!hasInList) return;
if (!prefixField || index === -1 || first) {
first ? schemaList.unshift(schema) : schemaList.push(schema);
schemaRef.value = schemaList;
_setDefaultValue(schema);
return;
}
if (index !== -1) {
schemaList.splice(index + 1, 0, schema);
}
_setDefaultValue(schema);
schemaRef.value = schemaList;
}
......@@ -192,9 +195,34 @@ export function useFormEvents({
}
});
});
_setDefaultValue(schema);
schemaRef.value = uniqBy(schema, 'field');
}
function _setDefaultValue(data: FormSchema | FormSchema[]) {
let schemas: FormSchema[] = [];
if (isObject(data)) {
schemas.push(data as FormSchema);
}
if (isArray(data)) {
schemas = [...data];
}
const obj: Recordable = {};
schemas.forEach((item) => {
if (
item.component != 'Divider' &&
Reflect.has(item, 'field') &&
item.field &&
!isNullOrUnDef(item.defaultValue)
) {
obj[item.field] = item.defaultValue;
}
});
setFieldsValue(obj);
}
function getFieldsValue(): Recordable {
const formEl = unref(formElRef);
if (!formEl) return {};
......@@ -242,7 +270,7 @@ export function useFormEvents({
const values = await validate();
const res = handleFormValues(values);
emit('submit', res);
} catch (error) {
} catch (error: any) {
throw new Error(error);
}
}
......
......@@ -11,6 +11,43 @@ interface UseFormValuesContext {
getProps: ComputedRef<FormProps>;
formModel: Recordable;
}
/**
* @desription deconstruct array-link key. This method will mutate the target.
*/
function tryDeconstructArray(key: string, value: any, target: Recordable) {
const pattern = /^\[(.+)\]$/;
if (pattern.test(key)) {
const match = key.match(pattern);
if (match && match[1]) {
const keys = match[1].split(',');
value = Array.isArray(value) ? value : [value];
keys.forEach((k, index) => {
set(target, k.trim(), value[index]);
});
return true;
}
}
}
/**
* @desription deconstruct object-link key. This method will mutate the target.
*/
function tryDeconstructObject(key: string, value: any, target: Recordable) {
const pattern = /^\{(.+)\}$/;
if (pattern.test(key)) {
const match = key.match(pattern);
if (match && match[1]) {
const keys = match[1].split(',');
value = isObject(value) ? value : {};
keys.forEach((k) => {
set(target, k.trim(), value[k.trim()]);
});
return true;
}
}
}
export function useFormValues({
defaultValueRef,
getSchema,
......@@ -33,14 +70,18 @@ export function useFormValues({
if (isObject(value)) {
value = transformDateFunc?.(value);
}
if (isArray(value) && value[0]?._isAMomentObject && value[1]?._isAMomentObject) {
if (isArray(value) && value[0]?.format && value[1]?.format) {
value = value.map((item) => transformDateFunc?.(item));
}
// Remove spaces
if (isString(value)) {
value = value.trim();
}
set(res, key, value);
if (!tryDeconstructArray(key, value, res) && !tryDeconstructObject(key, value, res)) {
// 没有解构成功的,按原样赋值
set(res, key, value);
}
}
return handleRangeTimeValue(res);
}
......@@ -77,7 +118,10 @@ export function useFormValues({
const { defaultValue } = item;
if (!isNullOrUnDef(defaultValue)) {
obj[item.field] = defaultValue;
formModel[item.field] = defaultValue;
if (formModel[item.field] === undefined) {
formModel[item.field] = defaultValue;
}
}
});
defaultValueRef.value = obj;
......
import type { Ref } from 'vue';
import type { FormProps, FormSchema } from '../types/form';
import { computed, unref } from 'vue';
import type { FormProps, FormSchema } from '../types/form';
import { isNumber } from '/@/utils/is';
export function useItemLabelWidth(schemaItemRef: Ref<FormSchema>, propsRef: Ref<FormProps>) {
......@@ -14,6 +13,7 @@ export function useItemLabelWidth(schemaItemRef: Ref<FormSchema>, propsRef: Ref<
labelWidth: globalLabelWidth,
labelCol: globalLabelCol,
wrapperCol: globWrapperCol,
layout,
} = unref(propsRef);
// If labelWidth is set globally, all items setting
......@@ -33,7 +33,10 @@ export function useItemLabelWidth(schemaItemRef: Ref<FormSchema>, propsRef: Ref<
return {
labelCol: { style: { width }, ...col },
wrapperCol: { style: { width: `calc(100% - ${width})` }, ...wrapCol },
wrapperCol: {
style: { width: layout === 'vertical' ? '100%' : `calc(100% - ${width})` },
...wrapCol,
},
};
});
}
......@@ -40,6 +40,7 @@ export const basicProps = {
// 在INPUT组件上单击回车时,是否自动提交
autoSubmitOnEnter: propTypes.bool.def(false),
submitOnReset: propTypes.bool,
submitOnChange: propTypes.bool,
size: propTypes.oneOf(['default', 'small', 'large']).def('default'),
// 禁用表单
disabled: propTypes.bool,
......@@ -53,7 +54,7 @@ export const basicProps = {
transformDateFunc: {
type: Function as PropType<Fn>,
default: (date: any) => {
return date._isAMomentObject ? date?.format('YYYY-MM-DD HH:mm:ss') : date;
return date?.format?.('YYYY-MM-DD HH:mm:ss') ?? date;
},
},
rulesMessageJoinLabel: propTypes.bool.def(true),
......
......@@ -49,17 +49,20 @@ export type RegisterFn = (formInstance: FormActionType) => void;
export type UseFormReturnType = [RegisterFn, FormActionType];
export interface FormProps {
name?: string;
layout?: 'vertical' | 'inline' | 'horizontal';
// Form value
model?: Recordable;
// The width of all items in the entire form
labelWidth?: number | string;
//alignment
// alignment
labelAlign?: 'left' | 'right';
//Row configuration for the entire form
// Row configuration for the entire form
rowProps?: RowProps;
// Submit form on reset
submitOnReset?: boolean;
// Submit form on form changing
submitOnChange?: boolean;
// Col configuration for the entire form
labelCol?: Partial<ColEx>;
// Col configuration for the entire form
......@@ -129,7 +132,7 @@ export interface FormSchema {
// Variable name bound to v-model Default value
valueField?: string;
// Label name
label: string;
label: string | VNode;
// Auxiliary text
subLabel?: string;
// Help text on the right side of the text
......
......@@ -91,12 +91,15 @@ export type ComponentType =
| 'Select'
| 'ApiSelect'
| 'TreeSelect'
| 'ApiTree'
| 'ApiTreeSelect'
| 'ApiRadioGroup'
| 'RadioButtonGroup'
| 'RadioGroup'
| 'Checkbox'
| 'CheckboxGroup'
| 'AutoComplete'
| 'ApiCascader'
| 'Cascader'
| 'DatePicker'
| 'MonthPicker'
......
......@@ -7,7 +7,7 @@
v-model:value="currentSelect"
>
<template #addonAfter>
<Popover
<a-popover
placement="bottomLeft"
trigger="click"
v-model="visible"
......@@ -17,7 +17,7 @@
<div class="flex justify-between">
<a-input
:placeholder="t('component.icon.search')"
@change="handleSearchChange"
@change="debounceHandleSearchChange"
allowClear
/>
</div>
......@@ -31,18 +31,7 @@
v-for="icon in getPaginationList"
:key="icon"
:class="currentSelect === icon ? 'border border-primary' : ''"
class="
p-2
w-1/8
cursor-pointer
mr-1
mt-1
flex
justify-center
items-center
border border-solid
hover:border-primary
"
class="p-2 w-1/8 cursor-pointer mr-1 mt-1 flex justify-center items-center border border-solid hover:border-primary"
@click="handleClick(icon)"
:title="icon"
>
......@@ -53,7 +42,7 @@
</ul>
</ScrollContainer>
<div class="flex py-2 items-center justify-center" v-if="getTotal >= pageSize">
<Pagination
<a-pagination
showLessItems
size="small"
:pageSize="pageSize"
......@@ -63,7 +52,7 @@
</div>
</div>
<template v-else
><div class="p-5"><Empty /></div>
><div class="p-5"><a-empty /></div>
</template>
</template>
......@@ -71,16 +60,14 @@
<SvgIcon :name="currentSelect" />
</span>
<Icon :icon="currentSelect || 'ion:apps-outline'" class="cursor-pointer px-2 py-1" v-else />
</Popover>
</a-popover>
</template>
</a-input>
</template>
<script lang="ts">
import { defineComponent, ref, watchEffect, watch, unref } from 'vue';
<script lang="ts" setup>
import { ref, watchEffect, watch, unref } from 'vue';
import { useDesign } from '/@/hooks/web/useDesign';
import { ScrollContainer } from '/@/components/Container';
import { Input, Popover, Pagination, Empty } from 'ant-design-vue';
import Icon from './Icon.vue';
import SvgIcon from './SvgIcon.vue';
......@@ -94,6 +81,12 @@
import { useMessage } from '/@/hooks/web/useMessage';
import svgIcons from 'virtual:svg-icons-names';
// 没有使用别名引入,是因为WebStorm当前版本还不能正确识别,会报unused警告
const AInput = Input;
const APopover = Popover;
const APagination = Pagination;
const AEmpty = Empty;
function getIcons() {
const data = iconsData as any;
const prefix: string = data?.prefix ?? '';
......@@ -110,88 +103,70 @@
return svgIcons.map((icon) => icon.replace('icon-', ''));
}
export default defineComponent({
name: 'IconPicker',
components: { [Input.name]: Input, Icon, Popover, ScrollContainer, Pagination, Empty, SvgIcon },
inheritAttrs: false,
props: {
value: propTypes.string,
width: propTypes.string.def('100%'),
pageSize: propTypes.number.def(140),
copy: propTypes.bool.def(false),
mode: propTypes.oneOf<('svg' | 'iconify')[]>(['svg', 'iconify']).def('iconify'),
const props = defineProps({
value: propTypes.string,
width: propTypes.string.def('100%'),
pageSize: propTypes.number.def(140),
copy: propTypes.bool.def(false),
mode: propTypes.oneOf<('svg' | 'iconify')[]>(['svg', 'iconify']).def('iconify'),
});
const emit = defineEmits(['change', 'update:value']);
const isSvgMode = props.mode === 'svg';
const icons = isSvgMode ? getSvgIcons() : getIcons();
const currentSelect = ref('');
const visible = ref(false);
const currentList = ref(icons);
const { t } = useI18n();
const { prefixCls } = useDesign('icon-picker');
const debounceHandleSearchChange = useDebounceFn(handleSearchChange, 100);
const { clipboardRef, isSuccessRef } = useCopyToClipboard(props.value);
const { createMessage } = useMessage();
const { getPaginationList, getTotal, setCurrentPage } = usePagination(
currentList,
props.pageSize,
);
watchEffect(() => {
currentSelect.value = props.value;
});
watch(
() => currentSelect.value,
(v) => {
emit('update:value', v);
return emit('change', v);
},
emits: ['change', 'update:value'],
setup(props, { emit }) {
const isSvgMode = props.mode === 'svg';
const icons = isSvgMode ? getSvgIcons() : getIcons();
const currentSelect = ref('');
const visible = ref(false);
const currentList = ref(icons);
const { t } = useI18n();
const { prefixCls } = useDesign('icon-picker');
const debounceHandleSearchChange = useDebounceFn(handleSearchChange, 100);
const { clipboardRef, isSuccessRef } = useCopyToClipboard(props.value);
const { createMessage } = useMessage();
const { getPaginationList, getTotal, setCurrentPage } = usePagination(
currentList,
props.pageSize,
);
watchEffect(() => {
currentSelect.value = props.value;
});
watch(
() => currentSelect.value,
(v) => {
emit('update:value', v);
return emit('change', v);
},
);
function handlePageChange(page: number) {
setCurrentPage(page);
}
);
function handleClick(icon: string) {
currentSelect.value = icon;
if (props.copy) {
clipboardRef.value = icon;
if (unref(isSuccessRef)) {
createMessage.success(t('component.icon.copy'));
}
}
}
function handlePageChange(page: number) {
setCurrentPage(page);
}
function handleSearchChange(e: ChangeEvent) {
const value = e.target.value;
if (!value) {
setCurrentPage(1);
currentList.value = icons;
return;
}
currentList.value = icons.filter((item) => item.includes(value));
function handleClick(icon: string) {
currentSelect.value = icon;
if (props.copy) {
clipboardRef.value = icon;
if (unref(isSuccessRef)) {
createMessage.success(t('component.icon.copy'));
}
}
}
return {
t,
prefixCls,
visible,
isSvgMode,
getTotal,
getPaginationList,
handlePageChange,
handleClick,
currentSelect,
handleSearchChange: debounceHandleSearchChange,
};
},
});
function handleSearchChange(e: ChangeEvent) {
const value = e.target.value;
if (!value) {
setCurrentPage(1);
currentList.value = icons;
return;
}
currentList.value = icons.filter((item) => item.includes(value));
}
</script>
<style lang="less">
@prefix-cls: ~'@{namespace}-icon-picker';
......
<template>
<section class="full-loading" :class="{ absolute }" v-show="loading">
<section
class="full-loading"
:class="{ absolute, [theme]: !!theme }"
:style="[background ? `background-color: ${background}` : '']"
v-show="loading"
>
<Spin v-bind="$attrs" :tip="tip" :size="size" :spinning="loading" />
</section>
</template>
......@@ -35,6 +40,9 @@
background: {
type: String as PropType<string>,
},
theme: {
type: String as PropType<'dark' | 'light'>,
},
},
});
</script>
......@@ -49,7 +57,7 @@
height: 100%;
justify-content: center;
align-items: center;
background-color: rgba(240, 242, 245, 0.4);
background-color: rgb(240 242 245 / 40%);
&.absolute {
position: absolute;
......@@ -60,8 +68,12 @@
}
html[data-theme='dark'] {
.full-loading {
.full-loading:not(.light) {
background-color: @modal-mask-bg;
}
}
.full-loading.dark {
background-color: @modal-mask-bg;
}
</style>
<template>
<!-- eslint-disable vue/no-v-html -->
<div v-html="getHtmlData" :class="$props.class" class="markdown-viewer"></div>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { computed, defineProps } from 'vue';
import showdown from 'showdown';
const converter = new showdown.Converter();
......
......@@ -6,7 +6,7 @@
:openKeys="getOpenKeys"
:inlineIndent="inlineIndent"
:theme="theme"
@openChange="handleOpenChange"
@open-change="handleOpenChange"
:class="getMenuClass"
@click="handleMenuClick"
:subMenuOpenDelay="0.2"
......@@ -134,7 +134,9 @@
isClickGo.value = false;
return;
}
const path = (route || unref(currentRoute)).path;
const path =
(route || unref(currentRoute)).meta?.currentActiveMenu ||
(route || unref(currentRoute)).path;
setOpenKeys(path);
if (unref(currentActiveMenu)) return;
if (props.isHorizontal && unref(getSplit)) {
......
<template>
<Modal v-bind="getBindValue">
<Modal v-bind="getBindValue" @cancel="handleCancel">
<template #closeIcon v-if="!$slots.closeIcon">
<ModalClose
:canFullscreen="getProps.canFullscreen"
......@@ -72,6 +72,7 @@
import { basicProps } from './props';
import { useFullScreen } from './hooks/useModalFullScreen';
import { omit } from 'lodash-es';
import { useDesign } from '/@/hooks/web/useDesign';
export default defineComponent({
name: 'BasicModal',
......@@ -83,6 +84,7 @@
const visibleRef = ref(false);
const propsRef = ref<Partial<ModalProps> | null>(null);
const modalWrapperRef = ref<any>(null);
const { prefixCls } = useDesign('basic-modal');
// modal Bottom and top height
const extHeightRef = ref(0);
......@@ -175,7 +177,8 @@
// 取消事件
async function handleCancel(e: Event) {
e?.stopPropagation();
// 过滤自定义关闭按钮的空白区域
if ((e.target as HTMLElement)?.classList?.contains(prefixCls + '-close--custom')) return;
if (props.closeFunc && isFunction(props.closeFunc)) {
const isClose: boolean = await props.closeFunc();
visibleRef.value = !isClose;
......
......@@ -9,7 +9,8 @@ export default defineComponent({
name: 'Modal',
inheritAttrs: false,
props: basicProps,
setup(props, { slots }) {
emits: ['cancel'],
setup(props, { slots, emit }) {
const { visible, draggable, destroyOnClose } = toRefs(props);
const attrs = useAttrs();
useModalDragMove({
......@@ -18,8 +19,12 @@ export default defineComponent({
draggable,
});
const onCancel = (e: Event) => {
emit('cancel', e);
};
return () => {
const propsData = { ...unref(attrs), ...props } as Recordable;
const propsData = { ...unref(attrs), ...props, onCancel } as Recordable;
return <Modal {...propsData}>{extendSlots(slots)}</Modal>;
};
},
......
......@@ -97,7 +97,7 @@
}
}
& span:nth-child(2) {
& span:last-child {
&:hover {
color: @error-color;
}
......
......@@ -17,5 +17,6 @@
},
title: { type: String },
},
emits: ['dblclick'],
});
</script>
......@@ -54,7 +54,7 @@
}
&-content {
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
box-shadow: 0 4px 8px 0 rgb(0 0 0 / 20%), 0 6px 20px 0 rgb(0 0 0 / 19%);
}
&-footer {
......@@ -111,16 +111,19 @@
.ant-modal-confirm .ant-modal-body {
padding: 24px !important;
}
@media screen and (max-height: 600px) {
.ant-modal {
top: 60px;
}
}
@media screen and (max-height: 540px) {
.ant-modal {
top: 30px;
}
}
@media screen and (max-height: 480px) {
.ant-modal {
top: 10px;
......
......@@ -39,8 +39,8 @@
line-height: 44px;
background-color: @component-background;
border-top: 1px solid @border-color-base;
box-shadow: 0 -6px 16px -8px rgba(0, 0, 0, 0.08), 0 -9px 28px 0 rgba(0, 0, 0, 0.05),
0 -12px 48px 16px rgba(0, 0, 0, 0.03);
box-shadow: 0 -6px 16px -8px rgb(0 0 0 / 8%), 0 -9px 28px 0 rgb(0 0 0 / 5%),
0 -12px 48px 16px rgb(0 0 0 / 3%);
transition: width 0.2s;
&__left {
......
......@@ -5,7 +5,7 @@
:title="title"
v-bind="omit($attrs, 'class')"
ref="headerRef"
v-if="content || $slots.headerContent || title || getHeaderSlots.length"
v-if="getShowHeader"
>
<template #default>
<template v-if="content">
......@@ -99,6 +99,10 @@
];
});
const getShowHeader = computed(
() => props.content || slots?.headerContent || props.title || getHeaderSlots.value.length,
);
const getShowFooter = computed(() => slots?.leftFooter || slots?.rightFooter);
const getHeaderSlots = computed(() => {
......@@ -150,6 +154,7 @@
getClass,
getHeaderSlots,
prefixCls,
getShowHeader,
getShowFooter,
omit,
getContentClass,
......
......@@ -432,7 +432,7 @@
bottom: 0;
left: 0;
z-index: @preview-comp-z-index;
background: rgba(0, 0, 0, 0.5);
background: rgb(0 0 0 / 50%);
user-select: none;
&-content {
......@@ -458,7 +458,7 @@
overflow: hidden;
color: @white;
cursor: pointer;
background-color: rgba(0, 0, 0, 0.5);
background-color: rgb(0 0 0 / 50%);
border-radius: 50%;
transition: all 0.2s;
......@@ -470,7 +470,7 @@
}
&:hover {
background-color: rgba(0, 0, 0, 0.8);
background-color: rgb(0 0 0 / 80%);
}
}
......@@ -480,7 +480,7 @@
left: 50%;
padding: 0 22px;
font-size: 16px;
background: rgba(109, 109, 109, 0.6);
background: rgb(109 109 109 / 60%);
border-radius: 15px;
transform: translateX(-50%);
}
......@@ -494,7 +494,7 @@
height: 44px;
padding: 0 22px;
margin-left: -139px;
background: rgba(109, 109, 109, 0.6);
background: rgb(109 109 109 / 60%);
border-radius: 22px;
justify-content: center;
......@@ -526,12 +526,12 @@
height: 50px;
font-size: 28px;
cursor: pointer;
background-color: rgba(0, 0, 0, 0.5);
background-color: rgb(0 0 0 / 50%);
border-radius: 50%;
transition: all 0.2s;
&:hover {
background-color: rgba(0, 0, 0, 0.8);
background-color: rgb(0 0 0 / 80%);
}
&.left {
......
......@@ -88,7 +88,7 @@
}
.ant-image-preview-operations {
background-color: rgba(0, 0, 0, 0.4);
background-color: rgb(0 0 0 / 40%);
}
}
</style>
......@@ -148,7 +148,7 @@
display: none;
width: 0;
height: 0;
opacity: 0;
opacity: 0%;
}
}
}
......@@ -159,12 +159,12 @@
width: 0;
height: 0;
cursor: pointer;
background-color: rgba(144, 147, 153, 0.3);
background-color: rgb(144 147 153 / 30%);
border-radius: inherit;
transition: 0.3s background-color;
&:hover {
background-color: rgba(144, 147, 153, 0.5);
background-color: rgb(144 147 153 / 50%);
}
}
......@@ -174,8 +174,7 @@
bottom: 2px;
z-index: 1;
border-radius: 4px;
opacity: 0;
-webkit-transition: opacity 80ms ease;
opacity: 0%;
transition: opacity 80ms ease;
&.is-vertical {
......@@ -201,7 +200,7 @@
.scrollbar:active > .scrollbar__bar,
.scrollbar:focus > .scrollbar__bar,
.scrollbar:hover > .scrollbar__bar {
opacity: 1;
opacity: 100%;
transition: opacity 340ms ease-out;
}
</style>
......@@ -21,7 +21,7 @@
:overlayClassName="`${prefixCls}-menu-popover`"
v-else
:visible="getIsOpend"
@visibleChange="handleVisibleChange"
@visible-change="handleVisibleChange"
:overlayStyle="getOverlayStyle"
:align="{ offset: [0, 0] }"
>
......
......@@ -13,8 +13,8 @@
bottom: 0;
display: block;
width: 2px;
background-color: @primary-color;
content: '';
background-color: @primary-color;
}
}
......@@ -45,8 +45,8 @@
position: absolute;
top: 50%;
right: 18px;
transform: translateY(-50%) rotate(-90deg);
transition: transform @transition-time @ease-in-out;
transform: translateY(-50%) rotate(-90deg);
}
}
......@@ -128,12 +128,12 @@
position: relative;
z-index: 1;
display: flex;
align-items: center;
font-size: @font-size-base;
color: inherit;
list-style: none;
cursor: pointer;
outline: none;
align-items: center;
&:hover,
&:active {
......@@ -178,8 +178,8 @@
&-vertical &-submenu-collapse {
.@{submenu-popup-prefix-cls} {
display: flex;
justify-content: center;
align-items: center;
justify-content: center;
}
.@{menu-prefix-cls}-submenu-collapsed-show-tit {
flex-direction: column;
......@@ -188,7 +188,7 @@
&-vertical&-collapse &-item,
&-vertical&-collapse &-submenu-title {
padding: 0 0;
padding: 0;
}
&-vertical &-submenu-title-icon {
......@@ -244,8 +244,8 @@
left: 0;
width: 3px;
height: 100%;
background-color: @primary-color;
content: '';
background-color: @primary-color;
}
}
}
......@@ -276,8 +276,8 @@
left: 0;
width: 3px;
height: 100%;
background-color: @primary-color;
content: '';
background-color: @primary-color;
}
.@{menu-prefix-cls}-submenu-collapse {
......
......@@ -92,7 +92,7 @@
background-color: transparent;
border-color: @white;
border-style: solid;
border-width: 0 5px 0 5px;
border-width: 0 5px;
content: '';
}
......
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册